數據的雙向綁定,就是數據變化了自動更新視圖,視圖變化了自動更新數據,實際上視圖變化更新數據只要經過事件監聽就能夠實現了,並非數據雙向綁定的關鍵點。關鍵仍是數據變化了驅動視圖自動更新。vue
全部接下來,咱們詳細瞭解下數據如何驅動視圖更新的。
數據驅動視圖更新的重點就是,如何知道數據更新了,或者說數據更新了要如何主動的告訴咱們。可能你們都聽過,vue的數據雙向綁定原理是Object.defineProperty( )對屬性設置一個set/get,是這樣的沒錯,其實get/set只是能夠作到對數據的讀取進行劫持,就可讓咱們知道數據更新了。可是你詳細的瞭解整個過程嗎?
先來看張你們都不陌生的圖:react
介紹數據驅動更新以前,先介紹下面4個類和方法,而後從數據的入口initState開始按順序介紹,如下類和方法是如何協做,達到數據驅動更新的。git
這個方法,用處可就大了。
咱們看到他是給對象的鍵值添加get/set
方法,也就是對屬性的取值和賦值都加了攔截,同時用閉包給每一個屬性都保存了一個Dep
對象。
當讀取該值的時候,就把當前這個watcher
(Dep.target
)添加進他的dep裏的觀察者列表,這個watcher
也會把這個dep
添加進他的依賴列表。
當給設置值的時候,就讓這個閉包保存的dep
去通知他的觀察者列表的每個watcher
github
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 if (!getter && arguments.length === 2) { val = obj[key] } const setter = property && property.set let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const 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) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
什麼是可觀察者對象呢?
簡單來講:就是數據變動時能夠通知全部觀察他的觀察者。
一、取值的時候,能把要取值的watcher(觀察者對象)加入它的dep(依賴,也可叫觀察者管理器)管理的subs列表裏(即觀察者列表);
二、設置值的時候,有了變化,全部依賴於它的對象(即它的dep裏收集到的觀察者watcher)都獲得通知。segmentfault
這個類功能就是把數據轉化成可觀察對象。針對Object類型就調用defineReactive方法循環把每個鍵值都轉化。針對Array,首先是對Array通過特殊處理,使它能夠監控到數組發生了變化,而後對數組的每一項遞歸調用Observer進行轉化。
對於Array是如何處理的呢?這個放在下面單獨說。數組
export class Observer { /** *若是是對象就循環把對象的每個鍵值都轉化成可觀察者對象 */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } /** * 若是是數組就對數組的每一項作轉化 */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
這個類功能簡單來講就是管理數據的觀察者的。當有觀察者讀取數據時,保存觀察者到subs,以便當數據變化了的時候,能夠通知全部的觀察者去update,也能夠刪除subs裏的某個觀察者。promise
export default class Dep { addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } // 這個方法很是繞,Dep.target就是一個Watcher對象,Watcher把這個依賴加進他的依賴列表裏,而後調用dep.addSub再把這個Watcher加入到他的觀察者列表裏。 depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { // 省去了初始化各類屬性和option this.dirty = this.lazy // for lazy watchers // 解析expOrFn,賦值給this.getter // expOrFn也要明白他是什麼? // 當是渲染watcher時,expOrFn是updateComponent,即從新渲染執行render // 當是計算watcher時,expOrFn是計算屬性的計算方法 // 當是偵聽器watcher時,expOrFn是watch屬性的取值表達式,能夠去讀取要watch的數據,this.cb就是watch的handler屬性 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } this.value = this.lazy ? undefined : this.get() } /** * 執行this.getter,同時從新進行依賴收集 */ get () { pushTarget(this) const vm = this.vm let value = this.getter.call(vm, vm) if (this.deep) { // 對於deep的watch屬性,處理的很巧妙,traverse就是去遞歸讀取value的值, // 就會調用他們的get方法,進行了依賴收集 traverse(value) } popTarget() this.cleanupDeps() return value } /** * 不重複的把當前watcher添加進依賴的觀察者列表裏 */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /** * 清理依賴列表:當前的依賴列表和新的依賴列表比對,存在於this.deps裏面, * 卻不存在於this.newDeps裏面,說明這個watcher已經再也不觀察這個依賴了,因此 * 要讓個依賴從他的觀察者列表裏刪除本身,以避免形成沒必要要的watcher更新。而後 * 把this.newDeps的值賦給this.deps,再把this.newDeps清空 */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } /** * 當一個依賴改變的時候,通知它update */ update () { if (this.lazy) { // 對於計算watcher時,不須要當即執行計算方法,只要設置dirty,意味着 // 數據不是最新的了,使用時須要從新計算 this.dirty = true } else if (this.sync) { this.run() } else { // 調度watcher執行計算。 queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { this.cb.call(this.vm, value, oldValue) } } } /** * 對於計算屬性,當取值計算屬性時,發現計算屬性的watcher的dirty是true * 說明數據不是最新的了,須要從新計算,這裏就是從新計算計算屬性的值。 */ evaluate () { this.value = this.get() this.dirty = false } /** * 把這個watcher所觀察的全部依賴都傳給Dep.target,即給Dep.target收集 * 這些依賴。 * 舉個例子:具體能夠看state.js裏的createComputedGetter這個方法 * 當render裏依賴了計算屬性a,當渲染watcher在執行render時就會去 * 讀取a,而a會去從新計算,計算完了渲染watcher出棧,賦值給Dep.target * 而後執行watcher.depend,就是把這個計算watcher的全部依賴也加入給渲染watcher * 這樣,即便data.b沒有被直接用在render上,也經過計算屬性a被間接的是用了 * 當data.b發生改變時,也就能夠觸發渲染更新了 */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } }
綜上所述,就是vue數據驅動更新的方法了,下面是對整個過程的簡單概述:
每一個vue實例組件都有相應的watcher對象,這個watcher是負責更新渲染的。他會在組件渲染過程當中,把屬性記錄爲依賴,也就是說,她在渲染的時候就把全部渲染用到的prop和data都添加進watcher的依賴列表裏,只有用到的才加入。同時把這個watcher加入進data的依賴的訂閱者列表裏。也就是watcher保存了它都依賴了誰,data的依賴裏保存了都誰訂閱了它。這樣data在改變時,就能夠通知他的全部觀察者進行更新了。渲染的watcher觸發的更新就是從新渲染,後續的事情就是render生成虛擬DOM樹,進行diff比對,將不一樣反應到真實的DOM中。閉包
下面是Watcher的update方法,能夠看的除了是計算屬性和標記了是同步的狀況之外,所有都是推入觀察者隊列中,下一個tick時調用。也就是數據變化不是當即就去更新的,而是異步批量去更新的。app
update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
下面來看看queueWatcher方法異步
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
這裏使用了一個 has 的哈希map用來檢查是否當前watcher的id是否存在,若已存在則跳過,不存在則就push到queue,隊列中並標記哈希表has,用於下次檢驗,防止重複添加。由於執行更新隊列時,是每一個watcher都被執行run,若是是相同的watcher不必重複執行,這樣就算同步修改了一百次視圖中用到的data,異步更新計算的時候也只會更新最後一次修改。
nextTick(flushSchedulerQueue)
把回調方法flushSchedulerQueue傳遞給nextTick,一次異步更新,只要傳遞一次異步回調函數就能夠了,在這個異步回調裏統一批量的處理queue中的watcher,進行更新。
function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null watcher.run() } resetSchedulerState() }
每次執行異步回調更新,就是循環執行隊列裏的watcher.run方法。
在循環隊列以前對隊列進行了一次排序:
nextTick
export function nextTick (cb?: Function, ctx?: Object) { // 這個方法裏,我把關於不寫回調,使用promise的狀況處理去掉了,把trycatch都去掉了。 callbacks.push(() => { cb.call(ctx) }) if (!pending) { pending = true setTimeout(flushCallbacks, 0) // 異步任務進行了簡化 } }
下面是異步的回調方法flushCallbacks,遍歷執行callbacks裏的方法,也就是遍歷執行調用nextTick時傳入的回調方法。
你可能就要問了,queueWatcher的時候不是控制了只會調用一次nextTick嗎,爲啥要用callbacks數組來存儲呢。舉個例子:
你寫了一堆同步語句,改變了data等,而後又調用了一個this.$nextTick來作個異步回調,這個時候不就又會向callbacks數組裏push了一個回調方法嗎。
function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
不考慮兼容處理
本質就是改寫數組的原型方法。當數組調用methodsToPatch這些方法時,就意味者數組發生了變化,須要通知全部觀察者update。
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 保存數組的原始原型方法 const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let 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 }) })
後記
關於從數據入口initState開始解析的部分,寫在一篇裏篇幅太大,我放在下一篇文章了,記得去讀哦,能夠加深理解。
參考文章