前言與相關背景知識
本篇文章分析 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,無論是在 computed、template 中顯示的資料,或是 directive(如 v-if、v-for)使用的資料,都是相依於它的 function 執行結果。
Vue 為什麼需要 Reactivity
隨著應用操作複雜性增加,單一資料被更新的管道與情境會十分多變。以圖片上傳應用為例,狀態包括:尚未上傳 → 上傳中 → 上傳成功/失敗,期間可能涉及:
- 使用者操作(上傳、取消)
- Server 回應(成功、失敗)
- 重試邏輯
- 狀態衝突處理
若透過條件式處理,需要考慮許多因素組合,容易產生邏輯錯誤。而使用 Reactivity 方式,將資料做成可被觀測的變數,當資料異動時主動通知對應操作行為。這樣從描述「流程」轉變為描述「事件處理」,邏輯相對單純許多。
Reactivity 在 Vue 中發生的時機與作用區域
在每個 Component 的 lifecycle 中,computed、data、props 的 Reactivity 脫離不了關係。具體來說:
- 當 component mounted 觸發
render開始,需要對template、vue directive中的資料進行求值來顯示 - 例如:
{{ x + 10 }}、v-if="(x + 10) > 2",或在computed中使用這樣的邏輯
此過程需要進行「dependency collect(相依性收集)」——找出求值 f(x) = x + 10 時所需的變數 x。因此 x + 10 是一個 Observer,而 x 本身是一個 Observable。
流程為:
- 存取資料的
getter函式 - 將相依的變數放入
Watcher更新佇列 - 觸發畫面重新
render
當程式事件(touch、click、Ajax 回應等)更動資料值時,觸發資料的 setter,該資料發出通知,告知 Watcher 更新 Component,形成循環。
Vue 如何實現 Reactivity
Reactivity 邏輯實作主要在 observer 中:
初始化、Object.defineProperty 與 defineReactive
Vue 透過 initMixin 在 Vue.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 的 get 與 set 屬性進行修改。
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 接收並執行對應操作。
後記
一些使用細節
了解上述實現原理後,就不難理解以下細節:
- Watch handler 中 deep 的差別
- computed 的實現
- $set 使用時機與原因
- template / v-model 等 directive 資料如何被更新
- 如何偵測(追蹤)Array / Object type data 的變更
Source Code 與參考版本
本文參考 VueJS 2.6.10 版本中的實作版本。若有理解上的錯誤歡迎指出與糾正。
未來相關發展
在 Vue 3.0 的發布訊息中,有以下變更預計:
- Proxy-based Observation:透過
Object.defineProperty方式改寫 get、set function 將改用 Proxy 進行實作 - Decoupled Packages:預計將許多邏輯進行解耦合,包含
src/core/observer及 scheduler 邏輯,拆分作為獨立模組 - Exposed reactivity API:將 Reactivity 相關 API 公開給使用者使用
原文發表於 Medium