[toc]
## 數據初始化
明確定義的數據模型更加適合 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 的架構也是完全可行的。
##片段實例
自 0.12.2 起,replace 參數默認為 true。這意味著:
>組件的模板長什么樣,渲染出來的 DOM 就是什么樣。
父模板中調用組件的元素將會被組件本身的模板取代。因此,如果組件的模板包含多個頂級元素:
~~~
Vue.component('example', {
template:
'<div>A</div>' +
'<div>B</div>'
})
~~~
或者模板只包含文本:
~~~
Vue.component('example', {
template: 'Hello world'
})
~~~
在這兩個情況下,實例將變成一個片段實例 (fragment instance),也即沒有根元素的實例。它的 $el 指向一個錨節點(普通模式下是空的文本節點,debug 模式下是注釋節點)。更重要的是,父模板組件元素上的指令、過渡效果和屬性綁定(props 除外)將無效,因為生成的實例并沒有根元素供它們綁定:
~~~
<!-- 指令不生效,因為沒有根元素用來綁定 -->
<example v-show="ok" v-transition="fade"></example>
<!-- props 還是能夠正常生效 -->
<example prop="{{someData}}"></example>
~~~
雖然片段實例也有其使用場景,但是大部分情況下,給組件模板一個根元素是推薦的做法。這樣父模板組件元素上的指令和屬性能正常運轉,并且性能也會更好一點。
## 修改默認選項
通過修改全局的 Vue.options 對象,可以修改實例選項的默認值。例如,你可以設置 Vue.options.replace = false,使所有 Vue 實例都按照 replace: false 的規則被編譯。請謹慎使用這個功能 - 最好是只在一個項目剛開始的時候使用它,因為它會影響所有 Vue 實例的行為。