本系列一共七章,Github 地址請查閱這裡,原文地址請查閱這裡。
除了髒檢查之外的資料繫結簡介
這是編寫 JavaScript 框架系列第四章。本章將會闡述髒檢查和資料存取器繫結技術,並指出他們的優缺點。
資料繫結簡介
資料繫結是一個通用的技術,用來繫結來自提供者和消費者的資料來源並同步它們。
這是一個通用定義,概括了資料繫結技術的通用構建模組
- 一種定義資料提供者和消費者的語法
- 一種定義哪些變化會觸發同步的語法
- 一種在提供者中監聽這些變化的方法
- 當這些變化發生時執行的一個同步函式。從現在開始,我將把這個函式稱為
handler()
。
以上的步驟在不同的資料繫結技術中會以不同的方式實現。接下來將會介紹兩種技術,即髒檢查和存取器方法。他們都有優缺點,我將在介紹後簡要討論。
髒檢查
髒檢查是最為人熟知的資料繫結方法。它的概念很簡單,不需要複雜的語言特性,這使得它可以作為一個很好的候選預設選擇。
語法
定義提供者和消費者不要求任何特別的語法,只需要一個簡單的 JavaScript 物件。
const provider = {
message: 'Hello World'
}
const consumer = document.createElement('p')
複製程式碼
同步通常是提供者上的屬性變化觸發。需要監聽變化的屬性,必須明確對映到各自的 handler()
函式。
observe(provider, 'message', message => {
consumer.innerHTML = message
})
複製程式碼
observe()
函式只儲存 (provider, property) -> handler
對映供以後使用。
function observe(provider, prop, handler) {
provider._handlers[prop] = handler
}
複製程式碼
這樣,我們就有一個定義提供者和消費者的語法,以及一種為屬性改變而註冊 handler()
函式的方法。我們庫的公共 API 已經準備好了,現在介紹其內部實現。
監聽變化
髒檢查因一個原因而被稱為髒。它定時檢查而不是直接監聽屬性變化。從現在起,我們把這個檢查稱為摘要週期。一個摘要週期遍歷每一個由 observe()
函式新增的 (provider, property) -> handler
入口,並且檢查自上次遍歷以來屬性值是否發生變化。如果變化則執行 handler()
函式。一個簡單的實現類似如下:
function digest() {
providers.forEach(digestProvider)
}
function digestProvider (provider) {
for (let prop in provider._handlers) {
if (provider._prevValues[prop] !== provider[prop]) {
provider._prevValues[prop] = provider[prop]
handler(provider[prop])
}
}
}
複製程式碼
digest()
函式需要一直不停地執行以確保狀態同步。
存取器技術
存取器技術現在是熱點。因為需要 ES5 getter/ setter 功能而未受廣泛支援,但是它因語法優雅而彌補了這個弱點。
語法
定義提供者需要特殊的語法。簡單的提供者物件必須被傳入 observable
函式,這個函式可以用來把提供者物件轉化為一個被監聽的物件。
const provider = observable({
greeting: 'Hello',
subject: 'World'
})
const consumer = document.createElement('p')
複製程式碼
簡單的 handler()
對映語法彌補了這種小的不便。使用髒檢查,我們不得不如下顯式地定義每個被監聽的屬性。
observe(provider, 'greeting', greeting => {
consumer.innerHTML = greeting + ' ' + provider.subject
})
observe(provider, 'subject', subject => {
consumer.innerHTML = provider.greeting + ' ' + subject
})
複製程式碼
這顯得很囉嗦和笨拙。訪問器技術可以自動檢測在 handler()
函式中使用過的提供者屬性,這樣允許我們簡化以上程式碼。
observe(() => {
consumer.innerHTML = provider.greeting + ' ' + provider.subject
})
複製程式碼
observe()
的實現和髒檢查的不同。它只是執行了傳入的 handler()
函式,並且當handler()
執行的時候把函式標識為目前啟用狀態。
let activeHandler
function observe(handler) {
activeHandler = handler
handler()
activeHandler = undefined
}
複製程式碼
注意,我們現在利用了 JavaScript 的單執行緒特性,使用唯一的 activeHandler
變數來記錄目前執行的 handler()
的函式。
監聽變化
這就是存取器技術名字的由來。提供者使用 getters/setters 來擴充套件,這兩個方法在後臺進行復雜的工作。思路是以如下方式攔截提供者屬性的存取操作。
- get: 如果有一個
activeHandler
在執行,儲存(provider, property) -> activeHandler
對映以備後用。 - set: 執行所有由
(provide, property)
對映的handler()
函式。
以下程式碼展示了一個提供者的屬性變化的簡單的實現過程。
function observableProp(provider, prop) {
const value = provider[prop]
Object.defineProperty(provider, prop, {
get () {
if (activeHandler) {
provider._handlers[prop] = activeHandler
}
return value
},
set (newValue) {
value = newValue
const handler = obj._handlers[prop]
if (handler) {
activeHandler = handler
handler()
activeHandler = undefined
}
}
})
}
複製程式碼
在前面一節提到的 observable()
函式遞迴遍歷提供者的屬性,並且使用 observableProp()
函式來轉化所有屬性為可監測。
function observable (provider) {
for (let prop in provider) {
observableProp(provider, prop)
if (typeof provider[prop] === 'object') {
observable(provider[prop])
}
}
}
複製程式碼
這是一個非常簡單的實現,但是對於比較這兩種技術已經足夠了。
兩種技術的比較
本節將會概括性地指出髒檢查和存取器技術的優缺點。
語法
髒檢查不需要語法來定義的提供者和消費者,但是把 handler()
和 (provider, property)
進行對映配對是笨拙和不靈活的。
存取器技術要求用 observable()
來封裝提供者,但是自動 handler()
對映彌補了這個不足。對於使用資料繫結的大型專案來說,這是必須的功能。
效能
髒檢查因為糟糕的效能問題而廣受垢病。它不得不在每個摘要週期可能多次檢查每個 (provider, property) -> handler
入口。而且,因為它無法知曉什麼時候屬性值發生變化,所以即便應用處於閒置狀態也必須保持運轉。
存取器速度更快,但是如果是監聽一個很大的物件的話會不可避免地降低效能。使用存取器來替換提供者的每個屬性經常會導致過度濫用。一個方案是在需要的時候動態構建存取器樹而不是一開始就成批地建立。還有一種可替代的簡單方案即是把不需要監聽的屬性用 noObserve()
函式封裝起來,這個可以告訴 observable()
不要處理這些屬性。令人沮喪的是,這會引入一些額外的語法。
靈活性
髒檢查天生支援 expando(動態新增)和存取器屬性。
存取器技術在這裡有一個弱點。因為 Expando 屬性不在初始的存取樹上面,所以不支援存取器技術。舉個例子,這會導致陣列問題,但是可以在新增了一個屬性之後通過手動執行 observableProp()
來解決。因為存取器不能被存取器二次封裝,所以存取屬性是不支援的。通常的解決方法是使用 computed()
函式而不是 getter。這將引入更多的自定義語法。
定時選擇
髒檢查沒有給我們太多選擇的自由,因為我們不知道什麼時候屬性值真正發生了改變。handler()
函式只能通過不斷地執行 digest() 迴圈來非同步執行。
存取器技術建立的存取器是同步觸發的,所以我們可以自由選擇。可以選擇馬上執行 handler()
,或者將其儲存在稍後非同步執行的批處理中。前一種技術給予我們可預測性的優點,而後者可以通過移除重複項來提升效能。