7、vue計算屬性

image

細節流程圖

image

初始化

計算屬性的初始化是發生在 Vue 實例初始化階段的 initState 函數中,執行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定義在 src/core/instance/state.js 中:緩存

const computedWatcherOptions = { computed: 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 (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    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)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

函數首先建立 vm._computedWatchers 爲一個空對象,接着對 computed 對象作遍歷,拿到計算屬性的每個 userDef,而後嘗試獲取這個 userDef 對應的 getter 函數,拿不到則在開發環境下報警告。接下來爲每個 getter 建立一個 watcher,這個 watcher 和渲染 watcher 有一點很大的不一樣,它是一個 computed watcher,由於 const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差異我稍後會介紹。最後對判斷若是 key 不是 vm 的屬性,則調用 defineComputed(vm, key, userDef),不然判斷計算屬性對於的 key 是否已經被 data 或者 prop 所佔用,若是是的話則在開發環境報相應的警告。函數

接下來須要重點關注 defineComputed 的實現:oop

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

這段邏輯很簡單,其實就是利用 Object.defineProperty 給計算屬性對應的 key 值添加 getter 和 setter,setter 一般是計算屬性是一個對象,而且擁有 set 方法的時候纔有,不然是一個空函數。在平時的開發場景中,計算屬性有 setter 的狀況比較少,咱們重點關注一下 getter 部分,緩存的配置也先忽略,最終 getter 對應的是 createComputedGetter(key) 的返回值,來看一下它的定義:優化

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

createComputedGetter 返回一個函數 computedGetter,它就是計算屬性對應的 getter。this

整個計算屬性的初始化過程到此結束,咱們知道計算屬性是一個 computed watcher,它和普通的 watcher 有什麼區別呢,爲了更加直觀,接下來來咱們來經過一個例子來分析 computed watcher 的實現。lua

例子

以上關於計算屬性相關初始化工做已經完成了,初始化計算屬性的過程當中主要建立了計算屬性觀察者以及將計算屬性定義到組件實例對象上,接下來咱們將經過一些例子來分析計算屬性是如何實現的,假設咱們有以下代碼:prototype

data () {
  return {
    a: 1
  }
},
computed: {
  compA () {
    return this.a + 1
  }
}

如上代碼中,咱們定義了本地數據 data,它擁有一個響應式的屬性 a,咱們還定義了計算屬性 compA,它的值將依據 a 的值來計算求得。另外咱們假設有以下模板:設計

<div>{{compA}}</div>

模板中咱們使用到了計算屬性,咱們知道模板會被編譯成渲染函數,渲染函數的執行將觸發計算屬性 compA 的 get 攔截器函數,那麼 compA 的攔截器函數是什麼呢?就是咱們前面分析的 sharedPropertyDefinition.get 函數,咱們知道在非服務端渲染的狀況下,這個函數爲:code

sharedPropertyDefinition.get = function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    watcher.depend()
    return watcher.evaluate()
  }
}

也就是說當 compA 屬性被讀取時,computedGetter 函數將會執行,在 computedGetter 函數內部,首先定義了 watcher 常量,它的值爲計算屬性 compA 的觀察者對象,緊接着若是該觀察者對象存在,則會分別執行觀察者對象的 depend 方法和 evaluate 方法。component

咱們首先找到 Watcher 類的 depend 方法,以下:

depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

depend 方法的內容很簡單,檢查 this.dep 和 Dep.target 是否所有有值,若是都有值的狀況下便會執行 this.dep.depend 方法。這裏咱們首先要知道 this.dep 屬性是什麼,實際上計算屬性的觀察者與其餘觀察者對象不一樣,不一樣之處首先會體如今建立觀察者實例對象的時候,以下是 Watcher 類的 constructor 方法中的一段代碼:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // 省略...
>   if (this.computed) {
>     this.value = undefined
>     this.dep = new Dep()
>   } else {
    this.value = this.get()
  }
}

如上高亮代碼所示,當建立計算屬性觀察者對象時,因爲第四個選項參數中 options.computed 爲真,因此計算屬性觀察者對象的 this.computed 屬性的值也會爲真,因此對於計算屬性的觀察者來說,在建立時會執行 if 條件分支內的代碼,而對於其餘觀察者對象則會執行 else 分支內的代碼。同時咱們可以看到在 else 分支內直接調用 this.get() 方法求值,而 if 分支內並無調用 this.get() 方法求值,而是定義了 this.dep 屬性,它的值是一個新建立的 Dep 實例對象。這說明計算屬性的觀察者是一個惰性求值的觀察者。

如今咱們再回到 Watcher 類的 depend 方法中:

depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

此時咱們已經知道了 this.dep 屬性是一個 Dep 實例對象,因此 this.dep.depend() 這句代碼的做用就是用來收集依賴。那麼它收集到的東西是什麼呢?這就要看 Dep.target 屬性的值是什麼了,咱們回想一下整個過程:首先渲染函數的執行會讀取計算屬性 compA 的值,從而觸發計算屬性 compA 的 get 攔截器函數,最終調用了 this.dep.depend() 方法收集依賴。這個過程當中的關鍵一步就是渲染函數的執行,咱們知道在渲染函數執行以前 Dep.target 的值必然是 渲染函數的觀察者對象。因此計算屬性觀察者對象的 this.dep 屬性中所收集的就是渲染函數的觀察者對象。

記得此時計算屬性觀察者對象的 this.dep 中所收集的是渲染函數觀察者對象,假設咱們把渲染函數觀察者對象稱爲 renderWatcher,那麼:

this.dep.subs = [renderWatcher]

這樣 computedGetter 函數中的 watcher.depend() 語句咱們就講解完了,但 computedGetter 函數還沒執行完,接下來要執行的是 watcher.evaluate() 語句:

sharedPropertyDefinition.get = function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    watcher.depend()
    return watcher.evaluate()
  }
}

咱們找到 Watcher 類的 evaluate 方法看看它作了哪些事情,以下:

evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}

咱們知道計算屬性的觀察者是惰性求值,因此在建立計算屬性觀察者時除了 watcher.computed 屬性爲 true 以外,watcher.dirty 屬性的值也爲 true,表明着當前觀察者對象沒有被求值,而 evaluate 方法的做用就是用來手動求值的。能夠看到在 evaluate 方法內部對 this.dirty 屬性作了真假判斷,若是爲真則調用觀察者對象的 this.get 方法求值,同時將this.dirty 屬性重置爲 false。最後將求得的值返回:return this.value。

這段代碼的關鍵在於求值的這句代碼,以下高亮部分所示:

evaluate () {
  if (this.dirty) {
>     this.value = this.get()
    this.dirty = false
  }
  return this.value
}

咱們在計算屬性的初始化一節中講過了,在建立計算屬性觀察者對象時傳遞給 Watcher 類的第二個參數爲 getter 常量,它的值就是開發者在定義計算屬性時的函數(或 userDef.get),以下高亮代碼所示:

function initComputed (vm: Component, computed: Object) {
  // 省略...
  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
      )
    }

    // 省略...
  }
}

因此在 evaluate 方法中求值的那句代碼最終所執行的求值函數就是用戶定義的計算屬性的 get 函數。舉個例子,假設咱們這樣定義計算屬性:

computed: {
  compA () {
    return this.a +1
  }
}

那麼對於計算屬性 compA 來說,執行其計算屬性觀察者對象的 wather.evaluate 方法求值時,本質上就是執行以下函數進行求值:

compA () {
  return this.a +1
}

你們想想這個函數的執行會發生什麼事情?咱們知道數據對象的 a 屬性是響應式的,因此如上函數的執行將會觸發屬性 a 的 get 攔截器函數。因此這會致使屬性 a 將會收集到一個依賴,這個依賴實際上就是計算屬性的觀察者對象。

如今思路大概明朗了,若是計算屬性 compA 依賴了數據對象的 a 屬性,那麼屬性 a 將收集計算屬性 compA 的 計算屬性觀察者對象,而 計算屬性觀察者對象 將收集 渲染函數觀察者對象,整個路線是這樣的:

假如此時咱們修改響應式屬性 a 的值,那麼將觸發屬性 a 所收集的全部依賴,這其中包括計算屬性的觀察者。咱們知道觸發某個響應式屬性的依賴實際上就是執行該屬性所收集到的全部觀察者的 update 方法,如今咱們就找到 Watcher 類的 update 方法,以下:

update () {
  /* istanbul ignore else */
  if (this.computed) {
    // A computed property watcher has two modes: lazy and activated.
    // It initializes as lazy by default, and only becomes activated when
    // it is depended on by at least one subscriber, which is typically
    // another computed property or a component's render function.
    if (this.dep.subs.length === 0) {
      // In lazy mode, we don't want to perform computations until necessary,
      // so we simply mark the watcher as dirty. The actual computation is
      // performed just-in-time in this.evaluate() when the computed property
      // is accessed.
      this.dirty = true
    } else {
      // In activated mode, we want to proactively perform the computation
      // but only notify our subscribers when the value has indeed changed.
      this.getAndInvoke(() => {
        this.dep.notify()
      })
    }
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

如上高亮代碼所示,因爲響應式數據收集到了計算屬性觀察者對象,因此當計算屬性觀察者對象的 update 方法被執行時,如上 if 語句塊的代碼將被執行,由於 this.computed 屬性爲真。接着檢查了 this.dep.subs.length === 0 的真假,咱們知道既然是計算屬性的觀察者,那麼 this.dep 中將收集渲染函數做爲依賴(或其餘觀察該計算屬性變化的觀察者對象做爲依賴),因此當依賴的數量不爲 0 時,在 else 語句塊內會調用 this.dep.notify() 方法繼續觸發響應,這會致使 this.dep.subs 屬性中收集到的全部觀察者對象的更新,若是此時 this.dep.subs 中包含渲染函數的觀察者,那麼這就會致使從新渲染,最終完成視圖的更新。

以上就是計算屬性的實現思路,本質上計算屬性觀察者對象就是一個橋樑,它搭建在響應式數據與渲染函數觀察者中間,另外你們注意上面的代碼中並不是直接調用 this.dep.notify() 方法觸發響應,而是將這個方法做爲 this.getAndInvoke 方法的回調去執行的,爲何這麼作呢?那是由於 this.getAndInvoke 方法會從新求值並對比新舊值是否相同,若是知足相同條件則不會觸發響應,只有當值確實變化時纔會觸發響應,這就是文檔中的描述,如今你明白了吧:

經過以上的分析,咱們知道計算屬性本質上就是一個 computed watcher,也瞭解了它的建立過程和被訪問觸發 getter 以及依賴更新的過程,其實這是最新的計算屬性的實現,之因此這麼設計是由於 Vue 想確保不只僅是計算屬性依賴的值發生變化,而是當計算屬性最終計算的值發生變化纔會觸發渲染 watcher 從新渲染,本質上是一種優化。

相關文章
相關標籤/搜索