组件使用v-model、$listeners、.sync(区别于v-model的双向数据绑定)

自定义组件

  1. 自定义组件的v-model

    首先我们先说一下在自定义组件中使用v-model的必要条件

    • 在自定义的组件中要有input(这里我们先不讨论单选复选框)
    • 在自定义组件的模板对象中要有props属性,且里面要含有一个value
    • 在自定义组件的input标签上要绑定value属性值为props中传入的值,且还需要发出一个input事件

    这样讲可能会有点难理解,还是上代码吧...

    <div id="app">
      <child-com v-model="message"></child-com>
      <span>{{ message }}</span>
    </div>
    <template id="childCom">
      <div>
        <input type="text" :value="value" @input='inputEvent'>
      </div>
    </template>
    
    <script>
      const childCom = {
        template: '#childCom',
        props: ['value'],
        methods: {
          inputEvent(event) {
            this.$emit('aaa', event.target.value)
          }
        },
      }
    
      const vm = new Vue({
        el: '#app',
        data: {
          message: '可以双向绑定的了'
        },
        components: {
          childCom
        }
      })
    </script>
    

    这是最终实现效果需要必备的,看完这些代码如果你是小白,你可能会有点不理解为什么要这样做,下面我告诉你原理。

    首先在我们使用的v-model 中,其内部实现的原理就是一个 value属性和一个input事件,其主要步骤就是,用v-bind绑定value,然后用input事件监听值的变化,当文本框中的值发生变化的时候,input事件就会触发,那么我们可以在input事件中获取到改变后的值然后赋值给value,这样是不是就完成了双向数据绑定了。上代码:

    <div id="app">
      <input type='text' :value='message' @input='inputEvent'>
      <span>{{ message }}</span>
    </div>
    
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          message: '可以双向绑定的了'
        },
        methods: {
          inputEvent(event) {
            this.message = event.target.value;
          }
        }
      })
    </script>
    

    就这样几个步骤,就达到了v-model的效果了,这就是他的原理,然后让我们深一步想,让自定义组件使用双向数据绑定。因为我们知道其内部就是value和input事件,

    所以有了如下代码:

    <div id="app">
        <child-com :value='message' @input='message=$event'></child-com> <!-- 此代码就这里和最开始代码不同 -->
        <span>{{ message }}</span>
      </div>
    
      <template id="childCom">
        <div>
          <input type="text" :value="value" @input='inputEvent'>
        </div>
      </template>
    
      <script>
        const childCom = {
          template: '#childCom',
          props: ['value'],
          methods: {
            inputEvent(event) {
              this.$emit('input', event.target.value)
            }
          },
        }
    
        const vm = new Vue({
          el: '#app',
          data: {
            message: '可以双向绑定的了'
          },
          components: {
            childCom
          }
        })
    </script>
    

    根据上面的原理,现在你应该知道了为什么要传一个value在子组件了吧,明白之后,您就可以把 <child-com :value='message' @input='message=$event'></child-com> 替换成 <child-com v-model="message"></child-com> 了。

  2. $listeners的使用

    由来:当我们在项目开发过程中会出现很多组件嵌套的关系,那么如果还要在最外层的组件向内部传递数据的话,有如下几种方式:

    • 从父向子传递,子再向孙传递,一直传递下去,那么最里面的组件想往最外层传东西则可以从最里面向外面逐层$emit发送出去,但是仔细想想,一个简单的传递信息,却涉及到了这之间所有的组件,而他们只是一个中间者,这让代码维护起来非常困难
    • 使用vuex来进行传递,这样确实方便了很多,但是这样做如果没有其他用处的话就有点大材小用了
    • 使用事件总线,这样使用也不容易维护

    $listeners$attrs 的出现,就完美的解决了第一种情况的发生

    <div id="app">
        <child-com :name='name' :age='age' @test-listeners='testListeners'></child-com>
      </div>
    
      <script>
        const vm = new Vue({
          el: '#app', //  父组件
          data: {
            name: 'lyl',
            age: 20,
          },
          methods: {
            testListeners(arg) {
              console.log(arg)
            }
          },
          components: {
            childCom: { //  子组件
              inheritAttrs: false,
              template: `
                <div>
                  <span> {{name}} </span>
                  <grand-com v-bind='$attrs' v-on='$listeners'></grand-com>
                </div>
              `,
              props: ['name'],
              components: {
                grandCom: { //  孙子组件
                  inheritAttrs: false,
                  template: `
                    <div>
                      <span @click='listenClick'>{{$attrs.age}}</span>
                    </div>
                  `,
                  methods: {
                    listenClick() {
                      this.$emit('test-listeners','aaaaaaa');
                    }
                  },
                }
              }
            }
          }
        })
    </script>
    

    上面代码中,孙子组件要发出一个是将让父组件调用,这个时候我们可以在中间过渡的子组件模板使用的孙子组件上绑定这个属性,即:v-on='$listeners',这样一来父组件就能直接调用孙子组件发出的方法了,并且在中间层的子组件上并没有什么多余的部分

  3. .sync的使用方法

    我们都知道,在一个组件上,我们只能使用一个v-model,但是如果我们的组件中有多个input标签呢,并且每个input标签中的值都不同,且每个都想进行双向绑定,这个时候,v-model就不行了。于是乎就出现了.sync的出现。

    根据上面我说的那些需求,我们写一下代码:

    • 不使用.sync的代码
    <div id="app">
        <child-com 
          :value='obj.value' @aaa='obj.value = $event'
          :name='obj.name'  @bbb='obj.name = $event'
          :age='obj.age' @ccc='obj.age = $event'>
        </child-com>
    
        <p>{{ obj }}</p>
        <p>{{ obj.value }}</p>
        <p>{{ obj.name }}</p>
        <p>{{ obj.age }}</p>
      </div>
    
    <!-- childCom组件的模板 -->
      <template id="childCom">
        <div>
          <input type="text" :value="value" @input='inputValueEvent'>
          <br>
          <input type="text" :value="name" @input='inputNameEvent'>
          <br>
          <input type="text" :value="age" @input='inputAgeEvent'>
        </div>
      </template>
    
      <script>
        const childCom = {
          template: '#childCom',
          props: ['value','name','age'],
          methods: {
            inputValueEvent(event) {
              this.$emit('aaa', event.target.value)
            },
            inputNameEvent(event) {
              this.$emit('bbb', event.target.value)
            },
            inputAgeEvent(event) {
              this.$emit('ccc', event.target.value)
            }
          },
        }
    
        const vm = new Vue({
          el: '#app',
          data: {
            obj: { value: '双向绑定' , name: 'coderlyl' , age: 20}
          },
          components: {
            childCom
          }
        })
    </script>
    

    根据上面的代码,我们不难发现,我们在标签中书写了过多的重复的东西,可读性也不是很好,下面我们再使用 .sync 的方式

    • 使用.sync的代码
    <div id="app">
        <child-com v-bind:value.sync='obj.value' 
                   v-bind:name.sync="obj.name" 
                   v-bind:age.sync="obj.age">	<!--这里也发生了变化-->
        </child-com>
    
        <p>{{ obj }}</p>
        <p>{{ obj.value }}</p>
        <p>{{ obj.name }}</p>
        <p>{{ obj.age }}</p>
      </div>
      <template id="childCom">
        <div>
          <input type="text" :value="value" @input='inputValueEvent'>
          <br>
          <input type="text" :value="name" @input='inputNameEvent'>
          <br>
          <input type="text" :value="age" @input='inputAgeEvent'>
        </div>
      </template>
    
      <script>
        const childCom = {
          template: '#childCom',
          props: ['value','name','age'],
          methods: {
            inputValueEvent(event) {
              this.$emit('update:value', event.target.value) // 这里发生了变化
            },
            inputNameEvent(event) {
              this.$emit('update:name', event.target.value) // 这里发生了变化
            },
            inputAgeEvent(event) {
              this.$emit('update:age', event.target.value) // 这里发生了变化
            }
          },
        }
    
        const vm = new Vue({
          el: '#app',
          data: {
            message: '可以双向绑定的了',
            obj: { value: '双向绑定' , name: 'coderlyl' , age: 20}
          },
          components: {
            childCom
          }
        })
    </script>
    

    也许看完这里,你并没有觉得好到哪里去了,下面还有更简单的写法

    <child-com v-bind.sync="obj"></child-com>
    <!-- 其他代码一样 -->
    

    对,没错!这是终极简化版,但是这只针对于对象才能用

    注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model

    v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

posted @ 2020-01-19 21:01  scriptLiu  阅读(...)  评论(...编辑  收藏