数据初始化
明确定义的数据模型更加适合 Vue 的数据观察模式。建议在定义组件时,在 data
选项中初始化所有需要进行动态观察的属性。例如,给定下面的模版:
<div id="demo">
<p v-class="green: validation.valid">{{message}}</p>
<input v-model="message">
</div>
建议像这样初始化你的数据,而不是什么都不定义:
new Vue({
el: '#demo',
data: {
message: '',
validation: {
valid: false
}
}
})
为什么要这样做呢?因为 Vue 是通过递归遍历初始数据中的所有属性,并用 Object.defineProperty
把它们转化为 getter
和 setter
来实现数据观察的。如果一个属性在实例创建时不存在于初始数据中,那么 Vue 就没有办法观察这个属性了。
当然,你也不需要对每一个可能存在的嵌套属性都进行初始定义。在初始化的时候可以将一个属性置为空对象,然后在后面的操作中设置为一个新的拥有嵌套结构的对象。只要这个新对象包含了应有的属性,Vue 依然能对这个新对象进行递归遍历,从而观察其内部属性。
添加和删除属性
正如前面所说的, Vue 会使用 Object.defineProperty
通过转化属性值来观察数据。不过,在 ECMAScript 5 中,当一个新的属性被添加到对象或者从对象中删除的时候,并没有办法可以检测到这两种情况。为了解决这个问题,Vue 会为被观察的对象添加三个扩展方法:
obj.$add(key, value)
obj.$set(key, value)
obj.$delete(key)
通过调用这些方法给观察对象中添加或者删除属性,就能够触发所对应的 DOM 更新。$add
和 $set
的区别是,假如当前对象已经含有所使用的 key
, $add
会直接返回。所以当使用 $obj.$add(key)
的时候不会将已经存在的值覆盖为 undefined
。
另外需要注意的一点是,当你通过数组索引赋值来改动数组时 (比如 arr[1] = value
),Vue 是无法侦测到这类操作的。类似地,你可以使用扩展方法来确保 Vue.js 收到了通知。被观察的数组有两个扩展方法:
arr.$set(index, value)
arr.$remove(index | value)
Vue 组件实例也有相应的实例方法:
vm.$get(path)
vm.$set(path, value)
vm.$add(key, value)
vm.$delete(key, value)
注意 vm.$get
和 vm.set
都接受路径。
尽管存在这些方法,但我强烈建议你只在必要的时候才动态添加可观察属性。为了理解你的组件状态,将
data
选项看做一个schema
很有帮助。假如你清晰地列出一个组件中所有可能存在的属性,那么当你隔了几个月再来维护这个组件的时候,就可以更容易地理解这个组件可能包含怎样的状态。
理解异步更新
默认情况下, Vue 的 DOM 更新是异步执行的。理解这一点非常重要。当侦测到数据变化时, Vue 会打开一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。假如一个 watcher 在一个事件循环中被触发了多次,它只会被推送到队列中一次。然后,在进入下一次的事件循环时, Vue 会清空队列并进行必要的 DOM 更新。在内部,Vue 会使用 MutationObserver
来实现队列的异步处理,如果不支持则会回退到 setTimeout(fn, 0)
。
举例来说,当你设置 vm.someData = 'new value'
,DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新。如果你想要根据更新的 DOM 状态去做某些事情,就必须要留意这个细节。尽管 Vue.js 鼓励开发者用 “数据驱动” 的方式想问题,避免直接操作 DOM ,但有时候你可能就是想要使用某个熟悉的 jQuery 插件。这种情况下怎么办呢?你可以在数据改变后,立刻调用 Vue.nextTick(callback)
,并把你要做的事情放到回调函数里面。当 Vue.nextTick
的回调函数执行时,DOM 将会已经是更新后的状态了。
示例:
<div id="example">{{msg}}</div>
var vm = new Vue({
el: '#example',
data: {
msg: '123'
}
})
vm.msg = 'new message' // change data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
除此之外,也有一个实例方法 vm.$nextTick()
。这个方法和全局的 Vue.nextTick
功能一样,但更方便在组件内部使用,因为它不需要全局的 Vue
变量,另外它的回调函数的 this
上下文会自动绑定到调用它的 Vue
实例:
Vue.component('example', {
template: '<span>{{msg}}</span>',
data: function () {
return {
msg: 'not updated'
}
},
methods: {
updateMessage: function () {
this.msg = 'updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function () {
console.log(this.$el.textContent) // => 'updated'
})
}
}
})
组件作用域
每一个 Vue.js 组件都是一个拥有自己的独立作用域的 Vue 实例。在使用组件的时候,理解组件作用域机制非常重要。其规则概括来说就是:
在父模板中出现的,将在父模板作用域内编译;在子模板中出现的,将在子模板的作用域内编译。
一个常见的错误是,在父模版中尝试将一个指令绑定到子作用域里的属性或者方法上:
<div id="demo">
<!-- 不起作用,因为作用域不对! -->
<child-component v-on="click: childMethod"></child-component>
</div>
如果需要在子组件的根节点上绑定指令,应当将指令写在子组件的模板内:
Vue.component('child-component', {
// 这次作用域对了
template: '<div v-on="click: childMethod">Child</div>',
methods: {
childMethod: function () {
console.log('child method invoked!')
}
}
})
注意,当组件和 v-repeat
一同使用时,$index
作为子作用域属性也会受到此规则的影响。
另外,父模板里组件节点内部的 HTML 内容被看做是 “transclusion content”(插入内容)。除非子模版包含至少一个 <content>
出口,不然这些插入内容不会被渲染。需要留意的是,插入内容也是在父作用域中编译的:
<div>
<child-component>
<!-- 在父作用域里编译 -->
<p>{{msg}}</p>
</child-component>
</div>
你可以使用 inline-template
属性去明确内容在子模版的作用域中被编译:
<div>
<child-component inline-template>
<!-- 在子作用域里编译 -->
<p>{{msg}}</p>
</child-component>
</div>
更多关于内容插入的细节,请看组件一章的 内容插入 小节。
在多个实例之间通讯
一种常见的在 Vue 中进行父子通讯的方法是,通过 props
传递一个父方法作为一个回调到子组件中。这样使用时的回调传递可以被定义在父模版中,从而保持了组件之间 Javascript 实现细节上的解耦:
<div id="demo">
<p>Child says: {{msg}}</p>
<child-component send-message="{{onChildMsg}}"></child-component>
</div>
new Vue({
el: '#demo',
data: {
msg: ''
},
methods: {
onChildMsg: function(msg) {
this.msg = msg
return 'Got it!'
}
},
components: {
'child-component': {
props: [
// you can use prop assertions to ensure the
// callback prop is indeed a function.
{
name: 'send-message',
type: Function,
required: true
}
],
// props with hyphens are auto-camelized
template:
'<button v-on="click:onClick">Say Yeah!</button>' +
'<p>Parent responds: {{response}}</p>',
// component `data` option must be a function
data: function () {
return {
response: ''
}
},
methods: {
onClick: function () {
this.response = this.sendMessage('Yeah!')
}
}
}
}
})
Result:
Child says:
当你需要跨越多层嵌套的组件进行通讯时,你可以使用事件系统。另外,在构建大型应用时,用 Vue 搭配类似 Flux 的架构也是完全可行的。