前面幾篇文章一直都以源碼分析爲主,其實枯燥無味,對於新手玩家來講很不友好。這篇文章主要講講
Vue
的響應式系統,形式與前邊的稍顯 不一樣吧,分析爲主,源碼爲輔,若是能達到深刻淺出的效果那就更好了。javascript
「響應式系統」一直以來都是我認爲
Vue
裏最核心的幾個概念之一。想深刻理解Vue
,首先要掌握「響應式系統」的原理。html
因爲 Vue 不容許動態添加根級響應式屬性,因此你必須在初始化實例前聲明全部根級響應式屬性,哪怕只是一個空值:前端
var vm = new Vue({
data: {
// 聲明 message 爲一個空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 以後設置 `message`
vm.message = 'Hello!'
複製代碼
若是你未在 data 選項中聲明 message,
Vue
將警告你渲染函數正在試圖訪問不存在的屬性。vue
固然,僅僅從上面這個例子咱們也只能知道,Vue
不容許動態添加根級響應式屬性。這意味咱們須要將使用到的變量先在data
函數中聲明。java
新建一個空白工程,加入如下代碼react
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
的值是否爲空有關app
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)
},
}
複製代碼
然而,執行上面這段代碼後控制檯卻報錯了
[Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option.
其實,對於已經建立的實例,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
,從而進行視圖更新。
掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。