为了让 todo 组件内部的状态变化能在 Todo List 中呈现出来,我们在 Todo List 中添加计数,展示已经完成的 Todo 数量。因为这个数量受 todo 组件内部状态(数据)的影响,这就需要将 todo 内部数据变化反应到其父组件中,这才有 v-model 的用武之地。
这个数量我们在标题中以 n/m 的形式呈现,比如 2/4 表示一共 4 条 Todo,已经完成 2 条。这需要对 Todo List 的模板和代码部分进行修改,添加 countDone 和 count 两个计算属性:
<div> <h2>Todos ({{ countDone }}/{{ count }}):</h2> <!-- ... --> </div>
new Vue({ // ... computed: { count() { return this.todos.length; }, countDone() { return this.todos.filter(todo => todo.done).length; } } });
现在计数呈现出来了,但是现在改变任务状态并不会对这个计数产生影响。我们要让子组件的变动对父组件的数据产生影响。v-model 待会儿再说,先用最常见的方法,事件:
子组件 todo 在 toggle() 中触发 toggle 事件并将 isDone 作为事件参数
父组件为子组件的 toggle 事件定义事件处理函数
Vue.component("todo", { //... methods: { toggle(e) { this.isDone = !this.isDone; this.$emit("toggle", this.isDone); } } });
<!-- #app 中其它代码略 --> <todo :text="todo.text" :done="todo.done" @toggle="todo.done = $event"></todo>
这里为 @toggle 绑定的是一个表达式。因为这里的 todo 是一个临时变量,如果在 methods 中定义专门的事件处理函数很难将这个临时变量绑定过去(当然定义普通方法通过调用的形式是可以实现的)。
事件处理函数,一般直接对应于要处理的事情,比如定义 onToggle(e),绑定为 @toggle="onToggle"。这种情况下不能传入 todo 作为参数。普通方法,可以定义成 toggle(todo, e),在事件定义中以函数调用表达式的形式调用:@toggle="toggle(todo, $event)"。它和 todo.done = $event` 同属表达式。
注意二者的区别,前者是绑定的处理函数(引用),后者是绑定的表达式(调用)
现在通过事件方式已经达到了预期效果
改造成 v-model之前我们说了要用 v-model 实现的,现在来改造一下。注意实现 v-model 的几个要素
子组件通过 value 属性(Prop)接受输入
子组件通过触发 input 事件输出,带数组参数
父组件中用 v-model 绑定
Vue.component("todo", { // ... props: ["text", "value"], // <-- 注意 done 改成了 value data() { return { isDone: this.value // <-- 注意 this.done 改成了 this.value }; }, methods: { toggle(e) { this.isDone = !this.isDone; this.$emit("input", this.isDone); // <-- 注意事件名称变了 } } });
<!-- #app 中其它代码略 --> <todo :text="todo.text" v-model="todo.done"></todo>
.sync 实现其它数据绑定前面讲到了 Vue 2.2.0 引入 v-model 特性。由于某些原因,它的输入属性是 value,但输出事件叫 input。v-model、value、input 这三个名称从字面上看不到半点关系。虽然这看起来有点奇葩,但这不是重点,重点是一个控件只能双向绑定一个属性吗?
Vue 2.3.0 引入了 .sync 修饰语用于修饰 v-bind(即 :),使之成为双向绑定。这同样是语法糖,添加了 .sync 修饰的数据绑定会像 v-model 一样自动注册事件处理函数来对被绑定的数据进行赋值。这种方式同样要求子组件触发特定的事件。不过这个事件的名称好歹和绑定属性名有点关系,是在绑定属性名前添加 update: 前缀。
比如 <sub :some.sync="any" /> 将子组件的 some 属性与父组件的 any 数据绑定起来,子组件中需要通过 $emit("update:some", value) 来触发变更。
上面的示例中,使用 v-model 绑定始终感觉有点别扭,因为 v-model 的字面意义是双向绑定一个数值,而表示是否未完成的 done 其实是一个状态,而不是一个数值。所以我们再次对其进行修改,仍然使用 done 这个属性名称(而不是 value),通过 .sync 来实现双向绑定。
Vue.component("todo", { // ... props: ["text", "done"], // <-- 恢复成 done data() { return { isDone: this.done // <-- 恢复成 done }; }, methods: { toggle(e) { this.isDone = !this.isDone; this.$emit("update:done", this.isDone); // <-- 事件名称:update:done } } });
<!-- #app 中其它代码略 --> <!-- 注意 v-model 变成了 :done.sync,别忘了冒号哟 --> <todo :text="todo.text" :done.sync="todo.done"></todo>
揭密 Vue 双向绑定通过上面的讲述,我想大家应该已经明白了 Vue 的双向绑定其实就是普通单向绑定和事件组合来完成的,只不过通过 v-model 和 .sync 注册了默认的处理函数来更新数据。Vue 源码中有这么一段
// @file: src/compiler/parser/index.js if (modifiers.sync) { addHandler( el, `update:${camelize(name)}`, genAssignmentCode(value, `$event`) ) }