SSShooter

SSShooter

Write like you're running out of time.

Vue 響應式原理解析

幾年來看了不少 Vue 原理的文章,在這些文章的幫助下,我也多次嘗試自己理解 Vue 的源碼,終於,我覺得是時候自己輸出一下內容了,希望可以從不同於其他文章的角度帶大家熟悉 Vue。

這個專題自然是分多個部分講解 Vue 源碼,第一篇就先講最最經典的 Vue 響應式原理吧!

在正式講原理之前,我覺得應該首先講明白下面幾個概念 ↓

Dep#

var Dep = function Dep() {
  this.id = uid++
  this.subs = []
}

Dep 的含義,自然就是 dependency(也就是依賴,一個計算機領域的名詞)。

就像編寫 node.js 程序,常會使用 npm 倉庫的依賴。在 Vue 中,依賴具體指的是經過響應式處理的數據。後面會提到,響應式處理的關鍵函數之一是在很多 Vue 原理文章都會提到的 defineReactive

Dep 與每個響應式數據綁定後,該響應式數據就會成為一個依賴(名詞),下面介紹 Watcher 時會提到,響應式數據可能被 watch、computed、渲染模板 3 種函數依賴(動詞)。

subs#

Dep 對象下有一個 subs 屬性,是一個數組,很容易猜出,就是 subscriber(訂閱者)列表的意思咯。訂閱者可能是 watch 函數、computed 函數、視圖更新函數。

Watcher#

Watcher 是 Dep 裡提到的訂閱者(不要和後面的 Observer 觀察者搞混)。

因為 Watcher 的功能在於及時響應 Dep 的更新,就像一些 App 的訂閱推送,你(Watcher)訂閱了某些資訊(Dep),資訊更新時會提醒你閱讀。

deps#

與 Dep 擁有 subs 屬性類似,Watcher 對象也有 deps 屬性。這樣構成了 Watcher 和 Dep 就是一個多對多的關係,互相記錄的原因是當一方被清除的時候可以及時更新相關對象。

Watcher 如何產生#

上面多次提到的 watch、computed、渲染模板產生 Watcher,在 Vue 源碼裡都有簡明易懂的體現:

  • mountComponentvm._watcher = new Watcher(vm, updateComponent, noop);
  • initComputedwatchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
  • $watchervar watcher = new Watcher(vm, expOrFn, cb, options);

Observer#

Observer 是觀察者,他負責遞歸地觀察(或者說是處理)響應式對象(或數組)。在打印出的實例裡,可以注意到響應式的對象都會帶著一個 __ob__,這是已經被觀察的證明。觀察者沒有上面的 Dep 和 Watcher 重要,稍微了解下就可以了。

walk#

Observer.prototype.walk 是 Observer 初始化時遞歸處理的核心方法,不過此方法用於處理對象,另外還有 Observer.prototype.observeArray 處理數組。

核心流程#

按照上面幾個概念的關係,如何搭配,該如何實現數據響應式更新?

首先定下我們的目標:自然是在數據更新時,自動刷新視圖,顯示最新的數據。

這就是上面提到的 Dep 和 Watcher 的關係,數據是 Dep,而 Watcher 觸發的是頁面渲染函數(這是最重要的 watcher)。

但是新問題隨之而來,Dep 怎麼知道有什麼 Watcher 依賴於他?

Vue 采用了一個很有意思的方法:

  • 在運行 Watcher 的回調函數前,先記下當前 Watcher 是什麼(通過 Dep.target)
  • 運行回調函數中用到響應式數據,那麼必然會調用響應式數據的 getter 函數
  • 在響應式數據的 getter 函數中就能記下當前的 Watcher,建立 Dep 和 Watcher 的關係
  • 之後,在響應式數據更新時,必然會調用響應式數據的 setter 函數
  • 基於之前建立的關係,在 setter 函數中就能觸發對應 Watcher 的回調函數了

代碼#

上述邏輯就在 defineReactive 函數中。這個函數入口不少,這裡先講比較重要的 observe 函數。

observe 函數中會 new Observer 對象,其中使用 Observer.prototype.walk 對對象中的值進行逐個響應式處理,使用的就是 defineReactive 函數。

因為 defineReactive 函數太重要了,而且也不長,所以直接貼到這邊講比較方便。

function defineReactive(obj, key, val, customSetter, shallow) {
  var dep = new Dep()
  depsArray.push({ dep, obj, key })
  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set

  var childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      var value = getter ? getter.call(obj) : val
      // 後半部分詭異的條件是用於判斷新舊值都是 NaN 的情況
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // customSetter 用於提醒你設置的值可能存在問題
      if ('development' !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    },
  })
}

首先響應式對象的每個屬性都是一個 “依賴 ",所以第一步我們先借閉包的能力給每個值造一個 Dep。(到 Vue 3 就不需要閉包啦)

接著看核心的三個參數:

  • obj 當前需要響應式處理的值所在的對象
  • key 值的 key
  • val 當前的值

這個值還可能之前就定義了自己的 getter、setter,所以在做 Vue 的響應式處理時先處理原本的 getter、setter。

getter#

上面在核心流程中提到在 getter 函數會建立 Dep 和 Watcher 的關係,具體來說依靠的是 dep.depend()

下面貼一下 DepWatcher 互相調用的幾個方法:

Dep.prototype.depend = function depend() {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
Watcher.prototype.addDep = function addDep(dep) {
  var 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.prototype.addSub = function addSub(sub) {
  this.subs.push(sub)
}

通過這幾個函數,可以領略到了 DepWatcher 錯綜複雜的關係…… 不過看起來迂回,簡單來說,其實做的就是上面說的互相添加到多對多列表。

你可以在 Dep 的 subs 找到所有訂閱同一個 Dep 的 Watcher,也可以在 Watcher 的 deps 找到所有該 Watcher 訂閱的所有 Dep。

但是裡面還有一個隱藏問題,就是 Dep.target 怎麼來呢?先放一放,後會作出解答。

setter#

先接著看看 setter 函數,其中的關鍵是 dep.notify()

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

不難理解,就是 Dep 提醒他的訂閱者列表(subs)裡的所有人更新,所謂訂閱者都是 Watcher,subs[i].update() 調用的也就是 Watcher.prototype.update

那麼來看一下 Watcher 的 update 做了什麼 ——

Watcher.prototype.update = function update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

在這裡我覺得有兩個點比較值得展開,所以挖點坑 😂

  • 坑 1:這裡如果不是同步更新的話會跑到 queueWatcher,之後再來講異步更新,同時也降低了這裡的理解難度,總之知道 queueWatcher 在一頓操作之後還是會運行 run 就好了
  • 坑 2:Watcher 的 cb 函數可能會處理 watch、computed 和組件更新函數。尤其重要的是組件更新函數,也正在這裡進行 Vue 頁面更新,所以這裡也值得展開,為降低理解難度,只要知道更新在這裡觸發即可,更新方法後面再說
  • 坑 3:可以看到 lazy 時其實沒有運行下面的步驟只會標記數據更新過,在下次取值再計算新的值
Watcher.prototype.run = function run() {
  if (this.active) {
    var value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(
            e,
            this.vm,
            'callback for watcher "' + this.expression + '"'
          )
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

這段代碼的重點在於需要現在 get 方法中對 Dep.target 進行了設置。(具體路徑是 run -> get -> pushTarget)

因為只有 Dep.target 存在,之後在回調函數 cb(例如頁面渲染函數就是一個典型的 Watcher cb)調用時,Dep.prototype.depend 才能真正生效。再之後的邏輯,就回到使用響應式數據的取值,一切都連起來了!形成閉環(滑稽)!這就是上面 depend() 遺留問題的答案。

總結#

  • Dep 與數據關聯,代表數據可以成為依賴
  • Watcher 有 watch、computed、渲染函數 3 種,這些函數可以成為依賴的訂閱者
  • Observer 算是一個處理 Dep 的入口,遞歸處理響應式數據
  • Watcher 的回調函數在使用響應式數據時,會先設置 Dep.target
  • 響應式數據在 getter 函數中通過 Dep.target 得知調用者,並與調用者建立訂閱者和依賴的關係
  • 響應式數據在 setter 函數中遍歷 subs 通知所有訂閱者該數據更新
  • 當訂閱者為視圖更新函數(updateComponent -> _update)時,用戶就能在響應式數據更新時看到頁面更新,從而實現響應式更新效果

雖說粗略來說這個算法並不難理解,但實際上還有許多其他機制與這個算法一起協作,組成完整的 Vue。例如上面挖的坑:更新隊列和組件更新的函數本身的實現,都值得學習。

另外還有代碼裡還有更多小細節,這就留下感興趣的大家自己研究啦。

PS. 因為我的表達能力實在不算好,再加上知識的詛咒,不確定這篇文字是否能真的講清楚 Vue 響應式原理,如果有什麼看不懂的地方請在評論區提出,謝謝大家 💡

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。