依賴追蹤機制是 Vue 的核心之一,那麼依賴追蹤算法如何工做呢?在 30 行內咱們就能實現它🤓前端
提及依賴追蹤,就不能不提數據綁定的概念。前端最多見的重複勞動之一就是把數據綁定到 HTML 模板上,這時數據綁定可以實現數據更新時模板的自動更新。簡單的三行僞代碼就能描述出這個流程的實際使用場景:react
const data = { foo: 123 }
magic(data, dom) // 定義 Reactive 並綁定數據到 dom
data.foo = 456 // 數據更新時 dom 自動更新複製代碼
這裏的 magic
可以把普通的 JS 對象轉換爲支持數據綁定的 Reactive 對象,在 Reactive 對象數據更新時,被綁定的模板也會進行更新。做爲依賴追蹤的基礎,咱們仍是先用幾行實現一個最簡單的 Reactive 示例吧:git
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
return val
},
set (newValue) {
// 在此添加更新綁定數據相關代碼
val = newValue
}
})
}複製代碼
能夠看到,Reactive 自己其實就是經過 Object.defineProperperty
添加了自定義 getter / setter 後的對象。這類對象可以在讀寫其屬性值時,執行用戶自定的代碼,從而在此實現被綁定數據的更新。有了這個能力後,咱們就能夠開始編寫依賴追蹤器了。github
咱們能夠用 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 的依賴。mvc
爲了理解這個算法,咱們不妨先假設一個簡單的執行場景:假設 Computed 函數 C 依賴了 Reactive 對象 R1 和 R2,這時咱們添加一個全局的輔助對象 D 來爲當前 Computed 函數收集依賴。從而,咱們能夠用文字描述出這個算法的執行流程:框架
這個算法的核心,就是爲 Reactive 添加【依賴者】數組,從而在 Computed 觸發 Reactive 時,添加該 Computed 至 Reactive 的依賴者中。這樣,在 Reactive 下次更新時,就可以主動地觸發 Computed 的更新了。下面咱們使用代碼來實現這個文字流程。dom
動手實現 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 中:
這個在 getter 中收集依賴,然後在 setter 中觸發的模式,實際就是本系列中第一篇 MVC 框架介紹中,所涉及的 PubSub 發佈訂閱模式了。不一樣之處在於,在依賴收集器中,咱們經過 Object.defineProperty 這一高級特性將 PubSub 模式進行了封裝,PubSub 中須要用戶顯式操做的【訂閱】過程被平滑地優化爲了【經過 getter 自動化進行的依賴收集】。依賴收集完成後,就能在 Reactive 更新時實現依賴追蹤了。
OK,這就是依賴追蹤器的基礎實現了,本文的源碼亦託管在 Github 上,能夠拉取或直接複製到 Node 中運行🙃但願對感興趣的同窗有所幫助。
本系列後續會繼續專一用簡單的代碼解釋前端框架各種 Magic 的實現機制,安利往期文章: