明確定義的數(shù)據(jù)模型更加適合 Vue 的數(shù)據(jù)觀察模式。建議在定義組件時,在 data 選項中初始化所有需要進行動態(tài)觀察的屬性。例如,給定下面的模版:
<div id="demo">
<p v-class="green: validation.valid">{{message}}</p>
<input v-model="message">
</div>
建議像這樣初始化你的數(shù)據(jù),而不是什么都不定義:
new Vue({
el: '#demo',
data: {
message: '',
validation: {
valid: false
}
}
})
為什么要這樣做呢?因為 Vue 是通過遞歸遍歷初始數(shù)據(jù)中的所有屬性,并用 Object.defineProperty 把它們轉化為 getter 和 setter 來實現(xiàn)數(shù)據(jù)觀察的。如果一個屬性在實例創(chuàng)建時不存在于初始數(shù)據(jù)中,那么 Vue 就沒有辦法觀察這個屬性了。
當然,你也不需要對每一個可能存在的嵌套屬性都進行初始定義。在初始化的時候可以將一個屬性置為空對象,然后在后面的操作中設置為一個新的擁有嵌套結構的對象。只要這個新對象包含了應有的屬性,Vue 依然能對這個新對象進行遞歸遍歷,從而觀察其內部屬性。
正如前面所說的, Vue 會使用 Object.defineProperty 通過轉化屬性值來觀察數(shù)據(jù)。不過,在 ECMAScript 5 中,當一個新的屬性被添加到對象或者從對象中刪除的時候,并沒有辦法可以檢測到這兩種情況。為了解決這個問題,Vue 會為被觀察的對象添加三個擴展方法:
obj.$add(key, value)obj.$set(key, value)obj.$delete(key)通過調用這些方法給觀察對象中添加或者刪除屬性,就能夠觸發(fā)所對應的 DOM 更新。$add 和 $set 的區(qū)別是,假如當前對象已經含有所使用的 key, $add 會直接返回。所以當使用 $obj.$add(key) 的時候不會將已經存在的值覆蓋為 undefined。
另外需要注意的一點是,當你通過數(shù)組索引賦值來改動數(shù)組時 (比如 arr[1] = value),Vue 是無法偵測到這類操作的。類似地,你可以使用擴展方法來確保 Vue.js 收到了通知。被觀察的數(shù)組有兩個擴展方法:
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 都接受路徑。
盡管存在這些方法,但我強烈建議你只在必要的時候才動態(tài)添加可觀察屬性。為了理解你的組件狀態(tài),將
data選項看做一個schema很有幫助。假如你清晰地列出一個組件中所有可能存在的屬性,那么當你隔了幾個月再來維護這個組件的時候,就可以更容易地理解這個組件可能包含怎樣的狀態(tài)。
默認情況下, Vue 的 DOM 更新是異步執(zhí)行的。理解這一點非常重要。當偵測到數(shù)據(jù)變化時, Vue 會打開一個隊列,然后把在同一個事件循環(huán) (event loop) 當中觀察到數(shù)據(jù)變化的 watcher 推送進這個隊列。假如一個 watcher 在一個事件循環(huán)中被觸發(fā)了多次,它只會被推送到隊列中一次。然后,在進入下一次的事件循環(huán)時, Vue 會清空隊列并進行必要的 DOM 更新。在內部,Vue 會使用 MutationObserver 來實現(xiàn)隊列的異步處理,如果不支持則會回退到 setTimeout(fn, 0)。
舉例來說,當你設置 vm.someData = 'new value',DOM 并不會馬上更新,而是在異步隊列被清除,也就是下一個事件循環(huán)開始時執(zhí)行更新。如果你想要根據(jù)更新的 DOM 狀態(tài)去做某些事情,就必須要留意這個細節(jié)。盡管 Vue.js 鼓勵開發(fā)者用 “數(shù)據(jù)驅動” 的方式想問題,避免直接操作 DOM ,但有時候你可能就是想要使用某個熟悉的 jQuery 插件。這種情況下怎么辦呢?你可以在數(shù)據(jù)改變后,立刻調用 Vue.nextTick(callback),并把你要做的事情放到回調函數(shù)里面。當 Vue.nextTick 的回調函數(shù)執(zhí)行時,DOM 將會已經是更新后的狀態(tài)了。
示例:
<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 變量,另外它的回調函數(shù)的 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 實例。在使用組件的時候,理解組件作用域機制非常重要。其規(guī)則概括來說就是:
在父模板中出現(xiàn)的,將在父模板作用域內編譯;在子模板中出現(xiàn)的,將在子模板的作用域內編譯。
一個常見的錯誤是,在父模版中嘗試將一個指令綁定到子作用域里的屬性或者方法上:
<div id="demo">
<!-- 不起作用,因為作用域不對! -->
<child-component v-on="click: childMethod"></child-component>
</div>
如果需要在子組件的根節(jié)點上綁定指令,應當將指令寫在子組件的模板內:
Vue.component('child-component', {
// 這次作用域對了
template: '<div v-on="click: childMethod">Child</div>',
methods: {
childMethod: function () {
console.log('child method invoked!')
}
}
})
注意,當組件和 v-repeat 一同使用時,$index 作為子作用域屬性也會受到此規(guī)則的影響。
另外,父模板里組件節(jié)點內部的 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>
更多關于內容插入的細節(jié),請看組件一章的 內容插入 小節(jié)。
一種常見的在 Vue 中進行父子通訊的方法是,通過 props 傳遞一個父方法作為一個回調到子組件中。這樣使用時的回調傳遞可以被定義在父模版中,從而保持了組件之間 Javascript 實現(xiàn)細節(jié)上的解耦:
<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:
當你需要跨越多層嵌套的組件進行通訊時,你可以使用事件系統(tǒng)。另外,在構建大型應用時,用 Vue 搭配類似 Flux 的架構也是完全可行的。
自 0.12.2 起,replace 參數(shù)默認為 true。這意味著:
組件的模板長什么樣,渲染出來的 DOM 就是什么樣。
父模板中調用組件的元素將會被組件本身的模板取代。因此,如果組件的模板包含多個頂級元素:
Vue.component('example', {
template:
'<div>A</div>' +
'<div>B</div>'
})
或者模板只包含文本:
Vue.component('example', {
template: 'Hello world'
})
在這兩個情況下,實例將變成一個片段實例 (fragment instance),也即沒有根元素的實例。它的 $el 指向一個錨節(jié)點(普通模式下是空的文本節(jié)點,debug 模式下是注釋節(jié)點)。更重要的是,父模板組件元素上的指令、過渡效果和屬性綁定(props 除外)將無效,因為生成的實例并沒有根元素供它們綁定:
<!-- 指令不生效,因為沒有根元素用來綁定 -->
<example v-show="ok" v-transition="fade"></example>
`
`<!-- props 還是能夠正常生效 -->
<example prop="{{someData}}"></example>
雖然片段實例也有其使用場景,但是大部分情況下,給組件模板一個根元素是推薦的做法。這樣父模板組件元素上的指令和屬性能正常運轉,并且性能也會更好一點。
通過修改全局的 Vue.options 對象,可以修改實例選項的默認值。例如,你可以設置 Vue.options.replace = false,使所有 Vue 實例都按照 replace: false 的規(guī)則被編譯。請謹慎使用這個功能 - 最好是只在一個項目剛開始的時候使用它,因為它會影響所有 Vue 實例的行為。
下一節(jié):常見問題。