上一節,咱們深刻分析了以
data,computed
爲數據建立響應式系統的過程,並對其中依賴收集和派發更新的過程進行了詳細的分析。然而在使用和分析過程當中依然存在或多或少的問題,這一節咱們將針對這些問題展開分析,最後咱們也會分析一下watch
的響應式過程。這篇文章將做爲響應式系統分析的完結篇。react
在以前介紹數據代理章節,咱們已經詳細介紹過Vue
數據代理的技術是利用了Object.defineProperty
,Object.defineProperty
讓咱們能夠方便的利用存取描述符中的getter/setter
來進行數據的監聽,在get,set
鉤子中分別作不一樣的操做,達到數據攔截的目的。然而Object.defineProperty
的get,set
方法只能檢測到對象屬性的變化,對於數組的變化(例如插入刪除數組元素等操做),Object.defineProperty
卻沒法達到目的,這也是利用Object.defineProperty
進行數據監控的缺陷,雖然es6
中的proxy
能夠完美解決這一問題,但畢竟有兼容性問題,因此咱們還須要研究Vue
在Object.defineProperty
的基礎上如何對數組進行監聽檢測。es6
既然數組已經不能再經過數據的getter,setter
方法去監聽變化了,Vue
的作法是對數組方法進行重寫,在保留原數組功能的前提下,對數組進行額外的操做處理。也就是從新定義了數組方法。web
var arrayProto = Array.prototype;
// 新建一個繼承於Array的對象
var arrayMethods = Object.create(arrayProto);
// 數組擁有的方法
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
複製代碼
arrayMethods
是基於原始Array
類爲原型繼承的一個對象類,因爲原型鏈的繼承,arrayMethod
擁有數組的全部方法,接下來對這個新的數組類的方法進行改寫。算法
methodsToPatch.forEach(function (method) {
// 緩衝原始數組的方法
var original = arrayProto[method];
// 利用Object.defineProperty對方法的執行進行改寫
def(arrayMethods, method, function mutator () {});
});
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
複製代碼
這裏對數組方法設置了代理,當執行arrayMethods
的數組方法時,會代理執行mutator
函數,這個函數的具體實現,咱們放到數組的派發更新中介紹。express
僅僅建立一個新的數組方法合集是不夠的,咱們在訪問數組時,如何不調用原生的數組方法,而是將過程指向這個新的類,這是下一步的重點。api
回到數據初始化過程,也就是執行initData
階段,上一篇內容花了大篇幅介紹過數據初始化會爲data
數據建立一個Observer
類,當時咱們只講述了Observer
類會爲每一個非數組的屬性進行數據攔截,從新定義getter,setter
方法,除此以外對於數組類型的數據,咱們有意跳過度析了。這裏,咱們重點看看對於數組攔截的處理。數組
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 將__ob__屬性設置成不可枚舉屬性。外部沒法經過遍歷獲取。
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);
}
}
複製代碼
數組處理的分支分爲兩個,hasProto
的判斷條件,hasProto
用來判斷當前環境下是否支持__proto__
屬性。而數組的處理會根據是否支持這一屬性來決定執行protoAugment, copyAugment
過程,promise
// __proto__屬性的判斷
var hasProto = '__proto__' in {};
複製代碼
當支持__proto__
時,執行protoAugment
會將當前數組的原型指向新的數組類arrayMethods
,若是不支持__proto__
,則經過代理設置,在訪問數組方法時代理訪問新數組類中的數組方法。瀏覽器
//直接經過原型指向的方式
function protoAugment (target, src) {
target.__proto__ = src;
}
// 經過數據代理的方式
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
複製代碼
有了這兩步的處理,接下來咱們在實例內部調用push, unshift
等數組的方法時,會執行arrayMethods
類的方法。這也是數組進行依賴收集和派發更新的前提。app
因爲數據初始化階段會利用Object.definePrototype
進行數據訪問的改寫,數組的訪問一樣會被getter
所攔截。因爲是數組,攔截過程會作特殊處理,後面咱們再看看dependArray
的原理。
function defineReactive###1() {
···
var childOb = !shallow && observe(val);
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() {}
}
複製代碼
childOb
是標誌屬性值是否爲基礎類型的標誌,observe
若是遇到基本類型數據,則直接返回,不作任何處理,若是遇到對象或者數組則會遞歸實例化Observer
,會爲每一個子屬性設置響應式數據,最終返回Observer
實例。而實例化Observer
又回到以前的老流程: 添加__ob__
屬性,若是遇到數組則進行原型重指向,遇到對象則定義getter,setter
,這一過程前面分析過,就再也不闡述。
在訪問到數組時,因爲childOb
的存在,會執行childOb.dep.depend();
進行依賴收集,該Observer
實例的dep
屬性會收集當前的watcher
做爲依賴保存,dependArray
保證了若是數組元素是數組或者對象,須要遞歸去爲內部的元素收集相關的依賴。
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
複製代碼
咱們能夠經過截圖看最終依賴收集的結果。
收集前
收集後
當調用數組的方法去添加或者刪除數據時,數據的setter
方法是沒法攔截的,因此咱們惟一能夠攔截的過程就是調用數組方法的時候,前面介紹過,數組方法的調用會代理到新類arrayMethods
的方法中,而arrayMethods
的數組方法是進行重寫過的。具體咱們看他的定義。
methodsToPatch.forEach(function (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);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
複製代碼
mutator
是重寫的數組方法,首先會調用原始的數組方法進行運算,這保證了與原始數組類型的方法一致性,args
保存了數組方法調用傳遞的參數。以後取出數組的__ob__
也就是以前保存的Observer
實例,調用ob.dep.notify();
進行依賴的派發更新,前面知道了。Observer
實例的dep
是Dep
的實例,他收集了須要監聽的watcher
依賴,而notify
會對依賴進行從新計算並更新。具體看Dep.prototype.notify = function notify () {}
函數的分析,這裏也不重複贅述。
回到代碼中,inserted
變量用來標誌數組是不是增長了元素,若是增長的元素不是原始類型,而是數組對象類型,則須要觸發observeArray
方法,對每一個元素進行依賴收集。
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
複製代碼
總的來講。數組的改變不會觸發setter
進行依賴更新,因此Vue
建立了一個新的數組類,重寫了數組的方法,將數組方法指向了新的數組類。同時在訪問到數組時依舊觸發getter
進行依賴收集,在更改數組時,觸發數組新方法運算,並進行依賴的派發。
如今咱們回過頭看看Vue的官方文檔對於數組檢測時的注意事項:
Vue
不能檢測如下數組的變更:
- 當你利用索引直接設置一個數組項時,例如:
vm.items[indexOfItem] = newValue
- 當你修改數組的長度時,例如:
vm.items.length = newLength
顯然有了上述的分析咱們很容易理解數組檢測帶來的弊端,即便Vue
重寫了數組的方法,以便在設置數組時進行攔截處理,可是不論是經過索引仍是直接修改長度,都是沒法觸發依賴更新的。
咱們在實際開發中常常遇到一種場景,對象test: { a: 1 }
要添加一個屬性b
,這時若是咱們使用test.b = 2
的方式去添加,這個過程Vue
是沒法檢測到的,理由也很簡單。咱們在對對象進行依賴收集的時候,會爲對象的每一個屬性都進行收集依賴,而直接經過test.b
添加的新屬性並無依賴收集的過程,所以當以後數據b
發生改變時也不會進行依賴的更新。
瞭解決這一問題,Vue
提供了Vue.set(object, propertyName, value)
的靜態方法和vm.$set(object, propertyName, value)
的實例方法,咱們看具體怎麼完成新屬性的依賴收集過程。
Vue.set = set
function set (target, key, val) {
//target必須爲非空對象
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
// 數組場景,調用重寫的splice方法,對新添加屬性收集依賴。
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
}
// 拿到目標源的Observer 實例
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
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
}
// 手動調用defineReactive,爲新屬性設置getter,setter
defineReactive###1(ob.value, key, val);
ob.dep.notify();
return val
}
複製代碼
按照分支分爲不一樣的四個處理邏輯:
splice
方法,而前面分析數組檢測時,遇到數組新增元素的場景,會調用ob.observeArray(inserted)
對數組新增的元素收集依賴。getter,setter
方法,並經過notify
觸發依賴更新。在上一節的內容中,咱們說到數據修改時會觸發setter
方法進行依賴的派發更新,而更新時會將每一個watcher
推到隊列中,等待下一個tick
到來時再執行DOM
的渲染更新操做。這個就是異步更新的過程。爲了說明異步更新的概念,須要牽扯到瀏覽器的事件循環機制和最優的渲染時機問題。因爲這不是文章的主線,我只用簡單的語言概述。
macro-task
和micro-task
macro-task
常見的有 setTimeout, setInterval, setImmediate, script腳本, I/O操做,UI渲染
micro-task
常見的有 promise, process.nextTick, MutationObserver
等micro-task
空,macro-task
隊列只有script
腳本,推出macro-task
的script
任務執行,腳本執行期間產生的macro-task,micro-task
推到對應的隊列中 4.2 執行所有micro-task
裏的微任務事件 4.3 執行DOM
操做,渲染更新頁面 4.4 執行web worker
等相關任務 4.5 循環,取出macro-task
中一個宏任務事件執行,重複4的操做。從上面的流程中咱們能夠發現,最好的渲染過程發生在微任務隊列的執行過程當中,此時他離頁面渲染過程最近,所以咱們能夠藉助微任務隊列來實現異步更新,它可讓複雜批量的運算操做運行在JS層面,而視圖的渲染只關心最終的結果,這大大下降了性能的損耗。
舉一個這一作法好處的例子: 因爲Vue
是數據驅動視圖更新渲染,若是咱們在一個操做中重複對一個響應式數據進行計算,例如 在一個循環中執行this.num ++
一千次,因爲響應式系統的存在,數據變化觸發setter
,setter
觸發依賴派發更新,更新調用run
進行視圖的從新渲染。這一次循環,視圖渲染要執行一千次,很明顯這是很浪費性能的,咱們只須要關注最後第一千次在界面上更新的結果而已。因此利用異步更新顯得格外重要。
Vue
用一個queue
收集依賴的執行,在下次微任務執行的時候統一執行queue
中Watcher
的run
操做,與此同時,相同id
的watcher
不會重複添加到queue
中,所以也不會重複執行屢次的視圖渲染。咱們看nextTick
的實現。
// 原型上定義的方法
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};
// 構造函數上定義的方法
Vue.nextTick = nextTick;
// 實際的定義
var callbacks = [];
function nextTick (cb, ctx) {
var _resolve;
// callbacks是維護微任務的數組。
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
// 將維護的隊列推到微任務隊列中維護
timerFunc();
}
// nextTick沒有傳遞參數,且瀏覽器支持Promise,則返回一個promise對象
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
複製代碼
nextTick
定義爲一個函數,使用方式爲Vue.nextTick( [callback, context] )
,當callback
通過nextTick
封裝後,callback
會在下一個tick
中執行調用。從實現上,callbacks
是一個維護了須要在下一個tick
中執行的任務的隊列,它的每一個元素都是須要執行的函數。pending
是判斷是否在等待執行微任務隊列的標誌。而timerFunc
是真正將任務隊列推到微任務隊列中的函數。咱們看timerFunc
的實現。
1.若是瀏覽器執行Promise
,那麼默認以Promsie
將執行過程推到微任務隊列中。
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// 手機端的兼容代碼
if (isIOS) { setTimeout(noop); }
};
// 使用微任務隊列的標誌
isUsingMicroTask = true;
}
複製代碼
flushCallbacks
是異步更新的函數,他會取出callbacks數組的每個任務,執行任務,具體定義以下:
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
// 取出callbacks數組的每個任務,執行任務
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
複製代碼
2.不支持promise
,支持MutataionObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
}
複製代碼
3.若是不支持微任務方法,則會使用宏任務方法,setImmediate
會先被使用
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
}
複製代碼
4.全部方法都不適合,會使用宏任務方法中的setTimeout
else {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
複製代碼
當nextTick
不傳遞任何參數時,能夠做爲一個promise
用,例如:
nextTick().then(() => {})
複製代碼
說了這麼多原理性的東西,回過頭來看看nextTick
的使用場景,因爲異步更新的原理,咱們在某一時間改變的數據並不會觸發視圖的更新,而是須要等下一個tick
到來時纔會更新視圖,下面是一個典型場景:
<input v-if="show" type="text" ref="myInput">
// js
data() {
show: false
},
mounted() {
this.show = true;
this.$refs.myInput.focus();// 報錯
}
複製代碼
數據改變時,視圖並不會同時改變,所以須要使用nextTick
mounted() {
this.show = true;
this.$nextTick(function() {
this.$refs.myInput.focus();// 正常
})
}
複製代碼
到這裏,關於響應式系統的分析大部份內容已經分析完畢,咱們上一節還遺留着一個問題,Vue
對用戶手動添加的watch
如何進行數據攔截。咱們先看看兩種基本的使用形式。
// watch選項
var vm = new Vue({
el: '#app',
data() {
return {
num: 12
}
},
watch: {
num() {}
}
})
vm.num = 111
// $watch api方式
vm.$watch('num', function() {}, {
deep: ,
immediate: ,
})
複製代碼
咱們以watch
選項的方式來分析watch
的細節,一樣從初始化提及,初始化數據會執行initWatch
,initWatch
的核心是createWatcher
。
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
// handler能夠是數組的形式,執行多個回調
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher (vm,expOrFn,handler,options) {
// 針對watch是對象的形式,此時回調回選項中的handler
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
複製代碼
不管是選項的形式,仍是api
的形式,最終都會調用實例的$watch
方法,其中expOrFn
是監聽的字符串,handler
是監聽的回調函數,options
是相關配置。咱們重點看看$watch
的實現。
Vue.prototype.$watch = function (expOrFn,cb,options) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
// 當watch有immediate選項時,當即執行cb方法,即不須要等待屬性變化,馬上執行回調。
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
}
複製代碼
$watch
的核心是建立一個user watcher
,options.user
是當前用戶定義watcher
的標誌。若是有immediate
屬性,則當即執行回調函數。 而實例化watcher
時會執行一次getter
求值,這時,user watcher
會做爲依賴被數據所收集。這個過程能夠參考data
的分析。
var Watcher = function Watcher() {
···
this.value = this.lazy
? undefined
: this.get();
}
Watcher.prototype.get = function get() {
···
try {
// getter回調函數,觸發依賴收集
value = this.getter.call(vm, vm);
}
}
複製代碼
watch
派發更新的過程很好理解,數據發生改變時,setter
攔截對依賴進行更新,而此前user watcher
已經被當成依賴收集了。這個時候依賴的更新就是回調函數的執行。
這一節是響應式系統構建的完結篇,data,computed
如何進行響應式系統設計,這在上一節內容已經詳細分析,這一節針對一些特殊場景作了分析。例如因爲Object.defineProperty
自身的缺陷,沒法對數組的新增刪除進行攔截檢測,所以Vue
對數組進行了特殊處理,重寫了數組的方法,並在方法中對數據進行攔截。咱們也重點介紹了nextTick
的原理,利用瀏覽器的事件循環機制來達到最優的渲染時機。文章的最後補充了watch
在響應式設計的原理,用戶自定義的watch
會建立一個依賴,這個依賴在數據改變時會執行回調。