您好,登錄后才能下訂單哦!
Computed 計(jì)算屬性是 Vue 中常用的一個(gè)功能,但你理解它是怎么工作的嗎?
拿官網(wǎng)簡(jiǎn)單的例子來(lái)看一下:
<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 屬性非常頻繁的被使用到,但并不是很清楚它的實(shí)現(xiàn)原理。比如:計(jì)算屬性如何與屬性建立依賴(lài)關(guān)系?屬性發(fā)生變化又如何通知到計(jì)算屬性重新計(jì)算?
關(guān)于如何建立依賴(lài)關(guān)系,我的第一個(gè)想到的就是語(yǔ)法解析,但這樣太浪費(fèi)性能,因此排除,第二個(gè)想到的就是利用 JavaScript 單線(xiàn)程的原理和 Vue 的 Getter 設(shè)計(jì),通過(guò)一個(gè)簡(jiǎn)單的發(fā)布訂閱,就可以在一次計(jì)算屬性求值的過(guò)程中收集到相關(guān)依賴(lài)。
因此接下來(lái)的任務(wù)就是從 Vue 源碼一步步分析 Computed 的實(shí)現(xiàn)原理。
Task
分析依賴(lài)收集實(shí)現(xiàn)原理,分析動(dòng)態(tài)計(jì)算實(shí)現(xiàn)原理。
Action
data 屬性初始化 getter setter:
// src/observer/index.js // 這里開(kāi)始轉(zhuǎn)換 data 的 getter setter,原始值已存入到 __ob__ 屬性中 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 判斷是否處于依賴(lài)收集狀態(tài) if (Dep.target) { // 建立依賴(lài)關(guān)系 dep.depend() ... } return value }, set: function reactiveSetter (newVal) { ... // 依賴(lài)發(fā)生變化,通知到計(jì)算屬性重新計(jì)算 dep.notify() } })
computed 計(jì)算屬性初始化
// src/core/instance/state.js // 初始化計(jì)算屬性 function initComputed (vm: Component, computed: Object) { ... // 遍歷 computed 計(jì)算屬性 for (const key in computed) { ... // 創(chuàng)建 Watcher 實(shí)例 // create internal watcher for the computed property. watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions) // 創(chuàng)建屬性 vm.reversedMessage,并將提供的函數(shù)將用作屬性 vm.reversedMessage 的 getter, // 最終 computed 與 data 會(huì)一起混合到 vm 下,所以當(dāng) computed 與 data 存在重名屬性時(shí)會(huì)拋出警告 defineComputed(vm, key, userDef) ... } } export function defineComputed (target: any, key: string, userDef: Object | Function) { ... // 創(chuàng)建 get set 方法 sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = noop ... // 創(chuàng)建屬性 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步,判斷是否處于依賴(lài)收集狀態(tài) if (Dep.target) { watcher.depend() } return watcher.value } } }
無(wú)論是屬性還是計(jì)算屬性,都會(huì)生成一個(gè)對(duì)應(yīng)的 watcher 實(shí)例。
// src/core/observer/watcher.js // 當(dāng)通過(guò) vm.reversedMessage 獲取計(jì)算屬性時(shí),就會(huì)進(jìn)到這個(gè) getter 方法 get () { // this 指的是 watcher 實(shí)例 // 將當(dāng)前 watcher 實(shí)例暫存到 Dep.target,這就表示開(kāi)啟了依賴(lài)收集任務(wù) pushTarget(this) let value const vm = this.vm try { // 在執(zhí)行 vm.reversedMessage 的函調(diào)函數(shù)時(shí),會(huì)觸發(fā)屬性(步驟1)和計(jì)算屬性(步驟2)的 getter // 在這個(gè)執(zhí)行過(guò)程中,就可以收集到 vm.reversedMessage 的依賴(lài)了 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) } // 結(jié)束依賴(lài)收集任務(wù) popTarget() this.cleanupDeps() } return value }
上面多出提到了 dep.depend, dep.notify, Dep.target,那么 Dep 究竟是什么呢?
Dep 的代碼短小精悍,但卻承擔(dān)著非常重要的依賴(lài)收集環(huán)節(jié)。
// 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() 類(lèi)似, // 但 update 是給依賴(lài)變化時(shí)使用的,包含對(duì) watch 的處理 subs[i].update() } } } // 當(dāng)首次計(jì)算 computed 屬性的值時(shí),Dep 將會(huì)在計(jì)算期間對(duì)依賴(lài)進(jìn)行收集 Dep.target = null const targetStack = [] export function pushTarget (_target: Watcher) { // 在一次依賴(lài)收集期間,如果有其他依賴(lài)收集任務(wù)開(kāi)始(比如:當(dāng)前 computed 計(jì)算屬性嵌套其他 computed 計(jì)算屬性), // 那么將會(huì)把當(dāng)前 target 暫存到 targetStack,先進(jìn)行其他 target 的依賴(lài)收集, if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { // 當(dāng)嵌套的依賴(lài)收集任務(wù)完成后,將 target 恢復(fù)為上一層的 Watcher,并繼續(xù)做依賴(lài)收集 Dep.target = targetStack.pop() }
Result
總結(jié)一下依賴(lài)收集、動(dòng)態(tài)計(jì)算的流程:
1. data 屬性初始化 getter setter
2. computed 計(jì)算屬性初始化,提供的函數(shù)將用作屬性 vm.reversedMessage 的 getter
3. 當(dāng)首次獲取 reversedMessage 計(jì)算屬性的值時(shí),Dep 開(kāi)始依賴(lài)收集
4. 在執(zhí)行 message getter 方法時(shí),如果 Dep 處于依賴(lài)收集狀態(tài),則判定 message 為 reversedMessage 的依賴(lài),并建立依賴(lài)關(guān)系
5. 當(dāng) message 發(fā)生變化時(shí),根據(jù)依賴(lài)關(guān)系,觸發(fā) reverseMessage 的重新計(jì)算
到此,整個(gè) Computed 的工作流程就理清楚了。
Vue 是一個(gè)設(shè)計(jì)非常優(yōu)美的框架,使用 Getter Setter 設(shè)計(jì)使依賴(lài)關(guān)系實(shí)現(xiàn)的非常順其自然,使用計(jì)算與渲染分離的設(shè)計(jì)(優(yōu)先使用 MutationObserver,降級(jí)使用 setTimeout)也非常貼合瀏覽器計(jì)算引擎與排版引擎分離的的設(shè)計(jì)原理。
如果你想成為一名架構(gòu)師,不能只停留在框架的 API 使用層面。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。