美文网首页前端开发那些事儿熊爸的学习时间
Vue.js备忘记录(三) 自定义指令, 组件化, 父子组件,组

Vue.js备忘记录(三) 自定义指令, 组件化, 父子组件,组

作者: 熊爸天下_56c7 | 来源:发表于2020-10-22 16:54 被阅读0次

    一. 自定义指令 //操作DOM

    有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

    1.什么是自定义指令:

    自己配置的指令

    2.什么时候使用自定义指令?

    当你不可避免的操作DOM时

    3.如何全局注册指令

    全局注册: 如果需要在多个组件中使用该指令,则应声明为全局

    参数一:指令名

    参数二:钩子函数

    Vue.directive('focus', {
        // 当被绑定的元素插入到 DOM 中时……
        inserted: function (el) {
          // 聚焦元素
          el.focus()
        }
      })
    

    4.钩子函数

    这里需要解释什么是钩子函数:

    一个指令定义对象可以提供如下几个钩子函数 (均为可选):

    • bind:一开始就执行 绑定阶段 ,在bind阶段 el无法拿到父元素 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

    • inserted:一开始就执行 绑定后插入父元素中的阶段 在inserted阶段 el可以拿到父元素 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

    • update:window更新时触发,其获取的是更新之前的DOM 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

    • componentUpdated:组件数据更改时触发,其获取的是更新之后的DOM.指令所在组件的 VNode 及其子 VNode 全部更新后调用。

    • unbind:只调用一次,指令与元素解绑时调用。

    5.钩子函数参数

    每个钩子函数都默认传入两个参数 el和binding

    el是调用此函数的DOM元素, binding是 v-指令 对象

    指令钩子函数会被传入以下参数:

    • el:指令所绑定的元素,可以用来直接操作 DOM 。

    • binding:一个对象,包含以下属性:

    • name:指令名,不包括 v- 前缀。

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。

    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。

    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。

    • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。

    • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

    6. 自定义指令函数简写

    在很多时候,你可能想在bind和update时触发相同行为,而不关心其它的钩子。

    Vue.directive('color-swatch', function (el, binding) {
      el.style.backgroundColor = binding.value
    })
    

    7. 使用

    指令的名字前面加 v-来调用 ,

    如果是驼峰命名法,则应该转为小写并用 - 连接

    如 autoFocus 引用时 v-auto-focus

    例如:使输入框聚焦

    Vue.directive('focus', {
      // 当被绑定的元素插入到 DOM 中时……
      inserted: function (el) {
        // 聚焦元素
        el.focus()
      }
    })
    

    8. v-指令:参数 = xxx 参数是如何传递的?

    例如:v-bind:class='xxx' 这样的指令怎么定义?怎么把参数class传入 指令生成器中呢?

    其实它是通过 第二个参数binding中的 arg传的

    xxx怎么传入的呢?

    其实它是通过 第二个参数binding中的value 传的

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <style>
            [v-cloak] {
                display: none;
            }
        </style>
    </head>
    
    <body>
        <div id="app" v-cloak>
            <input type="text" name="" id="" v-my-bind:canshu1="msg">
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script>
            Vue.directive('my-bind', {
                inserted: function (el,binding) {
                    console.log(el);
                    console.log(binding);
                    console.log(binding.arg);
                    console.log(binding.value);
                }
            })
            var vm = new Vue({
                data: {
                    msg: 'hello'
                }
            }).$mount('#app')
    
        </script>
    </body>
    
    </html>
    

    9. 私有自定义指令

    和全局的指令生成器差不多,只不过写在组件的 directives成员里面

    <h1 v-color>hello</h1>
    
            new Vue({
                el: '#app',
                data: {
    
                },
                directives: {
                    color: function (el, bingding) {
                        el.style.color = 'red'
                    }
                }
            })
    

    二. 组件化

    组件化思想就是将一个大视图拆分成一个个小的模块

    组件化可以方便开发和维护,同时方便复用

    组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:

    • template只能有一个根元素

    • template可以传值字符串,但是这样写没有高亮提醒😭

    • 可以用vue里单文件组件解决这一个问题 详见:单文件组件

    • 组件其实就是一个vue实例,所以他有自己的data methods等成员,也是一个独立的作用域

    • 但是,组件的data必须是个方法,方法返回一个对象作为组件的data

    • 组件可以被认为是js模块,所以存在组件间参数传递的问题

    1.组件使用步骤 //只是阐述原理,现在这种写法已经不太常见了

    (1).创建组件构造器

    用Vue的extend构造方法构造组件

        const myComponent =Vue.extend({   //我们用Vue的extend构造方法构造了一个组件
          template:`<li>这是个待办项</li>`
                //其最重要的属性就是模板,vue会使用这个模板来渲染HTML
        })
    

    现在这种写法已经不太常见了

    (2).注册组件

    Vue.component(id, [definition])

    
    Vue.component('myComponent', myComponent)
    

    (3).使用组件

    使用组件时注意,驼峰命名法会自动转为 - 命名法

    <my-component></my-component>
    

    2 组件定义的语法糖

    在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。在 Vue 中注册组件很简单:

    这里,我们把组件构造器直接传入了组件注册器了

    // 定义名为 todo-item 的新组件
    Vue.component('todo-item', {//(由于代码比较简单,这里,我们把组件构造器直接传入了组件注册器了)
      template: '<li>这是个待办项</li>'
    })
    
    var app = new Vue(...)
    

    现在你可以用它构建另一个组件模板:

    <ol>
      <!-- 创建一个 todo-item 组件的实例 -->
      <todo-item></todo-item>
    </ol>
    

    问题一: 感觉template写在模板里太乱了~~~!!

    可以按如下的方法提取出来

    • 在html的body里 先写一个template标签 命名好id

    • 再把模板内容放入

    • 在组件定于语法中写: template:'#id' 即可完成关联

    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8'>
      <meta name='viewport' content='width=device-width, initial-scale=1.0'>
      <title>Document</title>
    </head>
    <body>
      <div id='app'>
        <my-component></my-component>
      </div>
    
      <template id="cpn">
        <div>
          <li>这是个待办项</li>
          <li>这是个待办项</li>
        </div>
      </template>
      <script src='https://cdn.jsdelivr.net/npm/vue/dist/vue.js'></script>
      <script>
        Vue.component('myComponent', {
          template:'#cpn'
        })
        const app= new Vue({
          el:'#app',
        })
      </script>
    </body>
    </html>
    

    问题二: 这样写不算乱,但是模板如果写在body里,将来如何拆分文件????

    的确是这样,看来模板不能写在body里 还是要写在script里,如果写在script里,我们可以这样做:

    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8'>
      <meta name='viewport' content='width=device-width, initial-scale=1.0'>
      <title>Document</title>
    </head>
    <body>
      <div id='app'>
        <my-component></my-component>
      </div>
      <script src='https://cdn.jsdelivr.net/npm/vue/dist/vue.js'></script>
      <script>
        let template=`
        <div>
          <li>这是个待办项</li>
          <li>这是个待办项</li>
        </div>
        `
        Vue.component('myComponent', {
          template:template
        })
        const app= new Vue({
          el:'#app',
        })
      </script>
    </body>
    </html>
    

    这样写确实可以拆分了,但是在script里写html没有代码缩进和提示非常别扭,也不方便修改~!

    的确是这样!!想要改变这一情况,请看后续章节中的 现代化的解决方案.

    3.全局组件和局部组件

    • 全局组件定义在全局,在任意组件中都可以直接使用

    • 局部组件定义在局部,只能在当前组件中使用

    • 建议把通用组件定义为全局,把不通用的涉及具体业务的组件定义为局部

    • 全局注册也必须在Vue接管的作用域中用

    在此之前,我们先弄清楚一个问题:组件看成是一个Vue实例的话,它有没有data属性?

    当然了~~!!但是组件里的data必须是传入一个function,function里面再return对象出来 (这样做是为了防止组件多次调用时,如果他们在内存中指向同一个对象,会导致data内的数据被不同实例互相篡改)

    (1).全局注册

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="app">
            <my-component></my-component>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script>
            Vue.component('my-component', {
                template:'<p>{{msg}}</p>',
                data:function(){
                    return{
                        msg:'hello component!'
                    }
                }
            })
            const app = new Vue({
                data: {
                    message:'hello world',
                }
            }).$mount('#app')
        </script>
    </body>
    
    </html>
    

    (2) 局部组件

    首先明白子组件,子组件是声明在父组件的components属性内的组件,声明方法 如下:

            Vue.component('my-component1', {
                template: `
                <div>    
                    <input type="checkbox" v-model="seen">{{msg}}
                    <div :class="{box:seen}"></div>
                    <component2></component2>
                </div>
                `,
                data: function () {
                    return {
                        msg: 'hello component!',
                        seen:true
                    }
                },
                components:{
                    component2:{
                        template:"<h1>hello</h1>"
                    }
                }
            })
    

    这种声明方式使得component2只能在my-component1的作用域内被引用,无法被全局引用

    4.传参的组件

    上面的组件没有参数,生成的标签千篇一律,为了解决这个问题,我们可以给自己的组件传参

    Vue.component('todo-item', {
      // todo-item 组件现在接受一个
      // "prop",类似于一个自定义 attribute。
      // 这个 prop 名为 todo。
      props: ['todo'],
      template: '<li>{{ todo.text }}</li>'
    })
    

    这样,我们就可以生成不同内容的自定义组件了

    例如:

    <div id="app-7">
      <ol>
        <!--现在我们为每个 todo-item 提供 todo 对象 todo 对象是变量,即其内容可以是动态的。
          我们也需要为每个组件提供一个“key”,稍后再作详细解释。-->
        <todo-item
          v-for="item in groceryList"  
          v-bind:todo="item"
          v-bind:key="item.id"
        ></todo-item>
      </ol>
    </div>
    
    Vue.component('todo-item', {
      props: ['todo'],
      template: '<li>{{ todo.text }}</li>'
    })
    var app7 = new Vue({
      el: '#app-7',
      data: {
        groceryList: [
          { id: 0, text: '蔬菜' },
          { id: 1, text: '奶酪' },
          { id: 2, text: '随便其它什么人吃的东西' }
        ]
      }
    })
    

    在一个大型应用中,有必要将整个应用程序划分为组件

    <div id="app">
      <app-nav></app-nav>
      <app-view>
        <app-sidebar></app-sidebar>
        <app-content></app-content>
      </app-view>
    </div>
    

    三. 父子组件

    如果一个组件是另一个组件的局部组件,则我们称它为子组件,其外包组件为父组件.

    典型的父子组件结构:

      <script>
        let template=`
        <div>
          <li>这是个待办项</li>
          <li>这是个待办项</li>
        </div>
        `
        cpn={                        //子组件
          template:template,
          data() {
            return {
            }
          },
        }
    
        const app= new Vue({           //父组件  同时也是根组件
          el:'#app',
          data:{
          },
          components:{
            cpn
          }
        })
      </script>
    

    其实我们有一个组件通信大杀器:vuex,详见后续章节

    组件的初衷就是配合使用,最常见的就是形成父子关系,A中用B,所以他们之间必须通讯,父组件要能够给子组件下发数据,子组件也可能要将内部的事件告知父组件

    父子组件可以总结为prop向下传递,事件向上传递

    1.props //给子组件传递数据

    父组件调用子组件时,通过标签传参,子组件的props接收下发数据

    ①.在父组件template中找到子组件,通过声明属性的方式传递数据

    注意:如果需要动态传递值,必须用v-bind 如果只是想传个简单的值,可以直接写在标签里,如下:

    <todo-body  foo='bar'></todo-body>  
    <todo-body  v-bind:foo=todos></todo-body>
    

    ②.在子组件中声明成员props接收传递的参数 ,组件接收到的props数据后可以像访问data一样访问

    props可以传入数组,也可以传入对象 ,

    比较推荐传入对象,因为传入对象时,可以指定

    • 数据类型type

    • ,默认值default, //注意:如果type是数组或者对象,默认值必须是函数 return出来

    • 是否必须required

    props: ['foo']
    props:{
      movies: Array,
      count:{
          required:true
    },
      name:{
          type:String,
          default:'无名氏'
        }
    }
    

    支持的数据类型如下: 除此之外还可以要求是你自己的自定义类型

    还要注意一个问题!!!! 子组件props里的数据不要和子组件的表单做双向数据绑定!

    (因为他已经可以被父组件改变了!!!别再让自己的表单改变它)

    2.单项数据流

    prop是单项绑定的:当父组件的属性变化时,可以传递给子组件,反过来则不会,这种设计是为了放在子组件无意间改变父组件状态

    在子组件中,确实可以操作父组件传入的数据,但只能操作引用类型的数据,这跟JS数据存储原理有关,但是,非常不建议这么做

    普通类型不允许这样改, 实际上引用类型也不能用 = 号重新赋值

    3.子传父 //如何修改父组件数据

    合理的想法:子组件应该把数据给父组件,父组件接收后自己修改自己的数据

    那么,我们到底怎么修改父组件的数据呢?

    ① 在父组件中定义一个方法来修改自己的数据

    methods: {
                addTodo(titleText){
                    this.todos.push({
                        title:titleText,
                        done:false
                    })
                }
            },
    

    ②在子组件中发布一个事件,通知父组件

                methods: {
                    handleKeyUpEnter(e){
                        this.$emit('wantYouAddTodo', e.target.value)
                        this.title=''
                    }
                },
    

    ③在父组件使用子组件的标签上 用v-on订阅 子组件发布的自定义事件

    事件名就是子组件发射的事件名

    <todo-header  v-bind:todos=todos  @wantYouAddTodo=addTodo></todo-header>
    

    注意:虽然父元素的接收方法是带参数的,但v-on绑定的时候没有把参数写在指令中,系统会默认把参数传过去.

    整个过程采用了发布/订阅的通信方式.请体会.

    4. 综合练习

    接下来我们再举例: 子组件点击某个按钮,并通知父组件他点击了哪一个

    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8'>
      <meta name='viewport' content='width=device-width, initial-scale=1.0'>
      <title>Document</title>
    </head>
    <body>
      <div id='app'>
        <cpn :btns=buttons  @childclick=childclick></cpn>
        <h1>按钮{{child_be_clicked}}被点了</h1>
        
      </div>
      <script src='https://cdn.jsdelivr.net/npm/vue/dist/vue.js'></script>
      <script>
        let template=`
        <div>
          <button v-for="(btn, index) in btns" :key="btn.id" @click= btn_click(btn)>{{btn.name}}</button>
        </div>
        `
        cpn={
          template,
          props:{
            btns:{
              type:Array
            }
          },
          methods: {
            btn_click(btn){
              this.$emit('childclick', btn)
            }
          },
        }
        const app= new Vue({
          el:'#app',
          data:{
            child_be_clicked:'??',
            buttons:[
              {id:1,name:"OK"},
              {id:2,name:"cancle"},
              {id:3,name:"retry"}
            ]
          },
          components:{
            cpn
          },
          methods:{
            childclick(recive){
              this.child_be_clicked=recive.name
            }
          }
        })
      </script>
    </body>
    </html>
    

    5.父子组件互相访问

    直接获取对象

    (1)父访问子

    父访问子有两种方式:

    • $children 返回父组件的子组件,一般不建议这样做 (很少用,一般只用于拿到所有子组件)

    • refs 如果父组件调用子组件时,在标签里写个 ref='aaa' 则这个组件被存入父组件的ref中(常用,多用于拿到特定组件,但必须加ref键值)

    我们可以直接通过this.refs获取父组件的resf数组

    甚至用this.$refs.aaa访问它

    (2)子访问父

    子访问父: $parent (很少用.用了这个,子组件就不够独立了)

    (3) 子孙访问根组件

    $root

    四. 组件化思想拆分文件

    1.原生Vue方法

    index.html中没有任何标签,只留vue实例的入口标签,所有内容靠组件拼凑

    <!doctype html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Template • TodoMVC</title>
        <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
        <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
        <!-- CSS overrides - remove if you don't need it -->
        <link rel="stylesheet" href="css/app.css">
    </head>
    
    <body>
        <div id="app"></div> 
        <!-- index只留一个App入口 -->
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="components/todo-header.js"></script>
        <script src="components/todo-body.js"></script>
        <script src="components/todo-footer.js"></script>
        <script src="components/app-footer.js"></script>
        <script src="components/app.js"></script>
        <script src="main.js"></script>
        <!-- 所有js子组件按照先子后父的顺序引入 -->
    </body>
    
    </html>
    

    根据入口<divid="app"></div> ,我们就会进入main.js,寻找到这个实例,

    从某种意义上讲,main.js不算组件,是启动入口,我们把它放在根目录,而不放在components文件夹

    new Vue({   //这个vue实例对应着入口标签,接管了它
        el: '#app',
        template:'<App />', 
        //他的模板会替换掉<div id='app'></div>
        //<App />在哪定义的?原来是下面的子组件定义的
        components: {
            App
        }
    })
    

    顺理成章的.我们需要找到组件 App ,它定义在app.js中

    ; (function () {
        const template = `
        <div>
            <section class="todoapp">
                <todo-header></todo-header>
                <todo-body></todo-body>
                <todo-footer></todo-footer>
            </section>
            <app-footer></app-footer>
        </div>
        `
        //把template单独拿出来,更易于阅读
        //阅读上面的结构,我们发现,App里还是一个个自定义组件
        //为了让节点入口保持统一,以防将来操作DOM,我们给最外层div加了id为app
        window.App = {    
            //使用函数包裹,window调用是为了防止各个template命名冲突
            //现在这个template是window.App.template
            //相当于main.js调用的App是window提供的,
            //后续我们会有方法解决这个问题
            template,
            components: {
                TodoHeader,
                TodoBody,
                TodoFooter,
                AppFooter,
            }
            //这是App的子组件声明
        }
    })()
    

    于是,我们顺藤摸瓜找到了其他组件,它们和App组件结构相同.

    整个项目的结构为:

    2. 借用单文件页面系统

    五. 路由状态切换写入生命周期

    上一篇中的路由状态切换中,我们通过this.app将window.location.hash传递给

    this指代的是window, 它里面有一个app实例.

    改为组件化之后我们还有这个实例吗? 在调用时,我们可以把组件看成一个实例,但我们的组件文件里写的其实是组件的声明(类对象), 那我们怎么把window.location.hash传递给它?

    这时候,我们需要把相对应的函数写进组件的生命周期之中

    绑定在created周期是因为它是第一个拿到Vue组件的data的钩子

            created() {
                window.onhashchange=()=>{
                    this.filterText=window.location.hash
                }
            },  //这段代码写在todos-body组件中 需要app下发filterText
    

    六. Element组件库

    element是基于Vue的第三方组件库,他能帮我们更加快速的构建应用

    The world's most popular Vue UI framework​element.eleme.cn

    安装

    
    cnpm i element-ui -S
    

    或者CDN

    目前可以通过 unpkg.com/element-ui 获取到最新版本的资源,在页面上引入 js 和 css 文件即可开始使用。

    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    

    举例:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <!-- 引入Vue -->
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <!-- 引入element样式 -->
        <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
        <!-- 引入element组件库 -->
        <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    </head>
    
    <body>
        <div id="app">
            <template>
                <div class="block">
                  <span class="demonstration">请选择日期</span>
                  <el-date-picker
                    v-model="value1"
                    type="date"
                    placeholder="选择日期">
                  </el-date-picker>
                </div>
              </template>
            <el-rate v-model="rate" show-text></el-rate>
            <el-button type="primary" @click=handleClick icon="el-icon-star-on">弹出评分</el-button>
        </div>
        <script>
            var vm = new Vue({
                data: {
                    msg: 'hello',
                    rate:4,
                    value1:null,
                    value2:null
    
                },
                methods: {
                    handleClick(){
                        window.alert('当前评分:'+this.rate+'评分日期'+this.value1)
                    }
                },
            }).$mount('#app')
        </script>
    </body>
    
    </html>
    

    相关文章

      网友评论

        本文标题:Vue.js备忘记录(三) 自定义指令, 组件化, 父子组件,组

        本文链接:https://www.haomeiwen.com/subject/xtxpmktx.html