Vue:多角度剖析計算屬性的運行機制 #219

歡迎 star評論Vue:多角度剖析計算屬性的運行機制 #219vue

大綱

計算屬性的初始化過程

在建立Vue實例時調用this._init初始化。react

其中就有調用initState初始化git

export function initState (vm: Component) {
  // ...
  if (opts.computed) initComputed(vm, opts.computed)
 	// ...
}
複製代碼

initState會初始化計算屬性:調用initComputedgithub

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // ...
  for (const key in computed) {
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // ...
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // ...
    }
  }
}
複製代碼

遍歷computedtypescript

先建立計算屬性的watcher實例,留意computedWatcherOptions這個option決定了計算屬性的watcher和普通watcher的不一樣express

而後定義計算屬性的屬性的getter和setterapi

  • 再來看看watcher的建立
export default class Watcher {
  // ...

  constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) {
    // ...
    if (options) {
    	// ...
    	this.lazy = !!options.lazy
      // ...
    }
    this.dirty = this.lazy // for lazy watchers
      
    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()
  }
  // ...
}
複製代碼
  1. watcher.lazy = true;
  2. watcher.dirty = true;
  3. watcher.getter = typeof userDef === 'function' ? userDef : userDef.get
  4. 不會在構造函數內調用watcher.get()`(非計算屬性的watcher/lazy watcher會在建立watcher實例時調用)
  • 再來看計算屬性defineProperty的定義
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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
  }
  // ...
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼

shouldCache,瀏覽器渲染都是 shouldCache = true數組

那麼gtter就是由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
    }
  }
}
複製代碼

以上就是計算屬性的的初始化過程。緩存

計算屬性被訪問時的運行機制

如上,假設計算屬性當前被調用

就是觸發計算屬性的getter,再次強調:計算屬性的getter不是用戶定義的回調,而是由createComputedGetter返回的函數(詳細參考計算屬性的初始化過程的最後一段代碼)。 用戶定義的回調則是在計算屬性getter的邏輯中進行調用。

計算屬性getter中主要由兩個if控制流, 這個兩個if組合起來就可能由四種可能, 對於第二個控制流的邏輯watcher.depend,若是有看到Vue的Dep的功能的話,能夠推測這段代碼是用於收集依賴, 結合以上能夠以下推測:

序號 if (watcher.dirty) if (Dep.target) 功能
1 N N 返回舊值
2 N Y 收集依賴
3 Y N 更新計算屬性值(watcher.value)
4 Y Y 收集依賴,並更新計算屬性值(watcher.value)

目前掌握的信息有:

  1. 計算屬性的getter是核心功能就是獲取計算屬性的值,而getter返回的是watcher.value,說明計算屬性的值保存在watcher.value
  2. evaluate多是用於更新watcher.value;
  3. watcher.depend多是用於收集依賴,不清楚收集什麼;

咱們先來看第一個控制流:

// watcher.dirty = true
if (watcher.dirty) {
  watcher.evaluate()
}
複製代碼

根據計算屬性的初始化過程中建立計算屬性watcher實例時就能夠看出,第一次調用watcher.dirty確定是true

但不論watcher.dirty是否是「真」,咱們都要去看看「evaluate 」時何方神聖,並且確定會有訪問它的時候。

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

顯然,evaluate確實是用於更新計算屬性值(watcher.value)的。

另外,你能夠發如今this.value = this.get()執行完後,還執行了一句代碼:this.dirty = false

而後你會發現一個邏輯:

  1. 初始化計算屬性時,watcher.dirty = false;
  2. 執行evaluate更新後,watcher.dirty = true;
  3. watcher.dirty = true時不會去更新計算屬性的值。

一切說明計算屬性是懶加載的,在訪問時根據狀態值來判斷使用緩存數據仍是從新計算。

再者,咱們還能夠再總結一下dirty和lazy的信息:

對比普通的watcher實例建立:

構造函數中的邏輯

normal computed
this.value = this.get() this.value = undefined
this.lazy = false this.lazy = true
this.dirty = false this.dirty = true

綜上,能夠看出 lazy的意思

  • 實例化時調用get就是非lazy

  • 非實例化時調用get就是lazy

dirty(髒值)的意思

  • watcher.value仍是undefined(或者還不是當前真是)就是dirty
  • watcher.value已經存有當前計算的實際值就不是dirty

lazy屬性只是一個說明性的標誌位,主要用來代表當前watcher是惰性模式的。 而dirty則是對lazy的實現,做爲狀態爲表示當前是否是髒值狀態。

再來看看watcher.get()的調用,其內部的動做

import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  // ...
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw 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
  }
  // ...
}
複製代碼

在get()函數開頭的地方調用pushTarget函數,爲了接下來的內容,有必要先說明下pushTarget和結尾處的popTarget,根據字面意思就知道是對什麼進行入棧出棧。

你能夠看到是該方法來自於dep,具體函數實現以下:

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和popTarget操做的對象是Watcher,存放在全局變量targetStack中。每次出棧入棧都會更新Dep.target的值,而它值由上可知是targetStack的棧頂元素。

如今就知道pushTarget(this)的意思是:將當前的watcher入棧,並設置Dep.Target爲當前watcher。

而後就是執行:

value = this.getter.call(vm, vm)
複製代碼

計算屬性watcher的getter是什麼?

watcher.getter = typeof userDef === 'function' ? userDef : userDef.get
複製代碼

是用戶定義的回調函數,計算屬性的回調函數。 回顧這一節開頭的結論:

用戶定義的回調則是在計算屬性getter的邏輯中進行調用。

到此,咱們就能夠清晰知道:用戶定義的getter是在計算屬性的getter中的computedWatcher.evaluate()中的computedWatcher.value = computedWatcher.get()中調用!

調用完getter算是完事沒有呢?沒有,這裏還有一層隱藏的邏輯!

咱們知道通常計算屬性都依賴於$data的屬性,而調用計算屬性的回調函數就會訪問這些屬性,就會觸發這些屬性的getter。

這些基礎屬性的getter就是隱藏的邏輯,若是你有看過基礎屬性的數據劫持就知道他們的getter都是有收集依賴的邏輯。

這些基本屬性的getter都是在數據劫持的時候定義的,咱們去看看會發生什麼!

Object.defineProperty(obj, key, {
  // ...
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      // ...
    }
    return value
  },
  // ...
}
複製代碼

記得剛剛調用了pushTarget吧,如今Dep.target已經不爲空,而且Dep.target就是當前計算屬性的watcher。

則會執行dep.depend(),dep是每一個$data屬性關聯的(經過閉包關聯)。

dep是依賴收集器,收集watcher,用一個數組(dep.subs)存放watcher,

而執行dep.depend(),除了執行其餘邏輯,裏面還有一個關鍵邏輯就是將Dep.targetpush到當前屬性關聯的dep.subs,言外之意就是,計算屬性的訪問在條件適合的狀況下是會讓計算屬性所依賴的屬性收集它的wathcer,而這個收集操做的做用且聽下回分解。

小結

  1. 計算watcher.value:computed-watcher.evaluate(),訪問計算屬性時,若當前計算屬性是髒值狀態則調用evaluate計算計算屬性的真實值;
  2. 在計算計算屬性真實值時,合乎條件下會觸發它依賴的基礎屬性收集它的watcher。

計算屬性的更新機制

如何通知變更

計算屬性所依賴屬性的dep收集computed-watcher的意義何在呢?

假如如今更新計算屬性依賴的任一個屬性,會發生什麼?

更新依賴的屬性,固然是觸發對應屬性的setter,首先來看看基礎屬性setter的定義。

Object.defineProperty(obj, key, {
  // ...
  set: function reactiveSetter (newVal) {
    // ...
    dep.notify()
  }
})
複製代碼

首先是在setter裏面調用dep.notify(),通知變更。dep固然就是與屬性關聯的依賴收集器,notfiy必然是去通知訂閱者它們訂閱的數據之一已經發生變更。

export default class Dep {
  // ...

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
複製代碼

在notify方法裏面能夠看出,遍歷了當前收集裏面全部(訂閱者)watcher,而且調用了他們的update方法。

計算屬性被訪問時的運行機制已經知道,計算屬性的watcher是會被它所依賴屬性的dep收集的。所以,notify中的subs確定也包含了計算屬性的watcher。

因此,計算屬性所依賴屬性變更是經過調用計算屬性watcher的update方法通知計算屬性的。

接下來,在深刻去看看watcher.update是怎麼更新計算屬性的。

export default class Watcher {
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}
複製代碼

計算屬性被訪問時的運行機制中就知道,計算屬性watcher是lazy的,因此,comuptedWatcher.update的對應邏輯就是下面這一句:

this.dirty = true
複製代碼

再回想一下計算屬性被訪問時的運行機制中計算屬性getter調用evalute()的控制流邏輯(if(watcher.dirty)),這下計算屬性的訪問和他的被動更新就造成閉環!

每次變化通知都是隻更新髒值狀態,真是計算仍是訪問的時候再計算

計算屬性如何被更新

從上面咱們就知道通知計算屬性「變化」是不會直接引起計算屬性的更新!

那麼問題就來了,現實咱們看到的是:綁定的視圖上的計算屬性的值,只要它所依賴的屬性值更新,會直接響應到視圖上。

那就說明在通知完以後,當即訪問了計算屬性,引發了計算屬性值的更新,而且更新了視圖。

對於,不是綁定在視圖上的計算屬性很好理解,畢竟咱們也是在有須要的時候纔會去訪問他,至關於即時計算了(假如是髒值),所以不管是不是即時更新都無所謂,只要在訪問時能夠拿到最新的實際值就好。

可是對於視圖卻不同,要即時反映出來,因此確定是還有更新視圖這一步的,咱們如今須要作的測試找出vue是怎麼作的。

其實假如你有去看過vue數據劫持的邏輯就知道:在訪問屬性時,只要當前的Dep.target(訂閱者的引用)不爲空,與這個屬性關聯的dep就會收集這個訂閱者

這個訂閱者之一是「render-watcher」,它是視圖對應的watcher,只要在視圖上綁定了的屬性都會收集這個render-watcher,因此每一個屬性的dep.subs都有一個render-watcher。

沒錯,就是這個render-watcher完成了對計算屬性的訪問與視圖的更新。

到這裏咱們就能夠小結一下計算屬性對所依賴屬性的響應機制: 所依賴屬性更新,會通知該屬性收集的全部watcher,調用update方法,其中就包含計算屬性的watcher(computed-watcher),若是計算屬性綁定在視圖上,則還包含render-watcher,computed-watcher負責更新計算屬性的髒值狀態,render-watcher負責更新訪問計算屬性和更新視圖。

可是這裏又引出了一個問題!

假設如今計算屬性就綁定在視圖上,那麼如今計算屬性響應更新就須要兩個watcher,分別是computed-watcher和render-watcher。

你細心點就會發現,要達到預期的效果,對這兩個watcher.update()的調用順序是有要求的!

必需要先調用computed-watcher.update()更新髒值狀態,而後再調用render-watcher.update()去訪問計算屬性,纔會去從新算計算屬性的值,否者只會直接緩存的值watcher.value。

好比說有模板是

<span>{{ attr }}<span>
<span>{{ computed }}<span>
複製代碼

attr的dep.subs中的watcher順序就是

狀況1:

[render-watcher, computed-watcher]
複製代碼

反之就是

狀況2:

[computed-watcher, render-watcher]
複製代碼

咱們知道deo.notify的邏輯遍歷調用subs裏面的每一個watcher.update

假如這個遍歷的順序是按照subs數組的順序來更新的話,狀況1就會有問題

狀況1

是先觸發視圖watcher的更新,他會更新視圖上全部綁定的屬性,不論屬性有沒有更新過

然而此時computed-watcher的屬性dirty 仍是 false,這意味這着這個計算屬性不會從新計算,而是使用已有的掛在watcher.value的舊值。

若是真是如此,以後在調用computred-watcher的update也沒有意義了,除非從新調用render-watcher的update方法。

很明顯,vue不可能那麼蠢,確定會作控制更新順序的邏輯

咱們看看notify方法的邏輯:

notify (key) {
  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.sort((a, b) => a.id - b.id)
  }
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
複製代碼

你能夠看到控制流裏面確實作了順序控制

可是process.env.NODE_ENV !== 'production' && !config.async 的輸出是false呢

很直觀,在生成環境就進不了這個環境!

然而,現實表現出來的結果是,就算沒有進入這個控制流裏面,視圖仍是正確更新了

更使人驚異的是:更新的遍歷順序確實是按着[render-watcher, computed-watcher]進行的

image

你能夠看到是先遍歷了render-watcher(render-watcher的id確定是最大的,越日後建立的watcher的id越大,計算屬性是在渲染前建立,而render-watcher則是在渲染時)

可是若是你細心的話你能夠發現,render-watcher更新回調是在遍歷完全部的watcher以後才執行的(白色框)

image

咱們再來看看watcher.update的內部邏輯

update () {
  /* istanbul ignore else */
  console.log(
    'watcher.id:', this.id
  );
  if (this.lazy) {
    this.dirty = true
    console.log(`update with lazy`)
  } else if (this.sync) {
    console.log(`update with sync`)
    this.run()
  } else {
    console.log(`update with queueWatcher`)
    queueWatcher(this)
  }
  console.log(
    'update finish',
    this.lazy ? `this.dirty = ${this.dirty}` : ''
  )
}
複製代碼

根據打印的信息,能夠看到render-watcher進入了else的邏輯,調用queueWatcher(this)

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    console.log('queueWatcher:', queue)
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
複製代碼

根據函數名,能夠知道是個watcher的隊列

has是一個用於判斷待處理watcher是否存在於隊列中,而且在隊中的每一個watcher處理完都會將當前has[watcher.id] = null

flushing這個變量是一個標記:是否正在處理隊列

if (!flushing) {
  queue.push(watcher)
} else {
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}
複製代碼

以上是不一樣的將待處理watcher推入隊列的方式。

而後接下來的邏輯,纔是處理watcher隊列

waittingflushing這兩個標誌標量大體相同,他們都會在watcher隊列處理完以後重置爲false

而不一樣的是waitting在最開始就會置爲true,而flushing則是在調用flushSchedulerQueue函數的時候纔會置爲true

nextTick(flushSchedulerQueue)
複製代碼

這一句是關鍵,nextTick,能夠理解爲一個微任務,即會在主線程任務調用完畢以後纔會執行回調,

此時回調便是flushSchedulerQueue

關於nextTick能夠參考Vue:深刻nextTick的實現

這樣就能夠解析:

更使人驚異的是:更新的遍歷順序確實是按着[render-watcher, computed-watcher]進行的

可是若是你細心的話你能夠發現,render-watcher更新回調是在遍歷完全部的watcher以後才執行的(白色框)

小結

  • 經過遍歷調用dep.subs裏的watcher.update方法(其中就包含computed-watcher)來通知計算屬性基礎屬性已經更新,在下次訪問計算屬性時就是作髒值檢測,而後從新計算計算屬性。綁定在視圖上的計算屬性的即時更新是經過調用render-watcher的update方法達到,它會訪問計算屬性,並更新整個視圖。
  • 綁定在視圖上的計算屬性,它所依賴屬性的dep.subs中,computed-watcher和render-watcher的順序不會影響計算屬性在視圖上的正常更新,由於render-watcher的update方法的主體邏輯是放在微任務中執行,所以render-watcher.update()老是會在computed-watcher.update()以後執行。

計算屬性如何收集依賴

計算屬性的更新機制中咱們知道了計算屬性所依賴屬性的dep是會收集computed-watcher的,目的是爲了通知計算屬性當前依賴的屬性已經發生變化。

那麼計算屬性爲何要收集依賴?是如何收集依賴的?

「計算屬性所依賴屬性的dep具體怎麼收集computed-watcher」並無展開詳細說。如今咱們來詳細看看這部分邏輯。那就必然要從第一次訪問計算屬性開始, 第一次訪問必然會調用watcher.evaluate去算計算屬性的值,那就是必然會調用computed-watcher.get(),而後在get方法裏面去調用用戶定義的回調函數,算計算屬性的值,調用用戶定義的回調函數就必然會訪問計算屬性所依賴屬性,那就必然觸發他們的getter,沒錯咱們就是要從這裏開始看詳細的邏輯,也是從這裏開始收集依賴:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      // ...
    }
    return value
  },
  // ...
}
複製代碼

計算屬性依賴的屬性經過dep.depend()收集computed-watcher,展開dep.depend()看看詳細邏輯:

// # dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
複製代碼

很顯然如今的全局watcher就是computed-watcher,而this則是當前計算屬性所依賴屬性的dep(下面簡稱:prop-dep),繼續展開computed-watcher.addDep(prop-dep)

// # watcher.js
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)
    }
  }
}
複製代碼

在dep收集watcher的以前(dep.addSub(this)),watcher也在收集dep。

`this.newDeps.push(dep)`
複製代碼

watcher收集dep就是接下來咱們要說的點之一!

另外,上面的代碼中還包含了以前沒見過的三個變量this.newDepIdsthis.newDepsthis.depIds

先看看他們的聲明:

export default class Watcher {
  // ...
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
	// ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // ...
  }
複製代碼

depIdsnewDepIds都是Set的數據結構,結合if (!this.newDepIds.has(id))!this.depIds.has(id)就能夠推斷他們的功能是防止重複操做的。

到此,咱們知道了計算屬性是如何收集依賴的!而且,從上面知道了所收集的依賴是不重複的。

可是,到這裏尚未結束!

這個newDeps並非最終存放存放點,真實的dep存放點是deps,在上面聲明你就能夠看見它。

在調用computed-watcher.get()的過程當中還有一個比較關鍵的方法沒有給出:

get () {
  // ...
  // 在最後調用
  this.cleanupDeps()
}
複製代碼

形如其名,就是用來清除dep的,清除newDeps,而且轉移newDeps到Deps上。

cleanupDeps () {
  let i = this.deps.length
  // 遍歷deps,對比newDeps,看看哪些dep已經沒有被當前的watcher收集
  // 若是沒有,一樣也解除dep對當前watcher的收集
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // 轉存newDepIds到depIds
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  
  // 轉存newDeps到Deps
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}
複製代碼

下面是執行完computed-watcher.get()後的打印信息:

從上面的分析咱們能夠知道:計算屬性的watcher會在計算值(watcher.evalute())時,收集每一個它依賴屬性的dep,並最後存放在watcher.deps

接下來再來探究計算屬性爲何要收集依賴

還記得計算屬性的getter中的另外一個控制流,一直沒有展開細說。

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

從這段代碼能夠知道,只有全局watcher(Dep.target)不爲空,纔會執行watcher.depend(),這就是要想的第一個問題:什麼狀況下全局watcher是不爲空?

首先來確認下全局watcher的update機制:

  • pushTarget和popTarget是成對出現的;
  • 只有在watcher.get方法中才會入棧非空的watcher;
  • 在執行watcher.get的開頭pushTarget(this),在結尾popTarget(),意味着在get方法調用完成後,全局watcher就變回調用get方法前的全局watcher。

還記得computed的getter的邏輯吧!

if (watcher.dirty) {
  watcher.evaluate()
}
if (Dep.target) {
  watcher.depend()
}
複製代碼

在髒值狀態下會執行watcher.evaluate(),執行完已經完成watcher.get()的調用,因此watcher.evaluate不會影響到下面的if (Dep.target)判斷。

pushTarget和popTarget是成對出現的,顯然只有在調用完pushTarget後,且未調用popTarget這個時間段內調用計算屬性纔會執行watcher.depend()。另外,只有watcher.get()纔會入棧非空的watcher,因此咱們就能夠再次縮小範圍到:在調用watcher.get()的過程當中訪問了計算屬性

記得在計算屬性被訪問時的運行機制中有用表格對比過新建普通watcher和計算屬性watcher實例的異同,其中普通watcher的建立就會在實例化的時候調用this.get()

此刻讓我想到了render-watcher,它就是一個普通的watcher,並且render-watcher是會訪問綁定在視圖上的所用屬性,並且它訪問視圖上屬性的過程就是在get方法裏面的getter的調用中。

get () {
  // 那麼全局watcer就是render-watcher了
  pushTarget(this)
  // ...
  try {
    // 視圖上的全部屬性都在getter方法被訪問,包括計算屬性
    value = this.getter.call(vm, vm)
  } catch (e) {
    // ...
  } finally {
    // ...
    popTarget()
    this.cleanupDeps()
  }
  return value
}
複製代碼

接下展開watcher.depend看看:

depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
複製代碼

已經很明瞭,上面已經說過this.deps是計算屬性收集的dep(它所依賴的dep),而後如今遍歷deps,調用dep.depend(),上面也一樣已經說過dep.depend()的功能是收集全局watcher。

因此,watcher.depend()的功能就是讓計算屬性收集的deps去收集當前的全局watcher。 而如今的全局watcher就是render-watcher!

如今咱們知道watcher.depend的功能是讓prop-dep去收集全局watcher,可是爲何要這麼作? 不放將問題細化到render-watcher的場景上。爲何prop-watcher要去收集render-watcher?

首先,我要再次強調:一個綁定在視圖上的計算屬性要即時響應所依賴屬性的更新,那麼這些依賴屬性的dep.subs就必須包含computed-watcherrender-watcher,前者是用來更新計算屬性的髒值狀態,後者用來訪問計算屬性,讓計算屬性從新計算。並更新視圖。

計算屬性所依賴屬性的dep.subs中確定會包含computed-watcher,這一點不須要質疑,上面已經證實分析過!

可是,是否會包含render-watcher就不必定了!首先上面也有間接地提過,綁定在視圖上的屬性,它的dep會收集到render-watcher。那麼,計算屬性所依賴的屬性,有可能存在一些是沒有綁定在視圖上,而是直接定義在data上而已,對於這些屬性,它的dep.subs是確定沒有render-watcher的了。沒有render-watcher意味着沒有更新視圖的能力。那麼怎麼辦?那固然就是去保證它!

watcher.depend()就起到了這個做用!它讓計算屬性所依賴的屬性

對於這個推測

綁定在視圖上的屬性,它的dep會收集到render-watcher

咱們能夠探討一下。

要一個vue.$data屬性的dep去收集dep.subs沒有的watcher須要具有兩個條件:

  • 訪問這個屬性;
  • 全局watcher(Dep.target)不爲空;

而沒有綁定在視圖上的屬性,在render-watcher.get()調用的過程當中就沒有訪問,沒有訪問就不會調用dep.depend()去收集render-watcher!

可能有人會問,在訪問計算屬性的時候不是有調用用戶定義的回調嗎?不就訪問了這些依賴的屬性?

是!確實是訪問了,那個時候的Dep.target是computed-watcher。

ok,render-watcher這個場景也差很少了。咱們該抽離表象看本質!

首先想一想屬性dep爲何要收集依賴(訂閱者),由於有函數依賴了這個屬性,但願這個屬性在更新的時候通知訂閱者。能夠以此類比一下計算屬性,計算屬性的deps爲何須要收集依賴(訂閱者),是否是也是由於有函數依賴了計算屬性,但願計算屬性在更新時通知訂閱者,在想深一層:怎麼樣纔算是計算屬性更新?不就是它所依賴的屬性發生變更嗎?計算屬性所依賴屬性更新 = 計算屬性更新,計算屬性更新就要通知依賴他的訂閱者!再想一想,計算屬性所依賴屬性更新就能夠直接通知依賴計算屬性的訂閱者了,那麼計算屬性所依賴屬性的dep直接收集依賴計算屬性的訂閱者就行了!這不就是watcher.depend()在作的事情嗎?!

本質咱們知道了,可是怎麼才能夠實現依賴計算屬性!

首先全局watcher不爲空! 怎麼纔會讓Dep.target不爲空!只有一個方法:調用watcher.get(),在vue裏面只有這個方法會入棧非空的watcher,另外咱們知道pushTarget和popTarget是成對出現的,即要在未調用popTarget前訪問計算屬性,怎麼訪問呢?pushTarget和popTarget分別在get方法的一頭一尾,中間能夠用戶定義的只有一個地方!

get () {
  pushTarget(this)
  // ...
  value = this.getter.call(vm, vm)
  // ...
  popTarget()
}
複製代碼

就是getter,getter是能夠由用戶定義的~

再來getter具體存儲的是什麼

export default class Watcher {
  // ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        // ...
      }
    }
    // ...
  }
複製代碼

由上能夠知道,一個有效的getter是有expOrFn決定,expOrFn若是是Function則getter就是用戶傳入的函數!若是是String則由parsePath進行構造:

// 返回一個訪問vm屬性(包含計算屬性)的函數
export function parsePath (path: string): any {
  // 判斷是不是一個有效的訪問vm屬性的路徑
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
複製代碼

由上可知,咱們有兩種手段可讓getter訪問計算屬性: 而且在此我不作說明,直接說結論,watch一個屬性(包含計算屬性),包括使用$watch都是會建立一個watcher實例的,並且是普通的watcher,即會在構造函數直接調用watcher.get()

  • 直接watch計算屬性
const vm = new Vue({
  data: {
    name: 'isaac'
  },
  computed: {
    msg() { return this.name; }
  }
  watch: {
    msg(val) {
      console.log(`this is computed property ${val}`);
    }
  }
}).$mount('#app');
複製代碼

這種方法就是在建立實例時傳進了一個路徑,這個路徑就是msg,即expOrFn是String,而後由parsePath構造getter,從而訪問到計算屬性。

vm.$watch(function() {
  return this.msg;
}, function(val) {
  console.log(`this is computed property ${val}`);
});
複製代碼

這種方法直接就傳入一個函數,即expOrFn是Function,就是$watch的第一個參數!一樣在getter中訪問了計算屬性。

上面兩種都是在getter中訪問了計算屬性,從而讓deps收集訂閱者,計算屬性的變更(固然並不是真的更新了值,只是進入髒值狀態)就會通知依賴他的訂閱者,調用watcher.update(),若是沒有傳入什麼特殊的參數,就會調用watch的回調函數,若是在回調函數中有訪問計算屬性就會從新計算計算屬性,更新狀態爲非髒值!

小結

  • 計算屬性所依賴的屬性的dep會收集computed-watcher,存放在prop-dep.subs中;
  • computed-watcher也會收集它所依賴的dep,存放在computed-watcher.deps中,爲了確保計算屬性得到通知依賴他的訂閱者能夠監聽到他的變化,經過watcher.depend()來收集依賴它的訂閱者。

總結

  • 計算屬性在initState階段初始化;
  • 計算屬性也是會使用defineProperty進行計算屬性劫持;
  • 每一個計算屬性都會關聯一個特殊的watcher(lazy)。存放在一個對象中,以計算屬性的名字做爲鍵值,掛載在vm.computedWatchers
  • 經過讓計算屬性所依賴屬性的dep收集計算屬性watcher的行爲實現「依賴屬性的變更通知計算屬性」;
  • 計算屬性的watcher是lazy的,不會在建立實例時計算自身的值(即不會調用watcher.get());
  • 計算屬性是lazy的,調用計算屬性的watcher.update不會直接計算值,只是更新標誌位(this.dirty = true),直到計算屬性被訪問纔會計算值;
  • dep(依賴收集器)會收集watcher(訂閱者),watcher也會收集dep;
  • 計算屬性經過watcher.value對其值進行緩存,不會每次訪問都重新計算;
  • 計算屬性經過watcher.depend()來收集依賴它的訂閱者。
相關文章
相關標籤/搜索