sync修饰符做双向绑定
推荐使用update:myPropName模式取代双向绑定
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。所以在封装组件的时候,就需要使用watch或者v-model去实现组件间的数据传输。具体请参考我的其他两篇文章:《使用watch实现组件props双向绑定》和 《v-model实现双向绑定自定义组件》
这也是为什么官方推荐以update:myPropName
的模式触发事件取而代之。
举个例子,在一个包含title
prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle)
然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
为了方便起见,我们为这种模式提供一个缩写,即.sync
修饰符:
<text-document v-bind:title.sync="doc.title"></text-document>
注意带有.sync
修饰符的v-bind
不能和表达式一起使用 (例如v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似v-model
。
使用sync同步修改单一属性的例子
示例是说明问题最好的手段。我们来看这样一个例子:父子组件都可以通过点击修改父组件的状态,而父组件状态修改是自动发生的。我们不使用传统的watch方式去实现。
<style type="text/css">
#app{
width: 600px;margin: 20px auto;
}
.switchbtn{
width: 100%;height: 200px;color: #fff;text-align: center;line-height: 200px;font-size: 5em;
margin-bottom: 20px;cursor: pointer;user-select: none;
}
.switchbtn.cls-open{
background-color: green;
}
.switchbtn.cls-close{
background-color: red;
}
button{
display: block;
width: 100%;
height: 50px;
}
</style>
<div id="app">
<!-- 监听@update:open事件 触发父组件的toggle函数 改变父组件中值的状态 -->
<switchbtn :open="open" @update:open="toggle"></switchbtn>
<!-- 以下3种操作效果等价 -->
<switchbtn v-bind:open="open" v-on:update:open="open = $event"></switchbtn>
<switchbtn :open="open" @update:open="open = $event"></switchbtn>
<switchbtn :open.sync="open"></switchbtn>
<button type="button" @click="toggle">{{open ? '开' : '关'}}</button>
</div>
Vue.component("switchbtn", {
template:`
<div @click="toggleOpen"
:class="cls"
>
{{myOpen ? '开' : '关'}}
</div>
`,
props:{
open: {
type: Boolean,
default() {
return false;
}
}
},
computed: {
myOpen: {
get() {
return this.open;
},
set(val) {
// update:open update:后面必须跟着props中传递值的名称!不可以随便起
// 计算属性值改变的时候 向外部发送事件
this.$emit('update:open', val);
}
},
cls() {
let cls = 'cls-open';
if (!this.myOpen) {
cls = 'cls-close';
}
return `switchbtn ${cls}`;
}
},
methods:{
// 子组件中修改使用计算属性接收的值 从而触发set
toggleOpen(){
this.myOpen = !this.myOpen;
}
}
});
var vm = new Vue({
el: '#app',
data () {
return {
open: true
}
},
methods: {
toggle() {
// 只有@update:open="toggle"触发的时候才会log
console.log('@update:open="toggle"');
this.open = !this.open;
}
}
});
这里需要注意的是:update:传递到子组件属性名。名字不可以随便起。否则:myPropName.sync会找不到子组件向父组件发送的事件!
<switchbtn :open="open" @update:open="open = $event"></switchbtn>
<switchbtn :open.sync="open"></switchbtn>
// :open.sync="open" 实际上执行的就是如下两步的操作!
// :open="open" @update:open="open = $event"
将一整个对象作为props
当用一个对象同时设置多个 prop 的时候,也可以将这个.sync
修饰符和v-bind
配合使用:
<text-document v-bind.sync="doc"></text-document>
这样会把doc
对象中的每一个 property (如title
) 都作为一个独立的 prop 传进去,然后各自添加用于更新的v-on
监听器。
将v-bind.sync
用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
还是将上面的例子进行修改。
<div id="app">
<!-- 监听@update:open事件 触发父组件的toggle函数 改变父组件中值的状态 -->
<switchbtn :status="status" @update:status="toggle"></switchbtn>
<!-- 以下2种操作等价 -->
<switchbtn :status="status" @update:status="status = $event"></switchbtn>
<switchbtn :status.sync="status"></switchbtn>
<button type="button" @click="toggle">{{status.title}}</button>
</div>
let msg_old ='wubin.work';
let msg_new = 'is a website';
Vue.component("switchbtn", {
template:`
<div @click="toggleOpen"
:class="cls"
>
{{myStatus.title}}
</div>
`,
props:{
status: {
type: Object,
default() {
return {};
}
}
},
computed: {
myStatus: {
get() {
return this.status;
},
set(val) {
// update:status update:后面必须跟着props中传递值的名称!不可以随便起
// 计算属性值改变的时候 向外部发送事件
this.$emit('update:status', val);
}
},
cls() {
let cls = 'cls-open';
if (!this.myStatus.open) {
cls = 'cls-close';
}
return `switchbtn ${cls}`;
}
},
methods:{
// 子组件中修改使用计算属性接收的值 从而触发set
toggleOpen(){
if (this.myStatus.open) {
this.myStatus.title = msg_new;
this.myStatus.open = false;
} else {
this.myStatus.title = msg_old;
this.myStatus.open = true;
}
}
}
});
var vm = new Vue({
el: '#app',
data () {
return {
status: {
title: 'wubin.work',
open: true
}
}
},
methods: {
toggle() {
if (this.open) {
this.open = false;
this.status.title = msg_new;
} else {
this.open = true;
this.status.title = msg_old;
}
}
}
});