前言
前面幾篇文章一直都以源碼分析爲主,其實枯燥無味,對於新手玩家來說很不友好。這篇文章主要講講Vue
的響應式系統,形式與前邊的稍顯
不同吧,分析爲主,源碼爲輔,如果能達到深入淺出的效果那就更好了。
什麼是響應式系統
「響應式系統」一直以來都是我認爲Vue
裏最核心的幾個概念之一。想深入理解Vue
,首先要掌握「響應式系統」的原理。
從一個官方的例子開始
由於 Vue 不允許動態添加根級響應式屬性,所以你必須在初始化實例前聲明所有根級響應式屬性,哪怕只是一個空值:
var vm = new Vue({
data: {
// 聲明 message 爲一個空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之後設置 `message`
vm.message = 'Hello!'
如果你未在 data 選項中聲明 message,Vue
將警告你渲染函數正在試圖訪問不存在的屬性。
當然,僅僅從上面這個例子我們也只能知道,Vue
不允許動態添加根級響應式屬性。這意味我們需要將使用到的變量先在data
函數中聲明。
拋磚🧱引玉
新建一個空白工程,加入以下代碼
export default {
name: 'JustForTest',
data () {
return {}
},
created () {
this.b = 555
console.log(this.observeB)
this.b = 666
console.log(this.observeB)
},
computed: {
observeB () {
return this.b
}
}
}
運行上述代碼,結果如下:
555
555
在上面的代碼中我們做了些什麼?
- 沒有在
data
函數中聲明變量(意味着此時沒有根級響應式屬性) - 定義了一個
computed
屬性 ——observeB
,用來返回(監聽)變量b
- 使用了變量
b
同時賦值555
,打印this.observeB
- 使用了變量
b
同時賦值666
,打印this.observeB
打印結果爲什麼都是555
?
有段簡單的代碼可以解釋這個原因:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
...
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
createComputedGetter
函數返回一個閉包函數並掛載在computed
屬性的getter
上,一旦觸發computed
屬性的getter
,
那麼就會調用computedGetter
顯然,輸出 555
是因爲觸發了 this.observeB
的 getter
,從而觸發了 computedGetter
,最後執行 Watcher.evalute()
然而,決定 watcher.evalute()
函數執行與否與 watcher
和 watcher.dirty
的值是否爲空有關
深入瞭解響應式系統
Object.defineProperty
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
那麼這個函數應該怎麼使用呢?給個官方的源碼當做例子:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
def(value, '__ob__', this);
getter
和 setter
上面提到了 Object.defineProperty
函數,其實這個函數有個特別的參數 —— descriptor
(屬性描述符),簡單看下MDN
上的定義:
對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個具有值的屬性,該值可能是可寫的,也可能不是
可寫的。存取描述符是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是兩者。
其中需要特別提到的就是 getter
和 setter
,在 descriptor
(屬性描述符)中分別代表 get
方法和 set
方法
get
一個給屬性提供 getter 的方法,如果沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,
但是會傳入this對象(由於繼承關係,這裏的this並不一定是定義該屬性的對象)。
set
一個給屬性提供 setter 的方法,如果沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受唯一參數,
即該屬性新的參數值。
小結
- 對象在被訪問時會觸發
getter
- 對象在被賦值是會觸發
setter
- 利用
getter
我們可以知道哪些對象被使用了 - 利用
setter
我們可以知道哪些對象被賦值了
依賴收集
Vue
基於Object.defineProperty
函數,可以對變量進行依賴收集,從而在變量的值改變時觸發視圖的更新。簡單點來講就是:Vue
需要知道用到了哪些變量,不用的變量就不管,在它(變量)變化時,Vue
就通知對應綁定的視圖進行更新。
舉個例子:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
這段代碼做了哪些事情呢?主要有以下幾點:
- 對於
obj[key]
,定義它的get
和set
函數 - 在
obj[key]
被訪問時,觸發get
函數,調用dep.depend
函數收集依賴 - 在
obj[key]
被賦值時,調用set
函數,調用dep.notify
函數觸發視圖更新
如果你再深入探究下去,那麼還會發現 dep.notify
函數裏還調用了 update
函數,而它恰好就是 Watcher
類所屬
的方法,上面所提到的 computed
屬性的計算方法也恰好也屬於 Watcher
類
Observer
前面所提到的 Object.defineProperty
函數到底是在哪裏被調用的呢?答案就是 initData
函數和 Observer
類。
可以歸納出一個清晰的調用邏輯:
- 初始化
data
函數,此時調用initData
函數 - 在調用
initData
函數時,執行observe
函數,這個函數執行成功後會返回一個ob
對象 -
observe
函數返回的ob
對象依賴於Observer
函數 -
Observer
分別對對象和數組做了處理,對於某一個屬性,最後都要執行walk
函數 -
walk
函數遍歷傳入的對象的key
值,對於每個key
值對應的屬性,依次調用defineReactive$$1
函數 -
defineReactive$$1
函數中執行Object.defineProperty
函數 - ...
感興趣的可以看下主要的代碼,其實邏輯跟上面描述的一樣,只不過步驟比較繁瑣,耐心閱讀源碼的話還是能看懂。
initData
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
...
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
...
if (props && hasOwn(props, key)) {
...
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
observe
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
Observer
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
更加方便的定義響應式屬性
文檔中提到,Vue
建議在根級聲明變量。通過上面的分析我們也知道,在 data
函數中
聲明變量則使得變量變成「響應式」的,那麼是不是所有的情況下,變量都只能在 data
函數中
事先聲明呢?
$set
Vue
其實提供了一個 $set
的全局函數,通過 $set
就可以動態添加響應式屬性了。
export default {
data () {
return {}
},
created () {
this.$set(this, 'b', 666)
},
}
然而,執行上面這段代碼後控制檯卻報錯了
<font color=Red> [Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option. </font>
其實,對於已經創建的實例,Vue
不允許動態添加根級別的響應式屬性。$set
函數的執行邏輯:
- 判斷實例是否是數組,如果是則將屬性插入
- 判斷屬性是否已定義,是則賦值後返回
- 判斷實例是否是
Vue
的實例或者是已經存在ob
屬性(其實也是判斷了添加的屬性是否屬於根級別的屬性),是則結束函數並返回 - 執行
defineReactive$$1
,使得屬性成爲響應式屬性 - 執行
ob.dep.notify()
,通知視圖更新
相關代碼:
function set (target, key, val) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
c(ob.value, key, val);
ob.dep.notify();
return val
}
數組操作
爲了變量的響應式,Vue
重寫了數組的操作。其中,重寫的方法就有這些:
push
pop
shift
unshift
splice
sort
reverse
那麼這些方法是怎麼重寫的呢?
首先,定義一個 arrayMethods
繼承 Array
:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
然後,利用 object.defineProperty
,將 mutator
函數綁定在數組操作上:
def(arrayMethods, method, function mutator () { ... })
最後在調用數組方法的時候,會直接執行 mutator
函數。源碼中,對這三種方法做了特別
處理:
push
unshift
splice
因爲這三種方法都會增加原數組的長度。當然如果調用了這三種方法,會再調用一次 observeArray
方法(這裏的邏輯就跟前面提到的一樣了)
最後的最後,調用 notify
函數
核心代碼:
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
總結
「響應式原理」藉助了這三個類來實現,分別是:
Watcher
Observer
Dep
初始化階段,利用 getter
的特點,監聽到變量被訪問 Observer
和 Dep
實現對變量的「依賴收集」,
賦值階段利用 setter
的特點,監聽到變量賦值,利用 Dep
通知 Watcher
,從而進行視圖更新。