Computed 計算屬性是 Vue 中常用的一個功能,但你理解它是怎麼工作的嗎?
拿官網簡單的例子來看一下:
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})
Situation
Vue 裡的 Computed 屬性非常頻繁的被使用到,但並不是很清楚它的實現原理。比如:計算屬性如何與屬性建立依賴關係?屬性發生變化又如何通知到計算屬性重新計算?
關於如何建立依賴關係,我的第一個想到的就是語法解析,但這樣太浪費效能,因此排除,第二個想到的就是利用 JavaScript 單執行緒的原理和 Vue 的 Getter 設計,通過一個簡單的釋出訂閱,就可以在一次計算屬性求值的過程中收集到相關依賴。
因此接下來的任務就是從 Vue 原始碼一步步分析 Computed 的實現原理。
Task
分析依賴收集實現原理,分析動態計算實現原理。
Action
data 屬性初始化 getter setter:
// src/observer/index.js
// 這裡開始轉換 data 的 getter setter,原始值已存入到 __ob__ 屬性中
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 判斷是否處於依賴收集狀態
if (Dep.target) {
// 建立依賴關係
dep.depend()
...
}
return value
},
set: function reactiveSetter (newVal) {
...
// 依賴發生變化,通知到計算屬性重新計算
dep.notify()
}
})
computed 計算屬性初始化
// src/core/instance/state.js
// 初始化計算屬性
function initComputed (vm: Component, computed: Object) {
...
// 遍歷 computed 計算屬性
for (const key in computed) {
...
// 建立 Watcher 例項
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
// 建立屬性 vm.reversedMessage,並將提供的函式將用作屬性 vm.reversedMessage 的 getter,
// 最終 computed 與 data 會一起混合到 vm 下,所以當 computed 與 data 存在重名屬性時會丟擲警告
defineComputed(vm, key, userDef)
...
}
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {
...
// 建立 get set 方法
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
...
// 建立屬性 vm.reversedMessage,並初始化 getter setter
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// watcher 暴露 evaluate 方法用於取值操作
watcher.evaluate()
}
// 同第1步,判斷是否處於依賴收集狀態
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
無論是屬性還是計算屬性,都會生成一個對應的 watcher 例項。
// src/core/observer/watcher.js
// 當通過 vm.reversedMessage 獲取計算屬性時,就會進到這個 getter 方法
get () {
// this 指的是 watcher 例項
// 將當前 watcher 例項暫存到 Dep.target,這就表示開啟了依賴收集任務
pushTarget(this)
let value
const vm = this.vm
try {
// 在執行 vm.reversedMessage 的函調函式時,會觸發屬性(步驟1)和計算屬性(步驟2)的 getter
// 在這個執行過程中,就可以收集到 vm.reversedMessage 的依賴了
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
// 結束依賴收集任務
popTarget()
this.cleanupDeps()
}
return value
}
上面多出提到了 dep.depend, dep.notify, Dep.target,那麼 Dep 究竟是什麼呢?
Dep 的程式碼短小精悍,但卻承擔著非常重要的依賴收集環節。
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 更新 watcher 的值,與 watcher.evaluate() 類似,
// 但 update 是給依賴變化時使用的,包含對 watch 的處理
subs[i].update()
}
}
}
// 當首次計算 computed 屬性的值時,Dep 將會在計算期間對依賴進行收集
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
// 在一次依賴收集期間,如果有其他依賴收集任務開始(比如:當前 computed 計算屬性巢狀其他 computed 計算屬性),
// 那麼將會把當前 target 暫存到 targetStack,先進行其他 target 的依賴收集,
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
// 當巢狀的依賴收集任務完成後,將 target 恢復為上一層的 Watcher,並繼續做依賴收集
Dep.target = targetStack.pop()
}
Result
總結一下依賴收集、動態計算的流程:
1. data 屬性初始化 getter setter
2. computed 計算屬性初始化,提供的函式將用作屬性 vm.reversedMessage 的 getter
3. 當首次獲取 reversedMessage 計算屬性的值時,Dep 開始依賴收集
4. 在執行 message getter 方法時,如果 Dep 處於依賴收集狀態,則判定 message 為 reversedMessage 的依賴,並建立依賴關係
5. 當 message 發生變化時,根據依賴關係,觸發 reverseMessage 的重新計算
到此,整個 Computed 的工作流程就理清楚了。
Vue 是一個設計非常優美的框架,使用 Getter Setter 設計使依賴關係實現的非常順其自然,使用計算與渲染分離的設計(優先使用 MutationObserver,降級使用 setTimeout)也非常貼合瀏覽器計算引擎與排版引擎分離的的設計原理。
如果你想成為一名架構師,不能只停留在框架的 API 使用層面。