• 深入理解 Vuejs 组件


    本文主要归纳在 Vuejs 学习过程中对于 Vuejs 组件的各个相关要点。由于本人水平有限,如文中出现错误请多多包涵并指正,感谢。如果需要看更清晰的代码高亮,请跳转至我的个人站点的 深入理解 Vuejs 组件 查看本文。

    组件使用细节

    is属性

    我们通常使用 is 属性解决模板标签 bug 的问题。下面我们通过一个 table 标签的 bug 案例进行说明。
    我们先写一个简单的 Vue 实例,并创造一个 row 的组件,将它的模板 template 置为 '<tr><td>this is a row</td></tr>',按照下面的示例进行放置。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>is属性</title>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
    </head>
    <body>
    
      <div id="app">
        <table>
          <tbody>
            <row></row>
            <row></row>
            <row></row>
          </tbody>
        </table>
      </div>
    
      <script>
    
        Vue.component('row',{
          template: '<tr><td>this is a row</td></tr>'
        })
    
        var vm = new Vue({
          el: "#app"
        })
      </script>
    
    </body>
    </html>
    

    JSbin 预览

    该示例中,由于 H5 的规范 table 标签下 tbody 下只能是 tr,所以浏览器在渲染的时候出了问题。可以看到组件row渲染的 this is a row 都跑到了 table 之外。

     

    解决这个问题的方法就是,我们按照规范在 tbody 之下使用 tr 。但我们用 is= 将 tr变成 row 组件。

      <div id="app">
        <table>
          <tbody>
            <tr is="row"></tr>
            <tr is="row"></tr>
            <tr is="row"></tr>
          </tbody>
        </table>
      </div>
    

    JSbin 预览

    这样我们在遵循规范的同时,也使用了 Vuejs 的组件模板。可以看到接下来的浏览器 DOM 渲染已经正常。

     

    在使用 ul 时,同样建议使用 li 与 is=,而不是直接使用组件模板。
    在使用 select 时,同样建议使用 option 与 is=,而不是直接使用组件模板。

    子组件 data

    我们还是通过上面这个已有的案例进行演示。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Document</title>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
    </head>
    <body>
    
      <div id="app">
        <table>
          <tbody>
            <tr is="row"></tr>
            <tr is="row"></tr>
            <tr is="row"></tr>
          </tbody>
        </table>
      </div>
    
      <script>
    
        Vue.component('row',{
          data: {
            content: 'this is a row'
          },
          template: '<tr><td>{{content}}</td></tr>'
        })
    
        var vm = new Vue({
          el: "#app"
        })
      </script>
    
    </body>
    </html>
    

    经过之前的修改,我们将 tr 标签的 bug 解决掉了,并进行了修正。我们想在 Vue.component 的子组件中,添加数据 data ,并在模板 template 中使用插值表达式使用该数据内容。但这种写法打开浏览器是没有任何显示的。

    因为在子组件中定义 data 时,data 必须是一个函数 functionreturn 值为一个对象。而不能直接是一个对象。因为子组件不像根组件,只会被调用一次,可能在不同的地方被调用多次。所以通过函数 function 来让每一个子组件都有独立的数据存储,就不会出现多个子组件相互影响的情况。

    即在子组件中正确的写法应该是:

        Vue.component('row',{
          data: function(){
            return {
              content: 'this is a row'
            }
          },
          template: '<tr><td>{{content}}</td></tr>'
        })
    

    ref 引用

    在 Vuejs 中,使用 ref 引用的方式,可以找到相关的 DOM 元素。
    在 div 标签中添加一个 ref="hello",标记这个标签的引用名为 hello。并给他绑定一个事件,在点击它之后,输出出这个引用节点的 innerText

    <body>
    
      <div id="app">
        <div ref="hello" @click="handleClick">hello world</div>
      </div>
    
      <script>
    
        var vm = new Vue({
          el: "#app",
          methods: {
            handleClick: function(){
              alert(this.$refs.hello.innerText)
            }
          }
        })
      </script>
    
    </body>
    



    而当在一个组件中去设置 ref ,然后通过 this.$refs.name 获取 ref 里面的内容时,这个时候获取到的内容是子组件内容的引用。

    参考下面的计数器求和案例

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>计数器求和</title>
      <script src="./vue.js"></script>
    </head>
    <body>
    
      <div id="app">
        <counter ref="one" @change="handleChange"></counter>
        <counter ref="two" @change="handleChange"></counter>
        <div>{{total}}</div>
      </div>
    
      <script>
    
        Vue.component('counter',{
          template: '<div @click="handleClick">{{number}}</div>',
          data: function(){
            return {
              number: 0
            }
          },
          methods: {
            handleClick: function(){
              this.number ++
              this.$emit('change')
            }
          }
        })
    
        var vm = new Vue({
          el: "#app",
          data: {
            total: 0
          },
          methods: {
            handleChange: function(){
              this.total = this.$refs.one.number + this.$refs.two.number
            }
          }
    
        })
      </script>
    
    </body>
    </html>
    

    JSbin 预览

    在子组件中,绑定了 handleClick 事件使其每次点击后自增1,并且发布 $emit 将 change 传递给父组件,在组件中监听 @change ,执行 handleChange 事件,在父组件 methods 中设置 handleChange 事件,并使用 this.$refs.one.number 来获取子组件内容的引用。

    父子组件的数据传递

    Vue 中的单向数据流:即子组件只能使用父组件传递过来的数据,不能修改这些数据。因为这些数据很可能在其他地方被其他组件进行使用。

    所以当子组件在收到父组件传递过来的数据,并在后续可能要对这些数据进行修改时。可以先将 props 里面获取到的数据,在子组件自己的 data 的 return 中使用一个 number 进行复制,并在后续修改这个 number 即可。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>父子组件传值</title>
      <script src="./vue.js"></script>
    </head>
    <body>
    
      <div id="app">
        <counter :count="0"></counter>
        <counter :count="1"></counter>
      </div>
    
      <script>
    
        var counter = {
          props: ['count'],
          //在 data 的 return 中复制一份 父组件传递过来的值
          data: function(){
            return {
              number: this.count
            }
          },
          template: '<div @click="handleClick">{{number}}</div>',
          methods: {
            handleClick: function(){
              // 在子组件中不修改父组件传递过来的 count,而是修改自己 data 中的 number
              this.number ++
            }
          }
        }
    
        var vm = new Vue({
          el: "#app",
          components: {
            counter: counter
          }
    
        })
      </script>
    
    </body>
    </html>
    

    JSbin 预览

    传值总结

    • 父组件通过属性的形式向子组件进行传值
    • 子组件通过事件触发的形式向父组件传值
    • 父子组件传值时,有单向数据流的规定。父组件可以向子组件传递任何的数据,但子组件不能修改父组件传递过来的数据。如果一定要进行修改,只能通过修改复制副本的方式进行。

    组件参数校验 和 非props特性

    组件参数校验

    当子组件接收父组件数据类型要进行参数校验时,是可以通过组件参数校验来定义规则的。例如限制子组件接收父组件传递数据的类型,此时props 后不再使用数组,而是使用对象。

    • 当传递过来的值只接收数字类型时
     



    • 当传递过来的值只接收字符串类型时
     



    • 当传递过来的值既可以接收字符串类型、也可以接收数字类型时
     



    当做了组件参数校验,在传递过程中如果传递了组件参数校验规定之外的类型,就会报错(这里我们传递的是一个 Object)。

     



    自定义校验器

    props 中的 content 之后也可以写成对象形式,设置更多的参数。

        Vue.component('child',{
          props: {
            content: {
              type: String,
              required: false,   //设置是否必须传递
              default: 'default value'  //设置默认传递值  -- 无传递时传递的默认值
            }
          },
          template: '<div>{{content}}</div>'
        })
    

    type 设置传递类型;required 设置是否必须传递,false 为非必须传递;default 设置默认传递值,在无传递时,传递该值。

     

    当父组件调用子组件传递了 content 时,默认值便不会生效。

     




    除此之外,还可以限制传递字符串的长度等等。借助 validator

        Vue.component('child',{
          props: {
            content: {
              type: String,
              //对传入属性通过校验器要求它的长度必须大于5
              validator: function(value){
                return value.length > 5
              }
            }
          },
          template: '<div>{{content}}</div>'
        })
    

    在这个案例中,传入的属性长度必须超过 5 ,如果没有就会出现报错。

     



    prop 特性 与 非 props 特性

    prop 特性

    props 特性: 当父组件使用子组件时,通过属性向子组件传值,恰好子组件也声明了对父组件传递过来的属性的接收。即当父组件调用子组件时,传递了 content;子组件在 props 里面也声明了 content。所以父子组件有一个对应的关系。这种形式的属性,就称之为 props 特性。

    prop 特性特点:

    • 属性的传递,不会在 DOM 标签进行显示
    • 当父组件传递给子组件之后,子组件可以直接通过插值表达式或者通过 this.content取得内容。

    非 props 特性

    非 props 特性:父组件向子组件传递了一个属性,子组件并没有 props 的内容,即子组件并没有声明要接收父组件传递过来的内容。

     

    非 prop 特性特点:

    • 无法获取父组件内容,因为没有声明
    • 属性会展示在子组件最外层的 DOM 标签的 HTML 属性里。
     



    原生事件

    在下面示例中,代码这么书写在点击 Child 的时候,事件是不会被触发的。因为这个 @click 事件实际上是绑定的一个自定义的事件。但真正的鼠标点击 click 事件并不是绑定的这个事件。

    <body>
    
      <div id="app">
        <child @click="handleClick"></child>
      </div>
    
      <script>
    
        Vue.component('child', {
          template: '<div>Child</div>'
        })
    
        var vm = new Vue({
          el: "#app",
          methods: {
            handleClick: function(){
              alert('click')
            }
          }
    
        })
      </script>
    
    </body>
    



    如果我们想要触发这个自定义的 click 事件,应该把 @click 写到子组件的 template 中的 div 元素上。我们将代码改写成下面的样子,点击 Chlid 弹出 chlidClick。因为在 div 元素上绑定的事件是原生的事件,而之前在 child 上绑定的事件是监听的一个自定义事件。

    <body>
    
      <div id="app">
        <child @click="handleClick"></child>
      </div>
    
      <script>
    
        Vue.component('child', {
          template: '<div @click="handleChildClick">Child</div>',
          methods: {
            handleChildClick: function(){
              alert('chlidClick')
            }
          }
        })
    
        var vm = new Vue({
          el: "#app",
          methods: {
            handleClick: function(){
              alert('click')
            }
          }
    
        })
      </script>
    
    </body>
    



    而自定义事件,只有通过 this.$emit 去触发。

    <body>
    
      <div id="app">
        <child @click="handleClick"></child>
      </div>
    
      <script>
    
        Vue.component('child', {
          template: '<div @click="handleChildClick">Child</div>',
          methods: {
            handleChildClick: function(){
              alert('chlidClick')
              this.$emit('click')
            }
          }
        })
    
        var vm = new Vue({
          el: "#app",
          methods: {
            handleClick: function(){
              alert('click')
            }
          }
    
        })
      </script>
    
    </body>
    

    组件监听内部原生事件

    通常使用在 @click 之后加上 .native 修饰符达到直接在组件上监听原生事件的效果。

    <body>
    
      <div id="app">
        <child @click.native="handleClick"></child>
      </div>
    
      <script>
    
        Vue.component('child', {
          template: '<div>Child</div>'
        })
    
        var vm = new Vue({
          el: "#app",
          methods: {
            handleClick: function(){
              alert('click')
            }
          }
    
        })
      </script>
    
    </body>
    

    非父子组件间传值

    将左侧的网页用右侧的图进行表示。即细分组件之后,再进行二次细分。

     



    当出现以下情况,第二层的一个组件要跟第一层的大组件进行通信。这个时候就是父子组件的传值。即父组件通过 props 向子组件传值,子组件通过事件触发向父组件传值。

     



    当第三层的组件要和第一层的大组件进行通信。甚至两个不同二层组件下的三层组件要进行通信时。应该采用什么方法呢?
    这时显然就不能使用逐层传递的方式了。因为这样的操作会使得代码非常的复杂。

     

    既然不是父子组件之间传值,说明这两个组件之间不存在父子关系。如之前提到的第三层的组件要向第一层的大组件进行传值,两个不同二层组件下的三层组件要进行传值。这些都是非父子组件传值。

    解决方案

    一般有两种方式来解决 Vue 里面复杂的非父子组件之间传值的问题。

    • 一种是借助 Vue 官方提供的一种数据层的框架 Vuex。
    • 另一种是使用 发布 / 订阅 模式来解决非父子组件之间传值的问题,也被称之为 总线机制。
      下面着重讲解如何使用 总线机制 解决非父子组件之间传值的问题。

    Bus / 总线 / 发布订阅模式 / 观察者模式

    通过一个案例来实现该模式,当点击 even 时,yao 变为 even。当点击 yao 时,even变为 yao

    首先 new 一个 Vue 的实例,将其赋值给 Vue.prototype.bus。即给 Vue.prototype 上挂载了一个名为 bus 的属性。这个属性,指向 Vue 的实例。只要在之后,调用 new Vue()或者创建组件的时候,每一个组件上都会拥有 bus 这个属性。都指向同一个 Vue 的实例。

    通过 this.bus.$emit 向外触发事件,再借助生命周期钩子 mounted 通过 this.bus.$on监听事件。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>非父子组件间传值 Bus/总线/发布订阅模式/观察者模式)</title>
      <script src="./vue.js"></script>
    </head>
    <body>
    
      <div id="app">
        <child content="even"></child>
        <child content="yao"></child>
      </div>
    
      <script>
        // new 一个 Vue 的实例,将其赋值给 Vue.prototype.bus
        Vue.prototype.bus = new Vue()
    
        Vue.component('child',{
          data: function(){
            return {
              //因为不能改变传递过来的值 所以复制一份
              selfContent: this.content
            }
          },
          props: {
            content: String
          },
          template: '<div @click="handleClick">{{selfContent}}</div>',
          methods: {
            handleClick: function(){
              //实例上挂载的bus,通过 $emit 方法向外触发事件
              this.bus.$emit('change',this.selfContent)
            }
          },
          //借助生命周期钩子 通过 $on 方法 监听 change 事件
          mounted: function(){
            var _this = this
            this.bus.$on('change',function(msg){
              _this.selfContent = msg
            })
          }
        })
    
        var vm = new Vue({
          el: "#app"
        })
      </script>
    
    </body>
    </html>
    

    JSbin 预览

    插槽 slot

    子组件除了展示 p 标签中的 hello 之外,还需要展示一块内容,而这部分内容不是子组件决定的,而是父组件传递过来的。如果使用 v-html 搭配 content 属性传递值,会出现外部必须包裹 div 的问题。这个场景就应该使用插槽 slot

    在父组件使用 child 的时候,在标签内部,使用 h1 标签,并写入 yao 。这样就可以了。

      <div id="app">
        <child>
          <h1>yao</h1>
        </child>
      </div>
    

    在子组件的模板里,使用 <slot></slot> 就可以使之前写入的 yao 显示出来了。

        Vue.component('child',{
          template: '<div><p>hello</p><slot></slot></div>'
        })
    
        var vm = new Vue({
          el: "#app"
        })
    

    JSbin 预览

    除此之外, <slot></slot> 之前还可以添加默认内容,即 <slot>默认内容</slot>。添加默认内容的时候,如果在父组件使用子组件时,不传递插槽内容的话,就会显示默认内容,如果父组件使用子组件时,传递了插槽内容的话,就会显示传递的插槽内容,而不会显示默认内容。

    具名插槽

    当我有多个 slot 插槽需要进行填充的时候,可以使用具名插槽,即给插槽命名。例如下列示例中的 header 和 footer 都是由外部传递的情况。

    在父组件使用子组件的过程中,给插槽添加 slot="" 属性,对应之后的插槽命名。

      <div id="app">
        <body-content>
          <header slot="header">header</header>
          <footer slot="footer">footer</footer>
        </body-content>
      </div>
    

    在 slot 中使用 name="" 给插槽命名。

        Vue.component('body-content',{
          template: `<div>
                        <slot name="header"></slot>
                        <div class="content">content</div>
                        <slot name="footer"></slot>
                     </div>`
        })
    
        var vm = new Vue({
          el: "#app"
        })
    

    JSbin 预览

    具名插槽同样可以拥有默认值。

    作用域插槽

    当子组件做循环,或者某一部分的 DOM 结构是由外部传递进来时,使用作用域插槽。

    作用域插槽必须是 template 开头和结尾的内容,同时这个插槽声明从子组件接收的数据都放在 props 里面,然后通过相应的模板对子组件进行展示。

      <div id="app">
        <child>
          <template slot-scope="props">
            <li>{{props.item}} - hello</li>
          </template>
        </child>
      </div>
    
        Vue.component('child',{
          data: function(){
            return {
              list: [1,2,3,4]
            }
          },
          template: `<div>
                        <ul>
                          <slot v-for="item of list"
                                :item=item
                          ></slot>
                        </ul>
                     </div>`
        })
    
        var vm = new Vue({
          el: "#app"
        })
    

    JSbin 预览

    动态组件 与 v-once 指令

    下面代码可以实现点击 button 按钮的切换效果,除了这种方式之外,还可以使用动态组件的方式实现。

    <body>
      <div id="app">
        <child-one v-if="type === 'child-one'"></child-one>
        <child-two v-if="type === 'child-two'"></child-two>
        <button @click="handleBtnClick">change</button>
      </div>
    
      <script>
    
        Vue.component('child-one',{
          template: '<div>child-one</div>'
        })
    
        Vue.component('child-two',{
          template: '<div>child-two</div>'
        })
    
        var vm = new Vue({
          el: "#app",
          data: {
            type: 'child-one'
          },
          methods: {
            handleBtnClick: function(){
              this.type = this.type === 'child-one' ? 'child-two' : 'child-one'
            }
          }
        })
      </script>
    </body>
    

    JSbin 预览

    动态组件

    使用 component 标签,并使用 :is 绑定数据。即可以实现上面示例中相同的效果。
    即根据 :is 对应值的变化,自动的加载不同的组件。

      <div id="app">
        <component :is="type"></component>
        <!-- <child-one v-if="type === 'child-one'"></child-one>
        <child-two v-if="type === 'child-two'"></child-two> -->
        <button @click="handleBtnClick">change</button>
      </div>
    

    JSbin 预览

    v-once 指令

    在 Vue 中通过 v-once 指令可以提高静态内容的展示效率。例如上面的示例中,当我们不使用动态组件而使用下面的方式进行组件调用的时候。每次点击 button ,都会摧毁当前组件,然后创建一个新的组件。

      <div id="app">
        <child-one v-if="type === 'child-one'"></child-one>
        <child-two v-if="type === 'child-two'"></child-two>
        <button @click="handleBtnClick">change</button>
      </div>
    
        Vue.component('child-one',{
          template: '<div>child-one</div>'
        })
    
        Vue.component('child-two',{
          template: '<div>child-two</div>'
        })
    

    如果我们在这两个组件模板中加上 v-once 指令。在 child-one 和 child-two 第一次渲染的时候,就会被放入内存之中。当进行切换的时候,就并不需要重新创建一个组件了,而是从内存中去拿出以前的组件,所以性能更高。

        Vue.component('child-one',{
          template: '<div v-once>child-one</div>'
        })
    
        Vue.component('child-two',{
          template: '<div v-once>child-two</div>'
        })
  • 相关阅读:
    Java 设计模式之桥接模式,Java 桥接模式 ,java Bridge Pattern
    Java判断Object对象是否为数组,Java判断Object对象是否为集合,Java判断数组是否包含某个值
    Java Map转二维数组,Map转数组
    Java 设计模式之装饰模式,Java 装饰模式,java装饰模式和代理模式的区别
    Java 设计模式之适配器模式,Java 类适配器,Java 对象适配器
    获取List<Map<String, Object>中Map的属性值列表,获取所有map对象的某个属性列表
    Mybatis 一级缓存,Mybatis 二级缓存,Mybatis 缓存失效
    Java 设计模式之代理模式,Java 静态代理,Java 动态代理
    MongoDB安装和使用,MongoDB Like查询,Or查询,分页查询
    docker 安装 showdoc
  • 原文地址:https://www.cnblogs.com/evenyao/p/9630719.html
Copyright © 2020-2023  润新知