浅析 Vue 修饰符 sync

121 阅读3分钟

在什么情况下我们需要用到 sync?

在一些情况下,我们需要对组件的 prop 进行双向绑定,即实例和组件都能修改某个 prop。

但 Vue 中规定了,组件不能修改 props 外部数据,因为这会带来一些维护上的问题。此时在正常情况下,我们应该使用 Vue 中内置的 eventBus API $emit$event

Vue.component('Child', {
  template: `
    <div v-if="!hidden">
    这是子元素
    <button @click="$emit('update:hidden', true)">隐藏子元素</button>
    </div>
  `,
  props: ['hidden']
})

new Vue({
    el: '#app',
    template: `
      <div>
      <button @click="hiddenChild=!hiddenChild">显示/隐藏子元素</button>
      <hr>
      <Child :hidden="hiddenChild" @update:hidden="hiddenChild=$event"/>
      </div>
    `,
    data: {
      hiddenChild: true
    }
  }
)

在这个实例中,我们可以分别点击父元素的button、子元素的button来修改hiddenCihld属性,从而实现子元素的隐藏。

通过$emit来触发update:hidden事件,并传入一个参数true

@update:hidden来接收update:hidden事件,并用$event来表示接收到的参数。

另外,$emittemplate中使用不能加this,在methods中使用必须加this

methods:{  
  update(){  
    this.$emit('update:hidden', true)  
  }  
}

关于 $emit

由于$emit只接收两个参数,在想要传入多个参数时,可以以数组的形式。

<button @click="$emit('update:hidden', [false, true])">隐藏子元素</button>

在接收时$event只需要根据下标,选择需要的 value 就行了。

<Child :hidden="hiddenChild" @update:hidden="hiddenChild=$event[1]"/>

需要注意的是,在需要进行 true/false 判断时,如果在$emit传入一个数组,而在$event不给出下标,那么由于传入的是一个数组对象,$event的值将一直等同于true。

关于 $emit 自定义事件取名

$emit('myEvent')

由于不同于组件和 prop,事件名不存在自动化的大小写转化。而触发事件名需要完全匹配这个事件所用名称,则监听这个事件的 kebab-case 版本是不会有任何效果的:

<!-- 没有效果 -->
<div @my-event="..."></div>

同时由于 HTML 对于大小写不敏感,而v-on事件监听器在 DOM 模板中会被转化为全小写。因此myEvent会被自动转化为myevent,此时监听myEvent是不会有效果的。

<!-- 没有效果 -->
<div @myEvent="..."></div>

因此 Vue 官方建议,始终以 kebab-case 格式或 update:myPropName 格式来为事件取名。而一般情况下更推荐后者,至于原因会在之后赘述。

sync 的作用

以上情况就是sync修饰符的使用场景,那么我们应该怎么使用sync呢?

Vue.component('Child', {
  template: `
    <div v-if="!hidden">
    这是子元素
    <button @click="$emit('update:hidden', true)">隐藏子元素</button>
    </div>
  `,
  props: ['hidden']
})

new Vue({
    el: '#app',
    template: `
      <div>
      <button @click="hiddenChild=!hiddenChild">显示/隐藏子元素</button>
      <hr>
      <Child :hidden.sync="hiddenChild"/>
      </div>
    `,
    data: {
      hiddenChild: true
    }
  }
)

以上代码中,我把<Child :hidden="hiddenChild" @update:hidden="hiddenChild=$event"/>修改为了<Child :hidden.sync="hiddenChild"/>,除此之外没有变动,但却能实现和之前同样的效果。

sync标识符会自动监听 Child 对于hidden的修改,并自动同步到父元素的data中去。

需要注意的是,sync是直接将 Child 的传出值赋值给data中的对应属性的,因此应该在 Child 中就完成对传出值的运算。

多 prop 时使用 sync

在以下场景,子元素有两个props: ['context', 'context2'],都来源于父元素的同一个data.parentContext。此时在子元素中对context进行传参,那么contextcontext2都会改变:

Vue.component('Child', {
  template: `
    <div>
    {{ context }}
    <button @click="$emit('update:context', '这是来自【子】元素的文本')">改变 context</button>
    <hr>
    {{ context2 }}
    </div>
  `,
  props: ['context', 'context2']
})

new Vue({
    el: '#app',
    template: `
      <div>
      <Child :context.sync="parentContext" :context2.sync="parentContext"/>
      <hr>
      <button @click="parentContext='这是来自【父】元素的文本'">改变子元素 context</button>
      </div>
    `,
    data: {
      parentContext: '这是来自【父】元素的文本',
    }
  }
)

在以下场景,子元素有两个props: ['context1', 'context2'],分别源于父元素的data.parentContext1data.parentContext2。此时在子元素中对context1进行传参,那么只有context1会改变:

Vue.component('Child', {
  template: `
    <div>
    {{ context1 }}
    <button @click="$emit('update:context1', '这是来自【子①】元素的文本')">改变 context1</button>
    <hr>
    {{ context2 }}
    </div>
  `,
  props: ['context1', 'context2']
})

new Vue({
    el: '#app',
    template: `
      <div>
      <Child :context1.sync="parentContext1" :context2.sync="parentContext2"/>
      <hr>
      <button @click="parentContext1='这是来自【父①】元素的文本'">改变子元素 context1</button>
      </div>
    `,
    data: {
      parentContext1: '这是来自【父①】元素的文本',
      parentContext2: '这是来自【父②】元素的文本',
    }
  }
)

在以下场景,子元素有两个props: ['context', 'context2'],分别源于父元素的data.parentContext1data.parentContext2。此时在子元素中对context1context2分别进行传参,那么context1context2分别改变:

Vue.component('Child', {
  template: `
    <div>
    {{ context1 }}
    <button @click="$emit('update:context1', '这是来自【子①】元素的文本')">改变 context1</button>
    <hr>
    {{ context2 }}
    <button @click="$emit('update:context2', '这是来自【子②】元素的文本')">改变 context2</button>
    </div>
  `,
  props: ['context1', 'context2']
})

new Vue({
    el: '#app',
    template: `
      <div>
      <Child :context1.sync="parentContext1" :context2.sync="parentContext2"/>
      <hr>
      <button @click="parentContext1='这是来自【父①】元素的文本'">改变子元素 context1</button>
      <button @click="parentContext2='这是来自【父②】元素的文本'">改变子元素 context2</button>
      </div>
    `,
    data: {
      parentContext1: '这是来自【父①】元素的文本',
      parentContext2: '这是来自【父②】元素的文本',
    }
  }
)

并且此时我们还可以发现,当把$emit('update:context1')改为$emit('updateContext1')后,context1将无法改变,这似乎是理所当然的。

但由此我们也可以得出结论,Vue 中 update:myPropName 的事件命名格式起到了为sync指定对象的作用。

当我们需要同时改变context1context2时,同时发出多个事件即可:

Vue.component('Child', {
  template: `
    <div>
    {{ context1 }}
    <button @click="$emit('update:context1', '这是来自【子①】元素的文本')">改变 context1</button>
    <hr>
    {{ context2 }}
    <button @click="$emit('update:context2', '这是来自【子②】元素的文本')">改变 context2</button>
    <hr>
    <button @click="$emit('update:context1', '这是来自【子①】元素的文本');$emit('update:context2', '这是来自【子②】元素的文本')">我要同时改变 context1/context2</button>
    </div>
  `,
  props: ['context1', 'context2']
})

new Vue({
    el: '#app',
    template: `
      <div>
      <Child :context1.sync="parentContext1" :context2.sync="parentContext2"/>
      <hr>
      <button @click="parentContext1='这是来自【父①】元素的文本'">改变子元素 context1</button>
      <button @click="parentContext2='这是来自【父②】元素的文本'">改变子元素 context2</button>
      </div>
    `,
    data: {
      parentContext1: '这是来自【父①】元素的文本',
      parentContext2: '这是来自【父②】元素的文本',
    }
  }
)