上一節,咱們深刻分析了以
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
類的方法。這也是數組進行依賴收集和派發更新的前提。markdown
因爲數據初始化階段會利用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); } } } 複製代碼
咱們能夠經過截圖看最終依賴收集的結果。
收集前
收集後
/img/7.2.png)當調用數組的方法去添加或者刪除數據時,數據的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
會建立一個依賴,這個依賴在數據改變時會執行回調。