Vue中computed的本質—lazy Watch

兩個月前我曾在掘金翻譯了一篇關於Vue中簡單介紹computed是如何工做的文章,翻譯的很通常因此我就不貼地址了。有位我很是敬佩的前輩對文章作了評價,內容就是本文的標題「感受原文並無講清楚 computed 實現的本質- lazy watcher」。上週末正好研究一下Vue的源碼,特地看了computed,把本身看的成果和你們分享出來。javascript

Tips:若是你以前沒有看過Vue的源碼或者不太瞭解Vue數據綁定的原理的話,推薦你看我以前的一篇文章簡單易懂的Vue數據綁定源碼解讀,或者其餘論壇博客相關的文章均可以(這種文章網上很是多)。由於要看懂這篇文章,是須要這個知識點的。html

一. initComputed 

首先,先假設傳入這樣的一組computedvue

//先假設有兩個data: data_one 和 data_two
computed:{
    isComputed:function(){
        return this.data_one + 1;
    },
    isMethods:function(){
        return this.data_two + this.data_one;
    }
}
複製代碼

咱們知道,在new Vue()的時候會作一系列初始化的操做,Vue中的data,props,methods,computed都是在這裏初始化的:java

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) //初始化props
  if (opts.methods) initMethods(vm, opts.methods) //初始化methods
  if (opts.data) {
    initData(vm) //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) //初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) //初始化initWatch
  }
}
複製代碼

我在數據綁定的那邊文章裏,詳細介紹了initData()這個函數,而這篇文章,我則重點深刻initComputed()這個函數。緩存

const computedWatcherOptions = { lazy: true } //用於傳入Watcher實例的一個對象

function initComputed (vm: Component, computed: Object) {
  //聲明一個watchers,同時掛載到Vue實例上
  const watchers = vm._computedWatchers = Object.create(null)
  //是不是服務器渲染
  const isSSR = isServerRendering()

  //遍歷傳入的computed
  for (const key in computed) {
    //userDef是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
      )
    }
    
    //若是不是服務端渲染的,就建立一個Watcher實例
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      //若是computed中的key沒有在vm中,經過defineComputed掛載上去
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      //後面都是警告computed中的key重名的
      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)
      }
    }
  }
}
複製代碼

initComputed以前,咱們看到聲明瞭一個computedWatcherOptions的對象,這個對象是實現"lazy Watcher"的關鍵。bash

接下來看initComputed,它先聲明瞭一個名爲watchers的空對象,同時在vm上也掛載了這個空對象。以後遍歷計算屬性,並把每一個屬性的方法賦給userDef,若是userDef是function的話就賦給getter,接着判斷是不是服務端渲染,若是不是的話就建立一個Watcher實例。Watcher實例我也在上一篇文章分析過,就不逐行分析了,不過須要注意的是,這裏新建的實例中咱們傳入了第四個參數,也就是computedWatcherOptions,這時,Watcher中的邏輯就有變化了:服務器

//這段代碼在Watcher類中,文件路徑爲vue/src/core/observer/watcher.js
if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
 } else {
    this.deep = this.user = this.lazy = this.sync = false
 }
複製代碼

這裏的options指的就是computedWatcherOptions,當咱們走initData的邏輯的時候,options並不存在,因此this.lazy = false,但當咱們有了computedWatcherOptions後,this.lazy = true。同時,後面還有這樣一段代碼:this.dirty = this.lazydirty的值也爲true了。ide

this.value = this.lazy
      ? undefined
      : this.get()
複製代碼

這段代碼咱們能夠知道,當lazyfalse時,返回的是undefined而不是this.get()方法。也就是說,並不會執行computed中的兩個方法:(請看我開頭寫的computed示例)函數

function(){
  return this.data_one + 1;
}
function(){
  return this.data_two + this.data_one;
}
複製代碼

這也就意味着,computed的值還並無更新。而這個邏輯也就暫時先告一段落。oop

二. defineProperty

讓咱們再回到initComputed函數中來:

if (!(key in vm)) {
   //若是computed中的key沒有在vm中,經過defineComputed掛載上去
   defineComputed(vm, key, userDef)
} 複製代碼

能夠看到,當key值沒有掛載到vm上時,執行defineComputed函數:

//一個用來組裝defineProperty的對象
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
  //是不是服務端渲染,注意這個變量名 => shouldCache
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    //若是userDef是function,給sharedPropertyDefinition.get也就是當前key的getter
    //賦上createComputedGetter(key)
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    //不然就使用userDef.get和userDef.set賦值
    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
      )
    }
  }
  //最後,咱們把這個key掛載到vm上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼

defineComputed中,先判斷是不是服務端渲染,若是不是,說明計算屬性是須要緩存的,即shouldCache是爲true 。接下來,判斷userDef是不是函數,若是是就說明是咱們常規computed的用法,將getter設爲createComputedGetter(key)的返回值。若是不是函數,說明這個計算屬性是咱們自定義的,須要使用userDef.getuserDef.set來爲gettersetter賦值了,這個else部分我就不詳細說了,不會到自定義computed的朋友能夠看文檔計算屬性的setter。最後,將computed的這個key掛載到vm上,當你訪問這個計算屬性時就會調用getter。

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
    }
  }
}複製代碼

最後咱們來看createComputedGetter這個函數,他返回了一個函數computedGetter,此時若是watcher存在的狀況下,判斷watcher.dirty是否存在,根據前面的分析,第一次新建Watcher實例的時候this.dirty是爲true的,此時調用watcher.evaluate()

function evaluate () {
    this.value = this.get()
    this.dirty = false
}複製代碼

this.get()實際上就是執行計算屬性的方法。以後將this.dirty設爲false。另外,當咱們執行this.get()時是會爲Dep.target賦值的,因此還會執行watcher.depend(),將計算屬性的watcher添加到依賴中去。最後返回watcher.value,終於,咱們獲取到了計算屬性的值,完成了computed的初始化。

三. 計算屬性的緩存——lazy Watcher

不過,此時咱們還並無解決本文的重點,也就是"lazy watcher"。還記得Vue官方文檔是這樣形容computed的:

咱們能夠將同一函數定義爲一個方法而不是一個計算屬性。兩種方式的最終結果確實是徹底相同的。然而,不一樣的是 計算屬性是基於它們的依賴進行緩存的。計算屬性只有在它的相關依賴發生改變時纔會從新求值。這就意味着只要 message 尚未發生改變,屢次訪問 reversedMessage 計算屬性會當即返回以前的計算結果,而沒必要再次執行函數。

回顧以前的代碼,咱們發現只要不更新計算屬性的中data屬性的值,在第一次獲取值後,watch.lazy始終爲false,也就永遠不會執行watcher.evaluate(),因此這個計算屬性永遠不會從新求值,一直使用上一次得到(也就是所謂的緩存)的值。

一旦data屬性的值發生變化,根據咱們知道會觸發update()致使頁面從新渲染(這部份內容有點跳,不清楚的朋友必定先弄懂data數據綁定的原理),從新initComputed,那麼this.dirty = this.lazy = true,計算屬性就會從新取值。

OK,關於computed的原理部分我就說完了,不過這篇文章仍是留了個坑,在createComputedGetter函數中有這樣一行代碼:

const watcher = this._computedWatchers && this._computedWatchers[key]複製代碼

根據上下文咱們能夠推測出this._computedWatchers中確定保存着initComputed時建立的watcher實例,但何時把這個實例放到this._computedWatchers中的呢?我尚未找到,若是有知道的朋友請留言分享,你們一塊兒討論,很是感謝!

相關文章
相關標籤/搜索