Vue資料繫結簡析

coffee-ai發表於2019-05-06

作為MVVM框架的一種,Vue最為人津津樂道的當是資料與檢視的繫結,將直接操作DOM節點變為修改data資料,利用Virtual DomDiff對比新舊檢視,從而實現更新。不僅如此,還可以通過Vue.prototype.$watch來監聽data的變化並執行回撥函式,實現自定義的邏輯。雖然日常的編碼運用已經駕輕就熟,但未曾去深究技術背後的實現原理。作為一個好學的程式設計師,知其然更要知其所以然,本文將從原始碼的角度來對Vue響應式資料中的觀察者模式進行簡析。

初始化Vue例項

在閱讀原始碼時,因為檔案繁多,引用複雜往往使我們不容易抓住重點,這裡我們需要找到一個入口檔案,從Vue建構函式開始,拋開其他無關因素,一步步理解響應式資料的實現原理。首先我們找到Vue建構函式:

// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
複製程式碼
// src/core/instance/init.js
Vue.prototype._init = function (options) {
    ...
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // 初始化vm例項的$options
    if (options && options._isComponent) {
        initInternalComponent(vm, options)
    } else {
        vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
        )
    }
    ...
    initLifecycle(vm) // 梳理例項的parent、root、children和refs,並初始化一些與生命週期相關的例項屬性
    initEvents(vm) // 初始化例項的listeners
    initRender(vm) // 初始化插槽,繫結createElement函式的vm例項
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)  // 掛載元件到節點
    }
}
複製程式碼

為了方便閱讀,我們去除了flow型別檢查和部分無關程式碼。可以看到,在例項化Vue元件時,會呼叫Vue.prototype._init,而在方法內部,資料的初始化操作主要在initState(這裡的initInjectionsinitProvideinitProps類似,在理解了initState原理後自然明白),因此我們重點來關注initState

// src/core/instance/state.js
export function initState (vm) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

首先初始化了一個_watchers陣列,用來存放watcher,之後根據例項的vm.$options,相繼呼叫initPropsinitMethodsinitDatainitComputedinitWatch方法。

initProps

function initProps (vm, propsOptions) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    ...
    defineReactive(props, key, value)
    if (!(key in vm)) {
      proxy(vm, '_props', key)
    }
  }
  toggleObserving(true)
}
複製程式碼

在這裡,vm.$options.propsData是通過父元件傳給子元件例項的資料物件,如<my-element :item="false"></my-element>中的{item: false},然後初始化vm._propsvm.$options._propKeys分別用來儲存例項的props資料和keys,因為子元件中使用的是通過proxy引用的_props裡的資料,而不是父元件傳遞的propsData,所以這裡快取了_propKeys,用來updateChildComponent時能更新vm._props。接著根據isRoot是否是根元件來判斷是否需要呼叫toggleObserving(false),這是一個全域性的開關,來控制是否需要給物件新增__ob__屬性。這個相信大家都不陌生,一般的元件的data等資料都包含這個屬性,這裡先不深究,等之後和defineReactive時一起講解。因為props是通過父傳給子的資料,在父元素initState時已經把__ob__新增上了,所以在不是例項化根元件時關閉了這個全域性開關,待呼叫結束前在通過toggleObserving(true)開啟。

之後是一個for迴圈,根據元件中定義的propsOptions物件來設定vm._props,這裡的propsOptions就是我們常寫的

export default {
    ...
    props: {
        item: {
            type: Object,
            default: () => ({})
        }
    }
}
複製程式碼

迴圈體內,首先

const value = validateProp(key, propsOptions, propsData, vm)
複製程式碼

validateProp方法主要是校驗資料是否符合我們定義的type,以及在propsData裡未找到key時,獲取預設值並在物件上定義__ob__,最後返回相應的值,在這裡不做展開。

這裡我們先跳過defineReactive,看最後

if (!(key in vm)) {
  proxy(vm, '_props', key)
}
複製程式碼

其中proxy方法:

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製程式碼

vm不存在key屬性時,通過Object.defineProperty使得我們能通過vm[key]訪問到vm._props[key]

defineReactive

initProps中,我們瞭解到其首先根據使用者定義的vm.$options.props物件,通過對父元件設定的傳值物件vm.$options.propsData進行資料校驗,返回有效值並儲存到vm._props,同時儲存相應的keyvm.$options._propKeys以便進行子元件的props資料更新,最後利用getter/setter存取器屬性,將vm[key]指向對vm._props[key]的操作。但其中跳過了最重要的defineReactive,現在我們將通過閱讀defineReactive原始碼,瞭解響應式資料背後的實現原理。

// src/core/observer/index.js
export function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  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)
  ...
}
複製程式碼

首先const dep = new Dep()例項化了一個dep,在這裡利用閉包來定義一個依賴項,用以與特定的key相對應。因為其通過Object.defineProperty重寫target[key]getter/setter來實現資料的響應式,因此需要先判斷物件keyconfigurable屬性。接著

if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
}
複製程式碼

arguments.length === 2意味著呼叫defineReactive時未傳遞val值,此時valundefined,而!getter || setter判斷條件則表示如果在property存在getter且不存在setter的情況下,不會獲取key的資料物件,此時valundefined,之後呼叫observe時將不對其進行深度觀察。正如之後的setter訪問器中的:

if (getter && !setter) return
複製程式碼

此時資料將是隻讀狀態,既然是隻讀狀態,則不存在資料修改問題,繼而無須深度觀察資料以便在資料變化時呼叫觀察者註冊的方法。

Observe

defineReactive裡,我們先獲取了target[key]descriptor,並快取了對應的gettersetter,之後根據判斷選擇是否獲取target[key]對應的val,接著是

let childOb = !shallow && observe(val)
複製程式碼

根據shallow標誌來確定是否呼叫observe,我們來看下observe函式:

// src/core/observer/index.js
export function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let 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
}
複製程式碼

首先判斷需要觀察的資料是否為物件以便通過Object.defineProperty定義__ob__屬性,同時需要value不屬於VNode的例項(VNode例項通過Diff補丁演算法來實現例項對比並更新)。接著判斷value是否已有__ob__,如果沒有則進行後續判斷:

  • shouldObserve:全域性開關標誌,通過toggleObserving來修改。
  • !isServerRendering():判斷是否服務端渲染。
  • (Array.isArray(value) || isPlainObject(value)):陣列和純物件時才允許新增__ob__進行觀察。
  • Object.isExtensible(value):判斷value是否可擴充套件。
  • !value._isVue:避免Vue例項被觀察。

滿足以上五個條件時,才會呼叫ob = new Observer(value),接下來我們要看下Observer類裡做了哪些工作

// src/core/observer/index.js
export class Observer {
  constructor (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)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
複製程式碼

建構函式裡初始化了valuedepvmCount三個屬性,為this.value新增__ob__物件並指向自己,即value.__ob__.value === value,這樣就可以通過value__ob__物件取到depvaluevmCount的作用主要是用來區分是否為Vue例項的根datadep的作用這裡先不介紹,待與getter/setter裡的dep一起解釋。

接著根據value是陣列還是純物件來分別呼叫相應的方法,對value進行遞迴操作。當value為純物件時,呼叫walk方法,遞迴呼叫defineReactive。當value是陣列型別時,首先判斷是否有__proto__,有就使用__proto__實現原型鏈繼承,否則用Object.defineProperty實現拷貝繼承。其中繼承的基類arrayMethods來自src/core/observer/array.js

// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // cache original 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
  })
})
複製程式碼

這裡為什麼要對陣列的例項方法進行重寫呢?程式碼裡的methodsToPatch這些方法並不會返回新的陣列,導致無法觸發setter,因而不會呼叫觀察者的方法。所以重寫了這些變異方法,使得在呼叫的時候,利用observeArray對新插入的陣列元素新增__ob__,並能夠通過ob.dep.notify手動通知對應的被觀察者執行註冊的方法,實現陣列元素的響應式。

if (asRootData && ob) {
    ob.vmCount++
}
複製程式碼

最後新增這個if判斷,在Vue例項的根data物件上,執行ob.vmCount++,這裡主要為了後面根據ob.vmCount來區分是否為根資料,從而在其上執行Vue.setVue.delete

getter/setter

在對val進行遞迴操作後(假如需要的話),將obj[key]的資料物件封裝成了一個被觀察者,使得能夠被觀察者觀察,並在需要的時候呼叫觀察者的方法。這裡通過Object.defineProperty重寫了obj[key]的訪問器屬性,對getter/setter操作做了攔截處理,defineReactive剩餘的程式碼具體如下:

...
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) {
    ...
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})
複製程式碼

首先在getter呼叫時,判斷Dep.target是否存在,若存在則呼叫dep.depend。我們先不深究Dep.target,只當它是一個觀察者,比如我們常用的某個計算屬性,呼叫dep.depend會將dep當做計算屬性的依賴項存入其依賴列表,並把這個計算屬性註冊到這個dep。這裡為什麼需要互相引用呢?這是因為一個target[key]可以充當多個觀察者的依賴項,同時一個觀察者可以有多個依賴項,他們之間屬於多對多的關係。這樣當某個依賴項改變時,我們可以根據dep裡維護的觀察者,呼叫他們的註冊方法。現在我們回過頭來看Dep

// src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  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()
    }
  }
}
複製程式碼

建構函式裡,首先新增一個自增的uid用以做dep例項的唯一性標誌,接著初始化一個觀察者列表subs,並定義了新增觀察者方法addSub和移除觀察者方法removeSub。可以看到其在getter中呼叫的depend會將當前這個dep例項新增到觀察者的依賴項,在setter裡呼叫的notify會執行各個觀察者註冊的update方法,Dep.target.addDep這個方法將在之後的Watcher裡進行解釋。簡單來說就是會在keygetter觸發時進行dep依賴收集到watcher並將Dep.target新增到當前dep的觀察者列表,這樣在keysetter觸發時,能夠通過觀察者列表,執行觀察者的update方法。

當然,在getter中還有如下幾行程式碼:

if (childOb) {
    childOb.dep.depend()
    if (Array.isArray(value)) {
        dependArray(value)
    }
}
複製程式碼

這裡可能會有疑惑,既然已經呼叫了dep.depend,為什麼還要呼叫childOb.dep.depend?兩個dep之間又有什麼關係呢?

其實這兩個dep的分工是不同的。對於資料的增、刪,利用childOb.dep.notify來呼叫觀察者方法,而對於資料的修改,則使用的dep.notify,這是因為setter訪問器無法監聽到物件資料的新增和刪除。舉個例子:

const data = {
    arr: [{
        value: 1
    }],
}

data.a = 1; // 無法觸發setter
data.arr[1] = {value: 2}; // 無法觸發setter
data.arr.push({value: 3}); // 無法觸發setter
data.arr = [{value: 4}]; // 可以觸發setter
複製程式碼

還記得Observer建構函式裡針對陣列型別value的響應式轉換嗎?通過重寫value原型鏈,使得對於新插入的資料:

if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
複製程式碼

將其轉換為響應式資料,並通過ob.dep.notify來呼叫觀察者的方法,而這裡的觀察者列表就是通過上述的childOb.dep.depend來收集的。同樣的,為了實現物件新增資料的響應式,我們需要提供相應的hack方法,而這就是我們常用的Vue.set/Vue.delete

// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  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
  }
  const ob = (target: any).__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
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
複製程式碼
  • 判斷value是否為陣列,如果是,直接呼叫已經hack過的splice即可。
  • 是否已存在key,有的話說明已經是響應式了,直接修改即可。
  • 接著判斷target.__ob__是否存在,如果沒有說明該物件無須深度觀察,設定返回當前的值。
  • 最後,通過defineReactive來設定新增的key,並呼叫ob.dep.notify通知到觀察者。

現在我們瞭解了childOb.dep.depend()是為了將當前watcher收集到childOb.dep,以便在增、刪資料時能通知到watcher。而在childOb.dep.depend()之後還有:

if (Array.isArray(value)) {
    dependArray(value)
}
複製程式碼
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
複製程式碼

在觸發target[key]getter時,如果value的型別為陣列,則遞迴將其每個元素都呼叫__ob__.dep.depend,這是因為無法攔截陣列元素的getter,所以將當前watcher收集到陣列下的所有__ob__.dep,這樣當其中一個元素觸發增、刪操作時能通知到觀察者。比如:

const data = {
    list: [[{value: 0}]],
};
data.list[0].push({value: 1});
複製程式碼

這樣在data.list[0].__ob__.notify時,才能通知到watcher

target[key]getter主要作用:

  • Dep.target收集到閉包中dep的觀察者列表,以便在target[key]setter修改資料時通知觀察者
  • 根據情況對資料進行遍歷新增__ob__,將Dep.target收集到childOb.dep的觀察者列表,以便在增加/刪除資料時能通知到觀察者
  • 通過dependArray將陣列型的value遞迴進行觀察者收集,在陣列元素髮生增、刪、改時能通知到觀察者

target[key]setter主要作用是對新資料進行觀察,並通過閉包儲存到childOb變數供getter使用,同時呼叫dep.notify通知觀察者,在此就不再展開。

Watcher

在前面的篇幅中,我們主要介紹了defineReactive來定義響應式資料:通過閉包儲存depchildOb,在getter時來進行觀察者的收集,使得在資料修改時能觸發dep.notifychildOb.dep.notify來呼叫觀察者的方法進行更新。但具體是如何進行watcher收集的卻未做過多解釋,現在我們將通過閱讀Watcher來了解觀察者背後的邏輯。

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    ...
  }
}
複製程式碼

這是Vue計算屬性的初始化操作,去掉了一部分不影響的程式碼。首先初始化物件vm._computedWatchers用以儲存所有的計算屬性,isSSR用以判斷是否為服務端渲染。再根據我們編寫的computed鍵值對迴圈遍歷,如果不是服務端渲染,則為每個計算屬性例項化一個Watcher,並以鍵值對的形式儲存到vm._computedWatchers物件,接下來我們主要看下Watcher這個類。

Watcher的建構函式

建構函式接受5個引數,其中當前Vue例項vm、求值表示式expOrFn(支援Function或者String,計算屬性中一般為Function),回撥函式cb這三個為必傳引數。設定this.vm = vm用以後續繫結this.getter的執行環境,並將this推入vm._watchers(vm._watchers用以維護例項vm中所有的觀察者),另外根據是否為渲染觀察者來賦值vm._watcher = this(常用的render即為渲染觀察者)。接著根據options進行一系列的初始化操作。其中有幾個屬性:

  • this.lazy:設定是否懶求值,這樣能保證有多個被觀察者發生變化時,能只呼叫求值一次。
  • this.dirty:配合this.lazy,用以標記當前觀察者是否需要重新求值。
  • this.depsthis.newDepsthis.depIdsthis.newDepIds:用以維護被觀察物件的列表。
  • this.getter:求值函式。
  • this.value:求值函式返回的值,即為計算屬性中的值。

Watcher的求值

因為計算屬性是惰性求值,所以我們繼續看initComputed迴圈體:

if (!(key in vm)) {
  defineComputed(vm, key, userDef)
}
複製程式碼

defineComputed主要將userDef轉化為getter/setter訪問器,並通過Object.definePropertykey設定到vm上,使得我們能通過this[key]直接訪問到計算屬性。接下來我們主要看下userDef轉為getter中的createComputedGetter函式:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
複製程式碼

利用閉包儲存計算屬性的key,在getter觸發時,首先通過this._computedWatchers[key]獲取到之前儲存的watcher,如果watcher.dirtytrue時呼叫watcher.evaluate(執行this.get()求值操作,並將當前watcherdirty標記為false),我們主要看下get操作:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    ...
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
複製程式碼

可以看到,求值時先執行pushTarget(this),通過查閱src/core/observer/dep.js,我們可以看到:

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
複製程式碼

pushTarget主要是把watcher例項進棧,並賦值給Dep.target,而popTarget則相反,把watcher例項出棧,並將棧頂賦值給Dep.targetDep.target這個我們之前在getter裡見到過,其實就是當前正在求值的觀察者。這裡在求值前將Dep.target設定為watcher,使得在求值過程中獲取資料時觸發getter訪問器,從而呼叫dep.depend,繼而執行watcheraddDep操作:

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)
    }
  }
}
複製程式碼

先判斷newDepIds是否包含dep.id,沒有則說明尚未新增過這個dep,此時將depdep.id分別加到newDepIdsnewDeps。如果depIds不包含dep.id,則說明之前未新增過此dep,因為是雙向新增的(將dep新增到watcher的同時也需要將watcher收集到dep),所以需要呼叫dep.addSub,將當前watcher新增到新的dep的觀察者佇列。

if (this.deep) {
  traverse(value)
}
複製程式碼

再接著根據this.deep來呼叫traversetraverse的作用主要是遞迴遍歷觸發valuegetter,呼叫所有元素的dep.depend()並過濾重複收集的dep。最後呼叫popTarget()將當前watcher移出棧,並執行cleanupDeps

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  ...
}
複製程式碼

遍歷this.deps,如果在newDepIds中不存在dep.id,則說明新的依賴裡不包含當前dep,需要到dep的觀察者列表裡去移除當前這個watcher,之後便是depIdsnewDepIdsdepsnewDeps的值交換,並清空newDepIdsnewDeps。到此完成了對watcher的求值操作,同時更新了新的依賴,最後返回value即可。

回到createComputedGetter接著看:

if (Dep.target) {
  watcher.depend()
}
複製程式碼

當執行計算屬性的getter時,有可能表示式中還有別的計算屬性依賴,此時我們需要執行watcher.depend將當前watcherdeps新增到Dep.target即可。最後返回求得的watcher.value即可。

總的來說我們從this[key]觸發watcherget函式,將當前watcher入棧,通過求值表示式將所需要的依賴dep收集到newDepIdsnewDeps,並將watcher新增到對應dep的觀察者列表,最後清除無效dep並返回求值結果,這樣就完成了依賴關係的收集。

Watcher的更新

以上我們瞭解了watcher的依賴收集和dep的觀察者收集的基本原理,接下來我們瞭解下dep的資料更新時如何通知watcher進行update操作。

notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
複製程式碼

首先在dep.notify時,我們將this.subs拷貝出來,防止在watcherget時候subs發生更新,之後呼叫update方法:

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
複製程式碼
  • 如果是lazy,則將其標記為this.dirty = true,使得在this[key]getter觸發時進行watcher.evaluate呼叫計算。
  • 如果是sync同步操作,則執行this.run,呼叫this.get求值和執行回撥函式cb
  • 否則執行queueWatcher,選擇合適的位置,將watcher加入到佇列去執行即可,因為和響應式資料無關,故不再展開。

小結

因為篇幅有限,只對資料繫結的基本原理做了基本的介紹,在這畫了一張簡單的流程圖來幫助理解Vue的響應式資料,其中省略了一些VNode等不影響理解的邏輯及邊界條件,儘可能簡化地讓流程更加直觀:

Vue資料繫結簡析

最後,本著學習的心態,在寫作的過程中也零零碎碎的查閱了很多資料,其中難免出現紕漏以及未覆蓋到的知識點,如有錯誤,還請不吝指教。

相關文章