返回文章列表

Deep into Vue:Reactivity 的實現

2020-12-02
6 分鐘
VueFrontendArchitecture

前言與相關背景知識

本篇文章分析 Vue 對資料 Reactivity 的實現方式與原理。適合已經了解如何使用 Vue 或想要實作 Reactivity 特性的讀者。

閱讀本文的建議背景知識包括:

  • Concept: Reactivity Programming
  • Design Pattern: Observer
  • Data Structure: Tree, Set
  • Algorithm: Tree traversal

Reactivity / Reactive Programming 是什麼

Reactivity Programming 的核心概念在於:當某個值在一個位置發生變化時,所有依賴於該值的其他位置都會自動重新計算和更新,而無需阻塞線程等待事件發生。

此觀念可比喻為數學中的自變數與應變數關係:

x → f(x)
x → g(x)
x → f・g(x)

當自變數 x 變更時,所有相依的 function 都會重新計算。在程式設計中,這樣的 x 通常是 GUI 操作產生的事件。

在 Vue 中,會變動的 data 就是上述的 x,無論是在 computedtemplate 中顯示的資料,或是 directive(如 v-ifv-for)使用的資料,都是相依於它的 function 執行結果。

Vue 為什麼需要 Reactivity

隨著應用操作複雜性增加,單一資料被更新的管道與情境會十分多變。以圖片上傳應用為例,狀態包括:尚未上傳 → 上傳中 → 上傳成功/失敗,期間可能涉及:

  • 使用者操作(上傳、取消)
  • Server 回應(成功、失敗)
  • 重試邏輯
  • 狀態衝突處理

若透過條件式處理,需要考慮許多因素組合,容易產生邏輯錯誤。而使用 Reactivity 方式,將資料做成可被觀測的變數,當資料異動時主動通知對應操作行為。這樣從描述「流程」轉變為描述「事件處理」,邏輯相對單純許多。

Reactivity 在 Vue 中發生的時機與作用區域

在每個 Component 的 lifecycle 中,computeddataprops 的 Reactivity 脫離不了關係。具體來說:

  • 當 component mounted 觸發 render 開始,需要對 templatevue directive 中的資料進行求值來顯示
  • 例如:{{ x + 10 }}v-if="(x + 10) > 2",或在 computed 中使用這樣的邏輯

此過程需要進行「dependency collect(相依性收集)」——找出求值 f(x) = x + 10 時所需的變數 x。因此 x + 10 是一個 Observer,而 x 本身是一個 Observable

流程為:

  1. 存取資料的 getter 函式
  2. 將相依的變數放入 Watcher 更新佇列
  3. 觸發畫面重新 render

當程式事件(touch、click、Ajax 回應等)更動資料值時,觸發資料的 setter,該資料發出通知,告知 Watcher 更新 Component,形成循環。

Vue 如何實現 Reactivity

Reactivity 邏輯實作主要在 observer 中:

初始化、Object.defineProperty 與 defineReactive

Vue 透過 initMixinVue.prototype 中新增 Vue.prototype._init。當執行 new Vue(options) 時,會觸發 initState

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

observe 函式使用 Observer class 產生 observer instance。在建構子中會遍歷物件的 Object.keys,透過 defineReactive(obj, keys[i]) 將各項屬性重新包裝賦予 Reactive 性質。

defineReactive 透過使用 Object.defineProperty 將傳入 value 的 getset 屬性進行修改。

Collect as Dependency 與 Notify

在 reactive getter 及 setter 中分別執行 dep.depend()dep.notify()

Dep 物件在 observer 建構子中透過 new Dep() 產生,是一個具備可被觀察的變數。它擁有許多 subscribers 訂閱,並具有 notify 行為通知每個訂閱者進行 update 行為。

reactiveSetter 被執行,表示資料被變更,便透過 dep 執行 notify 通知所有相關的 subscribers 進行更新。

Dep class 的命名是 Dependency 的縮寫。Dep instance 就是 observable 的概念,其屬性 subs 表示許多的 subscribers (observers),型態為 Array of Watcher

值得注意的是 static target: ? Watcher 這個 Watcher class 的 nullable instance。當變數的 getter 被觸發時,其 dep instance 透過 depend 把自身加入 target 這個 Watcher 之中。當變數發出 notify 時便被 target 接收並執行對應操作。

後記

一些使用細節

了解上述實現原理後,就不難理解以下細節:

  1. Watch handler 中 deep 的差別
  2. computed 的實現
  3. $set 使用時機與原因
  4. template / v-model 等 directive 資料如何被更新
  5. 如何偵測(追蹤)Array / Object type data 的變更

Source Code 與參考版本

本文參考 VueJS 2.6.10 版本中的實作版本。若有理解上的錯誤歡迎指出與糾正。

未來相關發展

在 Vue 3.0 的發布訊息中,有以下變更預計:

  1. Proxy-based Observation:透過 Object.defineProperty 方式改寫 get、set function 將改用 Proxy 進行實作
  2. Decoupled Packages:預計將許多邏輯進行解耦合,包含 src/core/observer 及 scheduler 邏輯,拆分作為獨立模組
  3. Exposed reactivity API:將 Reactivity 相關 API 公開給使用者使用

原文發表於 Medium

Command Palette

Search for a command to run...