自制前端框架之依賴追蹤器

doodlewind發表於2017-08-03

依賴追蹤機制是 Vue 的核心之一,那麼依賴追蹤演算法如何工作呢?在 30 行內我們就能實現它?

Reactive 基礎

說起依賴追蹤,就不能不提資料繫結的概念。前端最常見的重複勞動之一就是把資料繫結到 HTML 模板上,這時資料繫結能夠實現資料更新時模板的自動更新。簡單的三行虛擬碼就能描述出這個流程的實際使用場景:

const data = { foo: 123 }
magic(data, dom) // 定義 Reactive 並繫結資料到 dom
data.foo = 456 // 資料更新時 dom 自動更新複製程式碼

這裡的 magic 能夠把普通的 JS 物件轉換為支援資料繫結的 Reactive 物件,在 Reactive 物件資料更新時,被繫結的模板也會進行更新。作為依賴追蹤的基礎,我們還是先用幾行實現一個最簡單的 Reactive 示例吧:

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      return val
    },
    set (newValue) {
      // 在此新增更新繫結資料相關程式碼
      val = newValue
    }
  })
}複製程式碼

可以看到,Reactive 本身其實就是通過 Object.defineProperperty 新增了自定義 getter / setter 後的物件。這類物件能夠在讀寫其屬性值時,執行使用者自定的程式碼,從而在此實現被繫結資料的更新。有了這個能力後,我們就可以開始編寫依賴追蹤器了。

依賴追蹤原理

我們可以用 Excel 來理解依賴追蹤:Reactive 就是普通單元格中的原始資料,而 Computed 就是插入了公式的單元格。Reactive 的格子更新時,Computed 的格子會根據為它設定的求和公式(即依賴)來自動更新出相應的值。

所以,對 Computed 最樸素的定義,就是一個簡單的函式,形如這樣:

// 這裡的 data 是一個 Reactive
const isEmpty = () => data.values.length === 0複製程式碼

這初看之下沒有任何 Magic,不過這裡關鍵的細節區別在於:在指定 Excel 的公式時,我們需要手動選擇公式所依賴的單元格,但在一個 Computed 函式中,我們沒有傳入 Computed 的依賴!既然沒有傳入依賴,那麼這個 Computed 函式是怎麼在它所使用的 Reactive 更新時去更新自身的呢?這就是依賴追蹤演算法所需要解決的問題了。

我們知道,Reactive 的資料繫結,本質上是在 set Reactive 時去執行更新。而依賴追蹤則相反,需要在 Computed 中 get Reactive 時,去標記 Computed 對 Reactive 的依賴。

為了理解這個演算法,我們不妨先假設一個簡單的執行場景:假設 Computed 函式 C 依賴了 Reactive 物件 R1 和 R2,這時我們新增一個全域性的輔助物件 D 來為當前 Computed 函式收集依賴。從而,我們可以用文字描述出這個演算法的執行流程:

  1. Computed 函式 C 初次求值時,標記 D 指向 C
  2. 對 C 求值過程中,獲取了 R1 和 R2 這兩個 Reactive 的值,使得各 Reactive 的 getter 被觸發
  3. 為每個 Reactive 維護一個自己的依賴者 deps 陣列,將 D 新增至陣列內。從而,在 C 求值完成後,R1 和 R2 均完成對 C 的依賴收集
  4. 求值完成後,C 將 D 標記為空,返回求值結果
  5. 經過標記後的 R1 和 R2 更新時,所有新增至各自 deps 陣列中的 Computed 均在 reactive 的 setter 中觸發,一併更新

這個演算法的核心,就是為 Reactive 新增【依賴者】陣列,從而在 Computed 觸發 Reactive 時,新增該 Computed 至 Reactive 的依賴者中。這樣,在 Reactive 下次更新時,就能夠主動地觸發 Computed 的更新了。下面我們使用程式碼來實現這個文字流程。

實現 Computed

動手實現 Computed 前,我們不妨設計出實際使用場景下一個簡單的 API,然後從 API 介面出發來進行編碼實現。假設我們有一個 elder 物件,他具有 now 這個 Reactive 來標記當前年份,那麼我們可以定義出一個 Computed 來計算出他的年齡:

const elder = {}
defineReactive(elder, 'now', null)
defineComputed(elder, 'age', () => elder.now - 1926,
  () => console.log('Now his age is', elder.age)
)

elder.now = 2016
console.log(elder.age)

elder.now = 2017
console.log(elder.age)複製程式碼

在使用方式上,可以發現我們先是定義 Reactive,再定義從 Reactive 衍生出的 Computed 函式。

接下來就是程式碼實現了。我們在前文的 defineReactive 函式基礎上,擴充出新的 defineComputed 函式。去除掉囉嗦的註釋後,是可以控制在 30 行內的?

// 標記當前正在求值的 computed 函式
let Dep = null

// 定義 computed,需傳入求值函式與 computed 更新時觸發的回撥
function defineComputed (obj, key, computeFn, updateCallback) {
  // 封裝供 reactive 收集的更新回撥,以觸發 computed 的更新事件
  const onDependencyUpdated = function () {
    // 在此呼叫 computeFn 計算出的值用於觸發 computed 的更新事件
    // 供後續可能的 watch 等模組使用
    const value = computeFn()
    updateCallback(value)
  }
  Object.defineProperty(obj, key, {
    get () {
      // 標記當前依賴,供 reactive 收集
      Dep = onDependencyUpdated
      // 呼叫求值函式,中途收集依賴
      const value = computeFn()
      // 完成求值後,清空標記
      Dep = null
      // 最終返回的 getter 結果
      return value
    },
    // 計算屬性無法 set
    set () {}
  })
}

// 通過 getter 與 setter 定義出一個 reactive
function defineReactive (obj, key, val) {
  // 在此標記哪些 computed 依賴了該 reactive
  const deps = []

  Object.defineProperty(obj, key, {
    // 為 reactive 求值時,收集其依賴
    get () {
      if (Dep) deps.push(Dep)
      // 返回 val 值作為 getter 求值結果
      return val
    },
    // 為 reactive 賦值時,更新所有依賴它的計算屬性
    set (newValue) {
      // 在 setter 中更新值
      val = newValue
      // 更新值後觸發所有 computed 依賴更新
      deps.forEach(changeFn => changeFn())
    }
  })
}複製程式碼

在上例中的程式碼實現中,我們除了實現了一個新的 defineComputed 函式外,還在 defineReactive 函式中進行了一定的修改。這主要體現在,我們在 Reactive 中:

  1. 新增了 deps 陣列
  2. 在 getter 中進行了依賴收集
  3. 在 setter 中計算出了所有依賴該 Reactive 的 Computed

這個在 getter 中收集依賴,而後在 setter 中觸發的模式,實際就是本系列中第一篇 MVC 框架介紹中,所涉及的 PubSub 釋出訂閱模式了。不同之處在於,在依賴收集器中,我們通過 Object.defineProperty 這一高階特性將 PubSub 模式進行了封裝,PubSub 中需要使用者顯式操作的【訂閱】過程被平滑地優化為了【通過 getter 自動化進行的依賴收集】。依賴收集完成後,就能在 Reactive 更新時實現依賴追蹤了。

OK,這就是依賴追蹤器的基礎實現了,本文的原始碼亦託管在 Github 上,可以拉取或直接複製到 Node 中執行?希望對感興趣的同學有所幫助。

本系列後續會繼續專注用簡單的程式碼解釋前端框架各類 Magic 的實現機制,安利往期文章:

自制前端框架之 50 行的虛擬 DOM
自制前端框架之 MVC
Github

相關文章