1. 程式人生 > 前端設計 >Vue Computed 程式碼走讀

Vue Computed 程式碼走讀

廢話不說,直接看程式碼:

const computedWatcherOptions = {lazy: true}
function initComputed(vm: Component,computed: Object) {
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null))
  // computed properties are just getters during SSR
  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 ) } // component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm,key,userDef) } } } 複製程式碼

這裡遍歷了 computed 裡面的每一個屬性,並且為每一個屬性初始化了一個 Watcher 物件。這樣,當我們在 computed 裡面訪問 data 裡面的屬性時,就可以收集到依賴了。注意到這裡傳入了 { lazy: true }

,我們看看會有什麼效果:

    this.dirty = this.lazy // for lazy watchers
    ...
    this.value = this.lazy
      ? undefined
      : this.get()
複製程式碼

該屬性僅僅是標記了當前資料是 “髒的”,並且不會立即求值。所謂 “髒的” 指的是當前值已經髒了,需要重新求值了,這個後面會再提到。

然後我們看看 defineComputed 做了啥:

export function defineComputed(
  target: any,key: string,userDef: Object | Function
) {
  // 不考慮服務端渲染,這裡為 true
  const shouldCache = !isServerRendering()
  // 只看 computed 值為函式的情況
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  }
  Object.defineProperty(target,sharedPropertyDefinition)
}
複製程式碼

這裡執行了 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
    }
  }
}
複製程式碼

當我們第一次訪問計算屬性的時候會觸發 get,由於 dirty 為 true,所以這裡會走 watcher.evaluate 進行求值,並將 this.dirty 置為 false,這樣下次再對 computed 進行求值的時候就不會執行 watcher.evaluate() 了,這樣就實現了快取功能。

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
複製程式碼

而當 computed 依賴的資料變化的時候,會觸發 Watcherupdate

  update () {
    /* istanbul ignore else */
    // computed
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 入隊
      queueWatcher(this)
    }
  }
複製程式碼

這裡僅僅是把 dirty 又重置為了 true 以使得下次對 computed 進行求值的時候重新執行 watcher.evaluate()

快取功能分析完了,我們來看看下面這兩段段程式碼做了什麼:

if (Dep.target) {
  watcher.depend()
}
複製程式碼
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
複製程式碼

這裡有點難理解,我們用一個例子來說明:

首次渲染的時候元件會例項化一個 Watcher 物件,同時會觸發對 description 的求值,這裡又會例項化一個 Watcher,而 description 中對 fullName 進行求值,又會例項化一個 Watcher。這樣就形成了一個依賴棧,靠近棧底的元素會依賴其上面的元素。

當執行 fullName 的時候,由於其依賴了 firstNamesecondName,所以它會被新增進兩者的 dep 中。收集完後會執行 popTarget(),此時 Dep.target 指向 descriptionWatcher,然後會執行 watcher.depend() 。注意這裡的 watcher 還是 fullName 的,即 fullName 依賴啥,其他依賴 fullNameWatcher 也需要跟它有同樣的依賴。舉個例子:兒子依賴老爸,老爸是個啃老族依賴父母,所以孫子也間接依賴了爺爺奶奶。同樣的,元件的 Watcher 也是同理。

我們除錯下這段程式碼,發現跟我們的分析是一致的: