vue源碼分析之計算屬性

最近總被問道vue的計算屬性原理是什麼、計算屬性是如何作依賴收集的之類的問題,今天用了一天時間好好研究了下源碼,把過程基本捋順了。總的來講仍是比較簡單。vue

先明確一下咱們須要弄清楚的知識點:node

  1. computed屬性如何初始化
  2. 響應式屬性的變化如何引發computed的從新計算

弄清楚以上兩點後對computed就會有一個比較全面的瞭解了。react

首先,須要弄明白響應式屬性是怎麼實現的,具體我會在其餘文章中寫,這裏瞭解個大概就能夠。在代碼中調用new Vue()的過程實際調用了定義在原型的_init(),在這個方法裏會初始化vue的不少屬性,這其中就包括創建響應式屬性。它會循環定義在data中的全部屬性值,經過Object.defineProperty設置每一個屬性的訪問器屬性。express

code

所以在這個階段,data中的屬性值在獲取或者賦值時就能被攔截。緊接着就是初始化computed屬性:數組

code2

這裏要給當前頁面實例上新增一個computedWatchers空對象,而後循環computed上的屬性。在vue的文檔裏關於computed介紹,它既能夠是函數,也但是是對象,好比下面這種:dom

new Vue({
    computed:{
        amount(){
            return this.price * this.count
        }
    }
    // 也能夠寫成下面這種
    computed:{
        amount:{
            get(){
                return this.price * this.count
            },
            set(){}
        }
    }
})

但由於不建議給computed屬性賦值,所以比較常見的都是上面那種。因此在上圖的源碼中,userDefgetter都是函數。以後就是判斷是不是服務端渲染,不是就實例化一個Watcher類。那接着來看一下實例化的這個類是什麼。源碼太長了我就只展現constructor裏的內容。異步

constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm)
      }
    }
    this.value = this.lazy ? undefined : this.get()
  }

在這個階段作了這麼幾件事情:async

  1. 向頁面實例的watchers屬性中依次push了每個計算屬性的實例。
  2. 將實例化類時傳入的第二個參數(也就是上文說起的getter)設置爲this.getter
  3. this.value設置爲undefined

到這裏爲止,計算屬性的初始化就完成了,若是給生命週期打了斷點,你就會發現這些步驟就是在created以前完成的。可是到如今,vue只是建立了響應式屬性和把每個計算屬性用watcher實例化,並無完成計算屬性的依賴收集。ide

緊接着,vue會調用原型上的$mount方法,這裏會返回一個函數mountComponent函數

code3

這裏關注一下這部分代碼:

// we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */
  )

在掛載階段,會再次實例化一次Watcher類,可是這裏和以前實例的類不同的地方在於,他的初始化屬性isRenderWatcher爲true。因此區分一下就是,前文所述的循環計算屬性時實例化的WatchercomputedWatcher,而這裏的則是renderWatcher。除了從字面上能看出他們之間的區別外。在實例化上也有不一樣。

// 不一樣一
if (isRenderWatcher) {
    vm._watcher = this
}
// 不一樣二
 this.dirty = this.lazy // for lazy watchers
// 不一樣三
this.value = this.lazy ? undefined : this.get()

renderWatcher會在頁面實例上新增一個_watcher屬性,而且dirty爲false,最重要的是這裏會直接調用實例上的方法get()

code0

這塊代碼就比較重要了,咱們一點一點說。

code01

首先是pushTarget(this)pushTarget方法是定義在Dep文件裏的方法,他的做用是往Dep類的自有屬性target上賦值,而且往Dep模塊的targetStack數組push當前的Watcher實例。所以對於此時的renderWatcher而言,它的實例被賦值給了Dep類上的屬性。

接下來就是調用當前renderWatcher實例的getter方法,也就是上面代碼中提到的updateComponent方法。

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

這裏涉及到虛擬dom的部分,我不在這裏詳說,之後會再分析。所以如今對於頁面來講,就是將vue中定義的全部data,props,methods,computed等掛載在頁面上。爲了頁面正常顯示,固然是須要獲取值的,上文中所說的爲data的每一個屬性設置getter訪問器屬性,這裏就能用到。再看下getter的代碼:

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
}

Dep.target上如今是有值的,就是renderWatcher實例,dep.depend就能被順利調用。來看下dep.depend的代碼:

depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

這裏調用了renderWatcher實例上的addDep方法:

/**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

代碼看起來可能不是很清晰,實際上這裏作了三件事:

  1. 若是該renderWatcher實例的newDepIds屬性不存在當前正在處理的data屬性的id,則添加
  2. 將當前data屬性的Dep實例添加到renderWatchernewDeps屬性中
  3. 調用當前data屬性的Dep實例上的方法dep.addSub
//  添加訂閱
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

因此第三步就是在作依賴收集的工做。對於這裏,就是爲每個響應式屬性添加了updateComponent依賴,這樣修改響應式屬性的值就可以引發頁面的從新渲染,也就是vnodepatch過程。

相應的,computed屬性也會被渲染在頁面上而被調用,和data屬性的原理同樣,computed也有訪問器屬性的設置,在第二張圖中,調到的defineComputed方法:

export function defineComputed(target: any, key: string, userDef: Object | Function) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get ? (shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get)) : noop
    sharedPropertyDefinition.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)
}

sharedPropertyDefinition是一個通用的訪問器對象:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

所以當調用計算屬性的時候,就是在調用計算屬性上綁定的函數。這裏在給get賦值時調用了另外一個函數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
    }
  }
}

這部分代碼作的事情就頗有意思了,和renderWatcher調用get作的相似,watcher.evaluate方法會間接調用computedWatcherget方法,而後調用計算屬性上的函數,由於計算屬性會根據不一樣的響應式屬性而返回值,調用每個響應式屬性都會觸發getter,所以和計算屬性相關的響應式屬性的Dep實例上會訂閱計算屬性的變化。

說到這,計算屬性的依賴收集就作完了。在這以後若是修改了某一個和計算屬性綁定的響應式屬性,就會觸發setter

set: function reactiveSetter(newVal) {
      // 獲取舊屬性值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      // 用於沒有setter的訪問器屬性
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify() // 注意這裏
    }

這裏會調用dep.notify

// 通知
  notify() {
    // stabilize the subscriber list first
    // 淺拷貝訂閱列表
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order

      // 關閉異步,則subs不在調度中排序
      // 爲了保證他們能正確的執行,如今就帶他們進行排序
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    debugger
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

對於計算屬性,會重複上面的邏輯,直到新的頁面渲染完成。

相關文章
相關標籤/搜索