vue深入響應式原理
現在是時候深入一下了!Vue 最獨特的特性之一,是其非侵入性的響應式系統。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。這使得狀態管理非常簡單直接,不過理解其工作原理同樣重要,這樣你可以回避一些常見的問題。在這個章節,我們將進入一些 Vue 響應式系統的底層的細節。
如何追蹤變化
當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器。
這些 getter/setter 對用戶來說是不可見的,但是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。這里需要注意的問題是瀏覽器控制台在打印數據對象時 getter/setter 的格式化並不同,所以你可能需要安裝 vue-devtools 來獲取更加友好的檢查接口。
每個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的 setter 被調用時,會通知 watcher 重新計算,從而致使它關聯的組件得以更新。
檢測變化的注意事項
受現代 JavaScript 的限制 (而且 Object.observe 也已經被廢棄),Vue 不能檢測到對象屬性的添加或刪除。由於 Vue 會在初始化實例時對屬性執行 getter/setter 轉化過程,所以屬性必須在 data 對象上存在才能讓 Vue 轉換它,這樣才能讓它是響應的。例如:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是響應的
vm.b = 2
// `vm.b` 是非響應的
Vue 不允許在已經創建的實例上動態添加新的根級響應式屬性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法將響應屬性添加到嵌套的對象上:
Vue.set(vm.someObject, 'b', 2)
您還可以使用 vm.$set 實例方法,這也是全局 Vue.set 方法的別名:
this.$set(this.someObject,'b',2)
有時你想向一個已有對象添加多個屬性,例如使用 Object.assign() 或 _.extend() 方法來添加屬性。但是,這樣添加到對象上的新屬性不會觸發更新。在這種情況下可以創建一個新的對象,讓它包含原對象的屬性和新的屬性:
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
也有一些數組相關的問題,之前已經在列表渲染中講過。
聲明響應式屬性
由於 Vue 不允許動態添加根級響應式屬性,所以你必須在初始化實例前聲明根級響應式屬性,哪怕只是一個空值:
var vm = new Vue({
data: {
// 聲明 message 為一個空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之后設置 `message`
vm.message = 'Hello!'
如果你未在 data 選項中聲明 message,Vue 將警告你渲染函數正在試圖訪問的屬性不存在。
這樣的限制在背后是有其技術原因的,它消除了在依賴項跟蹤系統中的一類邊界情況,也使 Vue 實例在類型檢查系統的幫助下運行的更高效。而且在代碼可維護性方面也有一點重要的考慮:data 對象就像組件狀態的概要,提前聲明所有的響應式屬性,可以讓組件代碼在以后重新閱讀或其他開發人員閱讀時更易於被理解。
vue.js的雙向數據綁定就是通過Object.defineProperty方法實現的,俗稱屬性攔截器。
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象。“`
// 語法:
/*
* @param: obj:需要定義屬性的對象;
* prop:需要定義或修改的屬性;
* descriptor:將被定義或修改屬性的描述符
*/
Object.defineProperty(obj,prop,descriptor)
對象里目前存在的屬性描述符主要有兩種形式: 數據描述符和存取描述符.
- 數據描述符: 擁有可寫或不可寫值的屬性*
可選鍵值:
configurable: 當且僅當configurable為true時,改屬性描述符才能夠被改變,也能被刪除
enumerable: 當其值為true時,該屬性才能夠出現在對象的枚舉屬性中,默認為false
writable: 當且僅當該屬性的值為true時,該屬性才能被賦值運算符改變, 默認為false。
value: 該屬性對應的值,可以是任意有效的javascript的值(數值,對象,函數等),默認為undefined
- 存取描述符: 由一對getter-setter函數功能來描述的屬性*
可選鍵值:
configurable: 當且僅當configurable為true時,改屬性描述符才能夠被改變,也能被刪除
enumerable: 當其值為true時,該屬性才能夠出現在對象的枚舉屬性中,默認為false
get: 給屬性提供getter的方法,如果沒有 getter 則為undefined。當我們讀取某個屬性的時候,其實是在對象內部調用了該 方法,此方法必須要有return語句。該方法返回值被用作屬性值。默認為 undefined
set:設置屬性值的方法, 如果沒有 setter 則為 undefined。該方法將接受唯一參數,並將該參數的新值分配給該屬性。默認為 undefined。也就是說,當我們設置某個屬性的時候,實際上是在對象的內部調用了該方法
note:兩者不能同時定義, 否則報錯==
實例:
var a = {};
Object.defineProperty(a, 'b', {
set: function(newValue) {
console.log('賦值操作, 賦值' + newValue);
},
get: function() {
console.log('取值操作');
return 2;
}
});
a.b = 1; // 賦值操作,賦值1
a.b; // 取值操作2
雖然我將a.b的值設置成了1,但是因為我在get方法中始終返回了2,所以a.b的值一直是2。
那么,這就好玩兒了:我們可以在頁面監聽某個變量,當變量發生變化的時候,我們就更新對應的視圖。由數據來驅動視圖的更新,是不是很熟悉?是的,vue .js的核心思想就是這個。我們寫個小例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<div id="test">這是一個測試</div>
<script>
var view = document.getElementById("test");
var data = {};
var i=0;
Object.defineProperty(data, "b", {
set: function(newValue) {
//當data.b的值改變的時候更新#test的視圖
view.textContent=newValue;
},
get: function() {
}
});
setInterval(function(){
i++;
data["b"] = "data.b的值更新了,我要更新視圖"+i;
},1000);
</script>
</body>
</html>
視圖的變化過程:
剛開始:
data.b的值更新了,我要更新視圖1;
1秒后:
data.b的值更新了,我要更新視圖2
2秒后:
data.b的值更新了,我要更新視圖3
Object對象有一個freeze的方法,用於實現對象屬性和方法的不可更改
// 使用方法:
const arr = [1,2,3,4];
Object.freeze(arr); // 變量arr不可更改
arr.push(5); // 報錯:不能添加屬性
Object.definePropperty也可以實現規定變量的不可更改
const obj = { key: 'chris', vlaue: 'person' }; Object.defineProperty(obj, 'key', { configurable: false, // 不可刪除 writable: false, // 不可寫 });
視圖和數據變化綁定原理
對於一個html頁面
<div>
<p>你好,<span id='nickName'></span></p>
<div id="introduce"></div>
</div>
設置一個數據的屬性的getter和setter
//視圖控制器
var userInfo = {};
Object.defineProperty(userInfo, "nickName", {
get: function(){
return document.getElementById('nickName').innerHTML;
},
set: function(nick){
document.getElementById('nickName').innerHTML = nick;
}
});
Object.defineProperty(userInfo, "introduce", {
get: function(){
return document.getElementById('introduce').innerHTML;
},
set: function(introduce){
document.getElementById('introduce').innerHTML = introduce;
}
})
然后就能愉快地綁定數據交互了。
userInfo.nickName = "xxx";
userInfo.introduce = "我是xxx,我來自雲南,..."
vue.js的數據變動原理
但是,這個例子只是數據和dom節點的綁定,而vue.js更為復雜一點,它在網頁dom和accessor之間會有兩層,一層是Wacher,一層是Directive,比如以下代碼。
var a = { b: 1 }
var vm = new Vue({
data: data
})
把一個普通對象(a={b:1})傳給 Vue 實例作為它的 data 選項,Vue.js 將遍歷它的屬性,用Object.defineProperty 將它們轉為 getter/setter,如圖綠色的部分所示。
每次用戶更改data里的數據的時候,比如a.b =1,setter就會重新通知Watcher進行變動,Watcher再通知Directive對dom節點進行更改。