3、vue依賴收集

image

Vue 會把普通對象變成響應式對象,響應式對象 getter 相關的邏輯就是作依賴收集,這一節咱們來詳細分析這個過程vue

Dep

Dep 是整個 getter 依賴收集的核心,它的定義在 src/core/observer/dep.js 中node

import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
// 一個靜態屬性 target,這是一個全局惟一 Watcher
// 同一時間只能有一個全局的 Watcher 被計算
  static target: ?Watcher;
  id: number;
  // 自身屬性 subs 也是 Watcher 的數組
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

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

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
使用 Object.defineProperty 函數定義訪問器屬性
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    // 省略...
  },
  set: function reactiveSetter (newVal) {
    // 省略...
})

當執行完以上代碼實際上 defineReactive 函數就執行完畢了,對於訪問器屬性的 get 和 set 函數是不會執行的,由於此時沒有觸發屬性的讀取和設置操做。react

當屬性被讀取的時候都作了哪些事情,get 函數

get 函數作了兩件事:正確地返回屬性值以及收集依賴算法

get: function reactiveGetter () {
// 正確地返回屬性值,有getter就使用,沒有放val
  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 是否存在,那麼 Dep.target 是什麼呢?其實 Dep.target 與咱們在 數據響應系統基本思路 一節中所講的 Target 做用相同,因此 Dep.target 中保存的值就是要被收集的依賴(觀察者)。因此若是 Dep.target 存在的話說明有依賴須要被收集,這個時候才須要執行 if 語句塊內的代碼,若是 Dep.target 不存在就意味着沒有須要被收集的依賴,因此固然就不須要執行 if 語句塊內的代碼了。express

在 if 語句塊內第一句執行的代碼就是:dep.depend(),執行 dep 對象的 depend 方法將依賴收集到 dep 中,這裏的 dep 對象就是屬性的 getter/setter 經過閉包引用的「筐」。數組

接着又判斷了 childOb 是否存在,若是存在那麼就執行 childOb.dep.depend(),這段代碼是什麼意思呢?要想搞清楚這段代碼的做用,你須要知道 childOb 是什麼,前面咱們分析過,假設有以下數據對象:閉包

const data = {
  a: {
    b: 1
  }
}
該數據對象通過觀測處理以後,將被添加 __ob__ 屬性,以下:

const data = {
  a: {
    b: 1,
    __ob__: {value, dep, vmCount}
  },
  __ob__: {value, dep, vmCount}
}

對於屬性 a 來說,訪問器屬性 a 的 setter/getter 經過閉包引用了一個 Dep 實例對象,即屬性 a 用來收集依賴的「筐」。除此以外訪問器屬性 a 的 setter/getter 還經過閉包引用着 childOb,且 childOb === data.a.__ob__ 因此 childOb.dep === data.a.__ob__.dep。也就是說 childOb.dep.depend() 這句話的執行說明除了要將依賴收集到屬性 a 本身的「筐」裏以外,還要將一樣的依賴收集到 data.a.__ob__.dep 這裏」筐「裏,爲何要將一樣的依賴分別收集到這兩個不一樣的」筐「裏呢?其實答案就在於這兩個」筐「裏收集的依賴的觸發時機是不一樣的,即做用不一樣,兩個」筐「以下:異步

  • 第一個」筐「是 dep
  • 第二個」筐「是 childOb.dep

第一個」筐「裏收集的依賴的觸發時機是當屬性值被修改時觸發,即在 set 函數中觸發:dep.notify()。而第二個」筐「裏收集的依賴的觸發時機是在使用 $set 或 Vue.set 給數據對象添加新屬性時觸發,咱們知道因爲 js 語言的限制,在沒有 Proxy 以前 Vue 沒辦法攔截到給對象添加屬性的操做。因此 Vue 才提供了 $set 和 Vue.set 等方法讓咱們有能力給對象添加新屬性的同時觸發依賴,那麼觸發依賴是怎麼作到的呢?就是經過數據對象的 ob 屬性作到的。由於 ob.dep 這個」筐「裏收集了與 dep 這個」筐「一樣的依賴。假設 Vue.set 函數代碼以下:函數

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify()
}

當咱們使用上面的代碼給 data.a 對象添加新的屬性:工具

Vue.set(data.a, 'c', 1)

上面的代碼之因此可以觸發依賴,就是由於 Vue.set 函數中觸發了收集在 data.a.__ob__.dep 這個」筐「中的依賴:

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify() // 至關於 data.a.__ob__.dep.notify()
}

Vue.set(data.a, 'c', 1)

因此 ob 屬性以及 ob.dep 的主要做用是爲了添加、刪除屬性時有能力觸發依賴,而這就是 Vue.set 或 Vue.delete 的原理

在 childOb.dep.depend() 這句話的下面還有一個 if 條件語句,以下:

if (Array.isArray(value)) {
  dependArray(value)
}

若是讀取的屬性值是數組,那麼須要調用 dependArray 函數逐個觸發數組每一個元素的依賴收集,爲何這麼作呢?那是由於 Observer 類在定義響應式屬性時對於純對象和數組的處理方式是不一樣,對於上面這段 if 語句的目的等到咱們講解到對於數組的處理時,會詳細說明。

渲染函數的觀察者 render watcher

不管是完整版 Vue 的 $mount 函數仍是運行時版 Vue 的 $mount 函數,他們最終都將經過 mountComponent 函數去真正的掛載組件,接下來咱們就看一看在 mountComponent 函數中發生了什麼,打開 src/core/instance/lifecycle.js 文件找到 mountComponent 以下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 省略...
}

繼續查看 mountComponent 函數的代碼,接下來是一段 if 語句塊:

if (!vm.$options.render) {
  vm.$options.render = createEmptyVNode
  if (process.env.NODE_ENV !== 'production') {
    /* istanbul ignore if */
    if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
      vm.$options.el || el) {
      warn(
        'You are using the runtime-only build of Vue where the template ' +
        'compiler is not available. Either pre-compile the templates into ' +
        'render functions, or use the compiler-included build.',
        vm
      )
    } else {
      warn(
        'Failed to mount component: template or render function not defined.',
        vm
      )
    }
  }
}

這段 if 條件語句塊首先檢查渲染函數是否存在,即 vm.$options.render 是否爲真,若是不爲真說明渲染函數不存在,這時將會執行 if 語句塊內的代碼,在 if 語句塊內首先將 vm.$options.render 的值設置爲 createEmptyVNode 函數,也就是說此時渲染函數的做用將僅僅渲染一個空的 vnode 對象,而後在非生產環境下會根據相應的狀況打印警告信息。

在上面這段 if 語句塊的下面,執行了 callHook 函數,觸發 beforeMount 生命週期鉤子:

callHook(vm, 'beforeMount')

在觸發 beforeMount 生命週期鉤子以後,組件將開始掛載工做,首先是以下這段代碼:

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  updateComponent = () => {
    const name = vm._name
    const id = vm._uid
    const startTag = `vue-perf-start:${id}`
    const endTag = `vue-perf-end:${id}`

    mark(startTag)
    const vnode = vm._render()
    mark(endTag)
    measure(`vue ${name} render`, startTag, endTag)

    mark(startTag)
    vm._update(vnode, hydrating)
    mark(endTag)
    measure(`vue ${name} patch`, startTag, endTag)
  }
} else {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

這段代碼的做用只有一個,即定義並初始化 updateComponent 函數,這個函數將用做建立 Watcher 實例時傳遞給 Watcher 構造函數的第二個參數,這也將是咱們第一次真正地接觸 Watcher 構造函數,不過如今咱們須要先把 updateComponent 函數搞清楚,在上面的代碼中首先定義了 updateComponent 變量,雖然是一個 if...else 語句塊,其中 if 語句塊的條件咱們已經遇到過不少次了,在知足該條件的狀況下會作一些性能統計,能夠看到在 if 語句塊中分別統計了 vm._render() 函數以及 vm._update() 函數的運行性能。也就是說不管是執行 if 語句塊仍是執行 else 語句塊,最終 updateComponent 函數的功能是不變的。

既然功能相同,咱們就直接看 else 語句塊的代碼,由於它要簡潔的多

let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  // 省略...
} else {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

能夠看到 updateComponent 是一個函數,該函數的做用是以 vm._render() 函數的返回值做爲第一個參數調用 vm._update() 函數。因爲咱們尚未講解 vm._render 函數和 vm._update 函數的做用,因此爲了讓你們更好理解,咱們能夠簡單地認爲:

  • vm._render 函數的做用是調用 vm.$options.render 函數並返回生成的虛擬節點(vnode)
  • vm._update 函數的做用是把 vm._render 函數生成的虛擬節點渲染成真正的 DOM

也就是說目前咱們能夠簡單地認爲 updateComponent 函數的做用就是:把渲染函數生成的虛擬DOM渲染成真正的DOM,其實在 vm._update 內部是經過虛擬DOM的補丁算法(patch)來完成的

再往下,咱們將遇到建立觀察者(Watcher)實例的代碼:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

由於 watcher 對錶達式的求值,觸發了數據屬性的 get 攔截器函數,從而收集到了依賴,當數據變化時可以觸發響應。在上面的代碼中 Watcher 觀察者實例將對 updateComponent 函數求值,咱們知道 updateComponent 函數的執行會間接觸發渲染函數(vm.$options.render)的執行,而渲染函數的執行則會觸發數據屬性的 get 攔截器函數,從而將依賴(觀察者)收集,當數據變化時將從新執行 updateComponent 函數,這就完成了從新渲染。同時咱們把上面代碼中實例化的觀察者對象稱爲 渲染函數的觀察者

初識 Watcher

接下來咱們就以渲染函數的觀察者對象爲例,順着脈絡瞭解 Watcher 類,Watcher 類定義在 src/core/observer/watcher.js 文件中,以下是 Watcher 類的所有內容:

export default class Watcher {
    // 實例時能夠傳遞五個參數
  constructor (
    // 組件實例對象 vm
    vm: Component,
    // 要觀察的表達式 expOrFn
    expOrFn: string | Function,
    // 當被觀察的表達式的值變化時的回調函數 cb
    cb: Function,
    // 一些傳遞給當前觀察者對象的選項 options 
    options?: ?Object,
    // 一個布爾值 isRenderWatcher 用來標識該觀察者實例是不是渲染函數的觀察者。
    isRenderWatcher?: boolean
  ) {

  }

  get () {
    // 省略...
  }

  addDep (dep: Dep) {
    // 省略...
  }

  cleanupDeps () {
    // 省略...
  }

  update () {
    // 省略...
  }

  run () {
    // 省略...
  }

  getAndInvoke (cb: Function) {
    // 省略...
  }

  evaluate () {
    // 省略...
  }

  depend () {
    // 省略...
  }

  teardown () {
    // 省略...
  }
}

以下是在 mountComponent 函數中建立渲染函數觀察者實例的代碼:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

在建立渲染函數觀察者實例對象時傳遞了所有的五個參數

  • 第一個參數 vm 很顯然就是當前組件實例對象;
  • 第二個參數 updateComponent 就是被觀察的目標,它是一個函數;
  • 第三個參數 noop 是一個空函數;
  • 第四個參數是一個包含 before 函數的對象,這個對象將做爲傳遞給該觀察者的選項;
  • 第五個參數爲 true,咱們知道這個參數標識着該觀察者實例對象是不是渲染函數的觀察者,很顯然上面的代碼是在爲渲染函數建立觀察者對象,因此第五個參數天然爲 true

這裏有幾個問題須要注意,首先被觀察的表達式是一個函數,即 updateComponent 函數,咱們知道 Watcher 的原理是經過對「被觀測目標」的求值,觸發數據屬性的 get 攔截器函數從而收集依賴,至於「被觀測目標」究竟是表達式仍是函數或者是其餘形式的內容都不重要,重要的是「被觀測目標」可否觸發數據屬性的 get 攔截器函數,很顯然函數是具有這個能力的。另一個咱們須要注意的是傳遞給 Watcher 構造函數的第三個參數 noop 是一個空函數,它什麼事情都不會作,有的同窗可能會有疑問:「不是說好了當數據變化時從新渲染嗎,如今怎麼什麼都不作了?」,實際上數據的變化不只僅會執行回調,還會從新對「被觀察目標」求值,也就是說 updateComponent 也會被調用,因此不須要經過執行回調去從新渲染。說到這裏你們或許又產生了一個疑問:「再次執行 updateComponent 函數難道不會致使再次觸發數據屬性的 get 攔截器函數致使重複收集依賴嗎?」,這是個好問題,不過不用擔憂,由於 Vue 已經實現了避免收集重複依賴的處理,咱們後面會講到的。

constructor 函數開始,看一下建立渲染函數觀察者實例對象的過程,進一步瞭解一個觀察者,以下是 constructor 函數開頭的一段代碼:

this.vm = vm
if (isRenderWatcher) {
  vm._watcher = this
}
vm._watchers.push(this)

首先將當前組件實例對象 vm 賦值給該觀察者實例的 this.vm 屬性,也就是說每個觀察者實例對象都有一個 vm 實例屬性,該屬性指明瞭這個觀察者是屬於哪個組件的。接着使用 if 條件語句判斷 isRenderWatcher 是否爲真,前面說過 isRenderWatcher 標識着是不是渲染函數的觀察者,只有在 mountComponent 函數中建立渲染函數觀察者時這個參數爲真,若是 isRenderWatcher 爲真那麼則會將當前觀察者實例賦值給 vm._watcher 屬性,也就是說組件實例的 _watcher 屬性的值引用着該組件的渲染函數觀察者。你們還記得 _watcher 屬性是在哪裏初始化的嗎?是在 initLifecycle 函數中被初始化的,其初始值爲 null。在 if 語句塊的後面將當前觀察者實例對象 push 到 vm._watchers 數組中,也就是說屬於該組件實例的觀察者都會被添加到該組件實例對象的 vm._watchers 數組中,包括渲染函數的觀察者和非渲染函數的觀察者。另外組件實例的 vm._watchers 屬性是在 initState 函數中初始化的,其初始值是一個空數組。

// 判斷是否傳遞了 options 參數
if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.computed = !!options.computed
  this.sync = !!options.sync
  this.before = options.before
} else {
  this.deep = this.user = this.computed = this.sync = false
}
  • options.deep,用來告訴當前觀察者實例對象是不是深度觀測
    咱們平時在使用 Vue 的 watch 選項或者 vm.$watch 函數去觀測某個數據時,能夠經過設置 deep 選項的值爲 true 來深度觀測該數據。

  • options.user,用來標識當前觀察者實例對象是 開發者定義的 仍是 內部定義的
    實際上不管是 Vue 的 watch 選項仍是 vm.$watch 函數,他們的實現都是經過實例化 Watcher 類完成的,等到咱們講解 Vue 的 watch 選項和 vm.$watch 的具體實現時你們會看到,除了內部定義的觀察者(如:渲染函數的觀察者、計算屬性的觀察者等)以外,全部觀察者都被認爲是開發者定義的,這時 options.user 會自動被設置爲 true。

  • options.computed,用來標識當前觀察者實例對象是不是計算屬性的觀察者
    這裏須要明確的是,計算屬性的觀察者並非指一個觀察某個計算屬性變化的觀察者,而是指 Vue 內部在實現計算屬性這個功能時爲計算屬性建立的觀察者。等到咱們講解計算屬性的實現時再詳細說明。

  • options.sync,用來告訴觀察者當數據變化時是否同步求值並執行回調
    默認狀況下當數據變化時不會同步求值並執行回調,而是將須要從新求值並執行回調的觀察者放到一個異步隊列中,當全部數據的變化結束以後統一求值並執行回調,這麼作的好處有不少,咱們後面會詳細講解。

  • options.before,能夠理解爲 Watcher 實例的鉤子,當數據變化以後,觸發更新以前,調用在建立渲染函數的觀察者實例對象時傳遞的 before 選項。

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

能夠看到當數據變化以後,觸發更新以前,若是 vm._isMounted 屬性的值爲真,則會調用 beforeUpdate 生命週期鉤子。

再往下又定義了一些實例屬性,以下:

// 定義了 this.cb 屬性,它的值爲 cb 回調函數
this.cb = cb
// 定義了 this.id 屬性,它是觀察者實例對象的惟一標識。
this.id = ++uid // uid for batching
// 定義了 this.active 屬性,它標識着該觀察者實例對象是不是激活狀態,默認值爲 true 表明激活。
this.active = true
//定義了 this.dirty 屬性,該屬性的值與 this.computed 屬性的值相同,也就是說只有計算屬性的觀察者實例對象的 this.dirty 屬性的值纔會爲真,由於計算屬性是惰性求值。
this.dirty = this.computed // for computed watchers

接着往下看代碼,以下:

// 其實它們就是傳說中用來實現避免收集重複依賴,且移除無用依賴的功能也依賴於它們

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

來到一段 if...else 語句塊:

if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
  this.getter = parsePath(expOrFn)
  if (!this.getter) {
    this.getter = function () {}
    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
    )
  }
}

這段代碼檢測了 expOrFn 的類型,若是 expOrFn 是函數,那麼直接使用 expOrFn 做爲 this.getter 屬性的值。若是 expOrFn 不是函數,那麼將 expOrFn 透傳給 parsePath 函數,並以 parsePath 函數的返回值做爲 this.getter 屬性的值。那麼 parsePath 函數作了什麼呢?parsePath 函數定義在 src/core/util/lang.js 文件,源碼以下:

const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
  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
  }
}

首先定義 segments 常量,它的值是經過字符 . 分割 path 字符串產生的數組,隨後 parsePath 函數將返回值一個函數,該函數的做用是遍歷 segments 數組循環訪問 path 指定的屬性值。這樣就觸發了數據屬性的 get 攔截器函數。但要注意 parsePath 返回的新函數將做爲 this.getter 的值,只有當 this.getter 被調用的時候,這個函數纔會執行,目的就是支持a.b.c那種多層的對象的調用。

再往下咱們來到了 constructor 函數的最後一段代碼:

if (this.computed) {
  this.value = undefined
  this.dep = new Dep()
} else {
  this.value = this.get()
}

經過這段代碼咱們能夠發現,計算屬性的觀察者和其餘觀察者實例對象的處理方式是不一樣的,對於計算屬性的觀察者咱們會在講解計算屬性時詳細說明。除計算屬性的觀察者以外的全部觀察者實例對象都將執行如上代碼的 else 分支語句,即調用 this.get() 方法。

依賴收集的過程

this.get() 是咱們遇到的第一個觀察者對象的實例方法,它的做用能夠用兩個字描述:求值。求值的目的有兩個

  • 第一個是可以觸發訪問器屬性的 get 攔截器函數
  • 第二個是可以得到被觀察目標的值

並且可以觸發訪問器屬性的 get 攔截器函數是依賴被收集的關鍵,下面咱們具體查看一下 this.get() 方法的內容:

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
}

一上來調用了 pushTarget(this) 函數,並將當前觀察者實例對象做爲參數傳遞,這裏的 pushTarget 函數來自於 src/core/observer/dep.js 文件,以下代碼所示:

export default class Dep {
  // 省略...
}

Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

當時咱們說每一個響應式數據的屬性都經過閉包引用着一個用來收集屬於自身依賴的「筐」,實際上那個「筐」就是 Dep 類的實例對象。更多關於 Dep 類的內容咱們會在合適的地方講解,如今咱們的主要目的是搞清楚 pushTarget 函數是作什麼的。在上面這段代碼中咱們能夠看到 Dep 類擁有一個靜態屬性,即 Dep.target 屬性,該屬性的初始值爲 null,其實 pushTarget 函數的做用就是用來爲 Dep.target 屬性賦值的,pushTarget 函數會將接收到的參數賦值給 Dep.target 屬性,咱們知道傳遞給 pushTarget 函數的參數就是調用該函數的觀察者對象,因此 Dep.target 保存着一個觀察者對象,其實這個觀察者對象就是即將要收集的目標。

this.get() 方法中,以下是簡化後的代碼:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 省略...
  } finally {
    // 省略...
  }
  return value
}

在調用 pushTarget 函數以後,定義了 value 變量,該變量的值爲 this.getter 函數的返回值,咱們知道觀察者對象的 this.getter 屬性是一個函數,這個函數的執行就意味着對被觀察目標的求值,並將獲得的值賦值給 value 變量,並且咱們能夠看到 this.get 方法的最後將 value 返回爲何要強調這一點呢?以下代碼所示:

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()
  }
}

這句高亮的代碼將 this.get() 方法的返回值賦值給了觀察者實例對象的 this.value 屬性。也就是說 this.value 屬性保存着被觀察目標的值。

this.get() 方法除了對被觀察目標求值以外,你們別忘了正是由於對被觀察目標的求值才得以觸發數據屬性的 get 攔截器函數,仍是以渲染函數的觀察者爲例,假設咱們有以下模板:

<div id="demo">
  <p>{{name}}</p>
</div>

這段模板被編譯將生成以下渲染函數:

// 編譯生成的渲染函數是一個匿名函數
function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "demo" } },
      [_v("\n      "+_s(name)+"\n    ")]
    )
  }
}

能夠發現渲染函數的執行會讀取數據屬性 name 的值,這將會觸發 name 屬性的 get 攔截器函數

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
}

這段代碼咱們已經很熟悉了,它是數據屬性的 get 攔截器函數,因爲渲染函數讀取了 name 屬性的值,因此 name 屬性的 get 攔截器函數將被執行,你們注意如上代碼中高亮的兩句代碼,首先判斷了 Dep.target 是否存在,若是存在則調用 dep.depend 方法收集依賴。那麼 Dep.target 是否存在呢?答案是存在,這就是爲何 pushTarget 函數要在調用 this.getter 函數以前被調用的緣由。既然 dep.depend 方法被執行,那麼咱們就找到 dep.depend 方法,以下:

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

爲了搞清楚這麼作的目的,咱們找到觀察者實例對象的 addDep 方法,以下:

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)
    }
  }
}

能夠看到 addDep 方法接收一個參數,這個參數是一個 Dep 對象,在 addDep 方法內部首先定義了常量 id,它的值是 Dep 實例對象的惟一 id 值。接着是一段 if 語句塊,該 if 語句塊的代碼很關鍵,由於它的做用就是用來 避免收集重複依賴 的,既然是用來避免收集重複的依賴,那麼就不得不用到咱們前面提到過的兩組屬性,即 newDepIds、newDeps 以及 depIds、deps。

咱們思考一下可不能夠把 addDep 方法修改爲以下這樣:

addDep (dep: Dep) {
  dep.addSub(this)
}

首先解釋一下 dep.addSub 方法,它的源碼以下:

addSub (sub: Watcher) {
  this.subs.push(sub)
}

addSub 方法接收觀察者對象做爲參數,並將接收到的觀察者添加到 Dep 實例對象的 subs 數組中,其實 addSub 方法纔是真正用來收集觀察者的方法,而且收集到的觀察者都會被添加到 subs 數組中存起來。

瞭解了 addSub 方法以後,咱們再回到以下這段代碼:

addDep (dep: Dep) {
  dep.addSub(this)
}

咱們修改了 addDep 方法,直接在 addDep 方法內調用 dep.addSub 方法,並將當前觀察者對象做爲參數傳遞。這不是很好嗎?難道有什麼問題嗎?固然有問題,假如咱們有以下模板:

<div id="demo">
  {{name}}{{name}}
</div>

這段模板的不一樣之處在於咱們使用了兩次 name 數據,那麼相應的渲染函數也將變爲以下這樣:

function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "demo" } },
      [_v("\n      "+_s(name)+_s(name)+"\n    ")]
    )
  }
}

能夠看到,渲染函數的執行將讀取兩次數據對象 name 屬性的值,這必然會觸發兩次 name 屬性的 get 攔截器函數,一樣的道理,dep.depend 也將被觸發兩次,最後致使 dep.addSub 方法被執行了兩次,且參數如出一轍,這樣就產生了同一個觀察者被收集屢次的問題。因此咱們不能像如上那樣修改 addDep 函數的代碼,那麼此時我相信你們也應該知道以下高亮代碼的含義了:

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)
    }
  }
}

在 addDep 內部並非直接調用 dep.addSub 收集觀察者,而是先根據 dep.id 屬性檢測該 Dep 實例對象是否已經存在於 newDepIds 中,若是存在那麼說明已經收集過依賴了,什麼都不會作。若是不存在纔會繼續執行 if 語句塊的代碼,同時將 dep.id 屬性和 Dep 實例對象自己分別添加到 newDepIds 和 newDeps 屬性中,這樣不管一個數據屬性被讀取了多少次,對於同一個觀察者它只會收集一次。

!this.depIds.has(id) 是什麼意思呢?

newDepIds 屬性用來避免在 一次求值 的過程當中收集重複的依賴,其實 depIds 屬性是用來在 屢次求值 中避免收集重複依賴的

什麼是屢次求值,其實所謂屢次求值是指當數據變化時從新求值的過程。你們可能會疑惑,難道從新求值的時候不能用 newDepIds 屬性來避免收集重複的依賴嗎?不能,緣由在於每一次求值以後 newDepIds 屬性都會被清空,也就是說每次從新求值的時候對於觀察者實例對象來說 newDepIds 屬性始終是全新的。雖然每次求值以後會清空 newDepIds 屬性的值,但在清空以前會把 newDepIds 屬性的值以及 newDeps 屬性的值賦值給 depIds 屬性和 deps 屬性,這樣從新求值的時候 depIds 屬性和 deps 屬性將會保存着上一次求值中 newDepIds 屬性以及 newDeps 屬性的值。爲了證實這一點,咱們來看一下觀察者對象的求值方法,即 get() 方法:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 省略...
  } finally {
    // 省略...
    popTarget()
    this.cleanupDeps()
  }
  return value
}

能夠看到在 finally 語句塊內調用了觀察者對象的 cleanupDeps 方法,這個方法的做用正如咱們前面所說的那樣,每次求值完畢後都會使用 depIds 屬性和 deps 屬性保存 newDepIds 屬性和 newDeps 屬性的值,而後再清空 newDepIds 屬性和 newDeps 屬性的值,以下是 cleanupDeps 方法的源碼:

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}

在 cleanupDeps 方法內部,首先是一個 while 循環,咱們暫且不關心這個循環的做用,咱們看循環下面的代碼,即高亮的部分,這段代碼是典型的引用類型變量交換值的過程,最終的結果就是 newDepIds 屬性和 newDeps 屬性被清空,而且在被清空以前把值分別賦給了 depIds 屬性和 deps 屬性,這兩個屬性將會用在下一次求值時避免依賴的重複收集。

如今咱們能夠作幾點總結:

  • 一、newDepIds 屬性用來在一次求值中避免收集重複的觀察者
  • 二、每次求值並收集觀察者完成以後會清空 newDepIds 和 newDeps 這兩個屬性的值,而且在被清空以前把值分別賦給了 depIds 屬性和 deps 屬性
  • 三、depIds 屬性用來避免重複求值時收集重複的觀察者

經過以上三點內容咱們能夠總結出一個結論,即 newDepIds 和 newDeps 這兩個屬性的值所存儲的老是當次求值所收集到的 Dep 實例對象,而 depIds 和 deps 這兩個屬性的值所存儲的老是上一次求值過程當中所收集到的 Dep 實例對象。

除了以上三點以外,其實 deps 屬性還可以用來移除廢棄的觀察者,cleanupDeps 方法中開頭的那段 while 循環就是用來實現這個功能的,以下代碼所示:

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // 省略...
}

這段 while 循環就是對 deps 數組進行遍歷,也就是對上一次求值所收集到的 Dep 對象進行遍歷,而後在循環內部檢查上一次求值所收集到的 Dep 實例對象是否存在於當前此次求值所收集到的 Dep 實例對象中,若是不存在則說明該 Dep 實例對象已經和該觀察者不存在依賴關係了,這時就會調用 dep.removeSub(this) 方法並以該觀察者實例對象做爲參數傳遞,從而將該觀察者對象從 Dep 實例對象中移除。

咱們能夠找到 Dep 類的 removeSub 實例方法,以下:

removeSub (sub: Watcher) {
  remove(this.subs, sub)
}

它的內容很簡單,接收一個要被移除的觀察者做爲參數,而後使用 remove 工具函數,將該觀察者從 this.subs 數組中移除。

相關文章
相關標籤/搜索