收集的目的就是爲了當咱們修改數據的時候,能夠對相關的依賴派發更新,那麼這一節咱們來詳細分析這個過程。react
setter 部分的邏輯:express
/** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // ... set: function reactiveSetter (newVal) { const 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() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
假設咱們有以下模板:性能優化
<div id="demo"> {{name}} </div>
咱們知道這段模板將會被編譯成渲染函數,接着建立一個渲染函數的觀察者,從而對渲染函數求值,在求值的過程當中會觸發數據對象 name 屬性的 get 攔截器函數,進而將該觀察者收集到 name 屬性經過閉包引用的「筐」中,即收集到 Dep 實例對象中。這個 Dep 實例對象是屬於 name 屬性自身所擁有的,這樣當咱們嘗試修改數據對象 name 屬性的值時就會觸發 name 屬性的 set 攔截器函數,這樣就有機會調用 Dep 實例對象的 notify 方法,從而觸發了響應,以下代碼截取自 defineReactive 函數中的 set 攔截器函數:閉包
set: function reactiveSetter (newVal) { // 省略... dep.notify() }
如上高亮代碼所示,能夠看到當屬性值變化時確實經過 set 攔截器函數調用了 Dep 實例對象的 notify 方法,這個方法就是用來通知變化的,咱們找到 Dep 類的 notify 方法,以下:異步
export default class Dep { // 省略... constructor () { this.id = uid++ this.subs = [] } // 省略... notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
你們觀察 notify 函數能夠發現其中包含以下這段 if 條件語句塊:async
if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) }
對於這段代碼的做用,咱們會在本章的 同步執行觀察者 一節中對其詳細講解,如今你們能夠徹底忽略,這並不影響咱們對代碼的理解。若是咱們去掉如上這段代碼,那麼 notify 函數將變爲:函數
notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
notify 方法只作了一件事,就是遍歷當前 Dep 實例對象的 subs 屬性中所保存的全部觀察者對象,並逐個調用觀察者對象的 update 方法,這就是觸發響應的實現機制,那麼你們應該也猜到了,從新求值的操做應該是在 update 方法中進行的,那咱們就找到觀察者對象的 update 方法,看看它作了什麼事情,以下:oop
update () { /* istanbul ignore else */ if (this.computed) { // 省略... } else if (this.sync) { this.run() } else { queueWatcher(this) }
在 update 方法中代碼被拆分紅了三部分,即 if...else if...else 語句塊。首先 if 語句塊的代碼會在判斷條件 this.computed 爲真的狀況下執行,咱們說過 this.computed 屬性是用來判斷該觀察者是否是計算屬性的觀察者,這部分代碼咱們將會在計算屬性部分詳細講解。也就是說渲染函數的觀察者確定是不會執行 if 語句塊中的代碼的,此時會繼續判斷 else...if 語句的條件 this.sync 是否爲真,咱們知道 this.sync 屬性的值就是建立觀察者實例對象時傳遞的第三個選項參數中的 sync 屬性的值,這個值的真假表明了當變化發生時是否同步更新變化。對於渲染函數的觀察者來說,它並非同步更新變化的,而是將變化放到一個異步更新隊列中,也就是 else 語句塊中代碼所作的事情,即 queueWatcher 會將當前觀察者對象放到一個異步更新隊列,這個隊列會在調用棧被清空以後按照必定的順序執行。關於更多異步更新隊列的內容咱們會在後面單獨講解,這裏你們只須要知道一件事情,那就是不管是同步更新變化仍是將更新變化的操做放到異步更新隊列,真正的更新變化操做都是經過調用觀察者實例對象的 run 方法完成的。因此此時咱們應該把目光轉向 run 方法,以下:性能
run () { if (this.active) { this.getAndInvoke(this.cb) } }
run 方法的代碼很簡短,它判斷了當前觀察者實例的 this.active 屬性是否爲真,其中 this.active 屬性用來標識一個觀察者是否處於激活狀態,或者可用狀態。若是觀察者處於激活狀態那麼 this.active 的值爲真,此時會調用觀察者實例對象的 getAndInvoke 方法,並以 this.cb 做爲參數,咱們知道 this.cb 屬性是一個函數,咱們稱之爲回調函數,當變化發生時會觸發,可是對於渲染函數的觀察者來說,this.cb 屬性的值爲 noop,即什麼都不作優化
如今咱們終於找到了更新變化的根源,那就是 getAndInvoke 方法,以下:
getAndInvoke (cb: Function) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }
在 getAndInvoke 方法中,第一句代碼就調用了 this.get 方法,這意味着從新求值,這也證實了咱們在上一小節中的假設。對於渲染函數的觀察者來說,從新求值其實等價於從新執行渲染函數,最終結果就是從新生成了虛擬DOM並更新真實DOM,這樣就完成了從新渲染的過程。在從新調用 this.get 方法以後是一個 if 語句塊,實際上對於渲染函數的觀察者來說並不會執行這個 if 語句塊,由於 this.get 方法的返回值其實就等價於 updateComponent 函數的返回值,這個值將永遠都是 undefined。實際上 if 語句塊內的代碼是爲非渲染函數類型的觀察者準備的,它用來對比新舊兩次求值的結果,當值不相等的時候會調用經過參數傳遞進來的回調。咱們先看一下判斷條件,以下:
const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // 省略... }
首先對比新值 value 和舊值 this.value 是否相等,只有在不相等的狀況下才須要執行回調,可是兩個值相等就必定不執行回調嗎?未必,這個時候就須要檢測第二個條件是否成立,即 isObject(value),判斷新值的類型是不是對象,若是是對象的話即便值不變也須要執行回調,注意這裏的「不變」指的是引用不變,以下代碼所示:
const data = { obj: { a: 1 } } const obj1 = data.obj data.obj.a = 2 const obj2 = data.obj console.log(obj1 === obj2) // true
上面的代碼中因爲 obj1 與 obj2 具備相同的引用,因此他們老是相等的,但其實數據已經變化了,這就是判斷 isObject(value) 爲真則執行回調的緣由。
接下來咱們就看一下 if 語句塊內的代碼:
const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) }
代碼若是執行到了 if 語句塊內,則說明應該執行觀察者的回調函數了。首先定義了 oldValue 常量,它的值是舊值,緊接着使用新值更新了 this.value 的值。咱們能夠看到如上代碼中是如何執行回調的:
cb.call(this.vm, value, oldValue)
將回調函數的做用域修改成當前 Vue 組件對象,而後傳遞了兩個參數,分別是新值和舊值。
另外你們可能注意到了這句代碼:this.dirty = false,將觀察者實例對象的 this.dirty 屬性設置爲 false,實際上 this.dirty 屬性也是爲計算屬性準備的,因爲計算屬性是惰性求值,因此在實例化計算屬性的時候 this.dirty 的值會被設置爲 true,表明着尚未求值,後面當真正對計算屬性求值時,也就是執行如上代碼時纔會將 this.dirty 設置爲 false,表明着已經求過值了。
除此以外,咱們注意以下代碼:
if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) }
在調用回調函數的時候,若是觀察者對象的 this.user 爲真意味着這個觀察者是開發者定義的,所謂開發者定義的是指那些經過 watch 選項或 $watch 函數定義的觀察者,這些觀察者的特色是回調函數是由開發者編寫的,因此這些回調函數在執行的過程當中其行爲是不可預知的,極可能出現錯誤,這時候將其放到一個 try...catch 語句塊中,這樣當錯誤發生時咱們就可以給開發者一個友好的提示。而且咱們注意到在提示信息中包含了 this.expression 屬性,咱們前面說過該屬性是被觀察目標(expOrFn)的字符串表示,這樣開發者就能清楚的知道是哪裏發生了錯誤。
異步更新的意義----性能優化
當全部的突變完成以後,再一次性的執行隊列中全部觀察者的更新方法,同時清空隊列,這樣就達到了優化的目的
看一看其具體實現,咱們知道當修改一個屬性的值時,會經過執行該屬性所收集的全部觀察者對象的 update 方法進行更新,那麼咱們就找到觀察者對象的 update 方法,以下:
update () { /* istanbul ignore else */ if (this.computed) { // 省略... } else if (this.sync) { this.run() } else { queueWatcher(this) } }
若是沒有指定這個觀察者是同步更新(this.sync 爲真),那麼這個觀察者的更新機制就是異步的,這時當調用觀察者對象的 update 方法時,在 update 方法內部會調用 queueWatcher 函數,並將當前觀察者對象做爲參數傳遞,queueWatcher 函數的做用就是咱們前面講到過的,它將觀察者放到一個隊列中等待全部突變完成以後統一執行更新。
queueWatcher 函數來自 src/core/observer/scheduler.js 文件,以下是 queueWatcher 函數的所有代碼:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
queueWatcher 函數接收觀察者對象做爲參數,首先定義了 id 常量,它的值是觀察者對象的惟一 id,而後執行 if 判斷語句,以下是簡化的代碼:
export function queueWatcher (watcher: Watcher) { const id = watcher.id // let has: { [key: number]: ?true } = {} if (has[id] == null) { has[id] = true // 省略... } }
當 queueWatcher 函數被調用以後,會嘗試將該觀察者放入隊列中,並將該觀察者的 id 值登記到 has 對象上做爲 has 對象的屬性同時將該屬性值設置爲 true。該 if 語句以及變量 has 的做用就是用來避免將相同的觀察者重複入隊的。在該 if 語句塊內執行了真正的入隊操做,以下代碼高亮的部分所示:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // let flushing = false 初始值是 false if (!flushing) { // const queue: Array<Watcher> = [] queue.push(watcher) } else { // 省略... } // 省略... } }
flushing 變量是一個標誌,咱們知道放入隊列 queue 中的全部觀察者將會在突變完成以後統一執行更新,當更新開始時會將 flushing 變量的值設置爲 true,表明着此時正在執行更新,因此根據判斷條件 if (!flushing) 可知只有當隊列沒有執行更新時纔會簡單地將觀察者追加到隊列的尾部有的同窗可能會問:「難道在隊列執行更新的過程當中還會有觀察者入隊的操做嗎?」,其實是會的,典型的例子就是計算屬性,好比隊列執行更新時常常會執行渲染函數觀察者的更新,渲染函數中極可能有計算屬性的存在,因爲計算屬性在實現方式上與普通響應式屬性有所不一樣,因此當觸發計算屬性的 get 攔截器函數時會有觀察者入隊的行爲,這個時候咱們須要特殊處理,也就是 else 分支的代碼,以下:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // 省略... } }
當變量 flushing 爲真時,說明隊列正在執行更新,這時若是有觀察者入隊則會執行 else 分支中的代碼,這段代碼的做用是爲了保證觀察者的執行順序,如今你們只須要知道觀察者會被放入 queue 隊列中便可
接着咱們再來看以下代碼:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 省略... // queue the flush > if (!waiting) { > waiting = true > if (process.env.NODE_ENV !== 'production' && !config.async) { > flushSchedulerQueue() > return > } > nextTick(flushSchedulerQueue) > } } }
你們觀察如上代碼中有這樣一段 if 條件語句:
if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return }
在接下來的講解中咱們將會忽略這段代碼,並在 同步執行觀察者 一節中補充講解,
咱們回到那段高亮的代碼,這段代碼是一個 if 語句塊,其中變量 waiting 一樣是一個標誌,它也定義在 scheduler.js 文件頭部,初始值爲 false
let waiting = false
咱們看 if 語句塊內的代碼就知道了,在 if 語句塊內先將 waiting 的值設置爲 true,這意味着不管調用多少次 queueWatcher 函數,該 if 語句塊的代碼只會執行一次。接着調用 nextTick 並以 flushSchedulerQueue 函數做爲參數,其中 flushSchedulerQueue 函數的做用之一就是用來將隊列中的觀察者統一執行更新的。對於 nextTick 相信你們已經很熟悉了,其實最好理解的方式就是把 nextTick 看作 setTimeout(fn, 0),以下:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 省略... // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } setTimeout(flushSchedulerQueue, 0) } } }
咱們對 Vue 數據修改派發更新的過程也有了認識,實際上就是當數據發生變化的時候,觸發 setter 邏輯,把在依賴過程當中訂閱的的全部觀察者,也就是 watcher,都觸發它們的 update 過程,這個過程又利用了隊列作了進一步優化,在 nextTick 後執行全部 watcher 的 run,最後執行它們的回調函數。nextTick 是 Vue 一個比較核心的實現了。