Vue3計算屬性是如何實現(xiàn)的
前言:
本篇內(nèi)容基于Vue3響應式對象是如何實現(xiàn)的(2)實現(xiàn)。
計算屬性
Vue3的官方文檔中,對于計算屬性有這樣的描述:
- 對于任何包含響應式數(shù)據(jù)的復雜邏輯,我們都應該使用計算屬性。
- 計算屬性只會在相關響應式依賴發(fā)生改變時重新求值。
從上面的描述可以明確計算屬性的需求,計算屬性計算的是響應式數(shù)據(jù)(滿足描述1),且計算結果應當被緩存(滿足描述2)。讓我們一個一個來實現(xiàn),先使用computed創(chuàng)建一個計算屬性。
function effect(fn) { // 副作用函數(shù)
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
...
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { // 響應式對象
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
...
const sumRes = computed(() => obj.foo + obj.bar) // (1)
console.log(sumRes.value)在(1)處,我們簡單寫了一個計算屬性的功能,為了實現(xiàn)通過sumRes.value讀取計算屬性值功能,在實現(xiàn)計算屬性時,需要返回一個對象,通過對象內(nèi)的get觸發(fā)副作用函數(shù)。
function computed(getter) {
const effectFn = effect(getter)
const obj = {
get value() {
return effectFn()
}
}
return obj
}但這個函數(shù)顯然是無法執(zhí)行的,這是因為前面我們在實現(xiàn)effect時,需要直接執(zhí)行副作用函數(shù),不需要提供返回值。沒有返回值,computed自然無法獲取到effect的執(zhí)行結果。因此,當在計算屬性中使用effect時,需要將副作用函數(shù)返回給計算屬性,由計算屬性決定何時執(zhí)行,而不再由effect立即執(zhí)行(即懶執(zhí)行)。
為了實現(xiàn)這點,就需要向effect中添加一個開關lazy,考慮到我們可能將來還需要對effect配置其它特性,我們使用一個對象options來封裝這個開關。
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn() // (1)
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res // (2)
}
effectFn.deps = []
effectFn.options = options // (3)
if (!options.lazy) { // (4)
effectFn()
}
return effectFn // (5)
}我們在(4)處放置了lazy開關,不需要懶執(zhí)行的副作用函數(shù)同樣會自動執(zhí)行。在(1)(2)(5)處返回了副作用函數(shù)的結果,供懶執(zhí)行使用。同時在(3)處向下傳遞了options,保證在effect發(fā)生嵌套時,也使得副作用函數(shù)執(zhí)行預期的行為。基于上述effect的修改,我們在computed中設置lazy開關。
function computed(getter) {
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() { // (6)
return effectFn()
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
從上圖中可以看出,我們已經(jīng)實現(xiàn)了描述1,即使用計算屬性進行響應式數(shù)據(jù)的計算,當響應式數(shù)據(jù)的值發(fā)生變化時,計算屬性的值也會隨之改變。但觀察上文代碼的(6)處,不難發(fā)現(xiàn),無論什么情況下,只要讀取sumRes.value的值,就會觸發(fā)一次副作用函數(shù),使其重新進行可能不必要的執(zhí)行。所以接著,我們嘗試實現(xiàn)描述2,緩存計算屬性的結果。
先從最簡單的入手,我們用一個變量value來緩存上次計算的值,并添加一個dirty開關,記錄是否需要重新觸發(fā)副作用函數(shù)。
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}修改之后,緩存的值就能生效了。但這樣做產(chǎn)生了一個明顯的BUG,當dirty的值被置為false時,無法再變?yōu)?code>true,這也就意味著,無論響應式數(shù)據(jù)obj.bar與obj.foo如何變化,計算屬性的值永遠都只能是緩存的值value,如下圖所示。

為了解決這個問題,我們需要一種方式,能夠在obj.bar或obj.foo的值變化時,在獲取sumRes.value之前,將dirty開關的值置為true。受前面懶加載的啟發(fā),我們嘗試能不能通過配置options來實現(xiàn)這個功能。
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})再來回憶一下響應式對象的整個流程,當響應式對象中的數(shù)據(jù)被修改時,執(zhí)行了trigger去觸發(fā)收集的副作用函數(shù)。而在計算屬性中,我們不再需要自動的觸發(fā)副作用函數(shù)。所以自然會想到,能否在這個地方將dirty置為true呢?按照這個思路,我們先對trigger進行修改。
function trigger(target, key) {
const propsMap = objsMap.get(target)
if(!propsMap) return
const fns = propsMap.get(key)
const otherFns = new Set()
fns && fns.forEach(fn => {
if(fn !== activeEffect) {
otherFns.add(fn)
}
})
otherFns.forEach(fn => {
if(fn.options.scheduler) { // (7)
fn.options.scheduler()
} else {
fn()
}
})
}按照前文的思路,我們在(7)處增加了一個判斷,如果副作用函數(shù)fn的配置項options中含有scheduler函數(shù),我們就執(zhí)行scheduler而非副作用函數(shù)fn。我們稱這里的scheduler為調(diào)度器。相應的,我們在計算屬性中添加調(diào)度器。
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() { // (8)
dirty = true
}
})
const obj = {
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}在(8)處我們添加了調(diào)度器。添加調(diào)度器后,讓我們再來串一下整個流程,當響應式數(shù)據(jù)被修改時,才會執(zhí)行trigger函數(shù)。由于我們傳入了調(diào)度器,因此trigger函數(shù)在執(zhí)行時不再觸發(fā)副作用函數(shù),轉而執(zhí)行調(diào)度器,此時dirty開關的值變?yōu)榱?code>true。當程序再往下執(zhí)行時,由于dirty已經(jīng)變?yōu)?code>true,副作用函數(shù)就可以正常被手動觸發(fā)。效果如下圖所示。

到這里為止,計算屬性在功能上已經(jīng)實現(xiàn)完畢了,讓我們嘗試完善它。在Vue中,當計算屬性中的響應式數(shù)據(jù)被修改時,計算屬性值會同步更改,進而重新渲染,并在頁面上更新。渲染函數(shù)也會執(zhí)行effect,為了說明問題,讓我們使用effect簡單的模擬一下。
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log('sumRes =', sumRes.value))這里我們的預期是當obj.foo或obj.bar改變時,effect會自動重新執(zhí)行。這里存在的問題是,正常的effect嵌套可以被自動觸發(fā)(這點我們在上一篇博客中已經(jīng)實現(xiàn)了),但sumRes包含的effect僅會在被讀取value時才會被get觸發(fā)執(zhí)行,這就導致響應式數(shù)據(jù)obj.foo與obj.bar無法收集到外部的effect,收集不到的副作用函數(shù),自然無法被自動觸發(fā)。
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(obj, 'value') // (9)
}
})
const obj = {
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value') // (10)
return value
}
}
return obj
}在(10)處我們手動收集了副作用函數(shù),并當響應式數(shù)據(jù)被修改時,觸發(fā)它們。

使用微任務優(yōu)化調(diào)度器
在設計調(diào)度器時,我們忽略了一個潛在的問題。
還是先來看一個例子:
effect(() => console.log(obj.foo))
for(let i = 0; i < 1e5; i++) {
obj.foo++
}
隨著響應式數(shù)據(jù)obj.foo被不停修改,副作用函數(shù)也被不斷重復執(zhí)行,在明顯的延遲之后,控制臺打印出了大量的數(shù)據(jù)。但在前端的實際開發(fā)中,我們往往只關心最終結果,拿到結果顯示在頁面上。在這種條件下,我們?nèi)绾伪苊飧弊饔煤瘮?shù)被重復執(zhí)行呢?
const jobQueue = new Set() // (11)
const p = Promise.resolve() // (12)
let isFlushing = false // (13)
function flushJob() { // (14)
if (isFlushing) return
isFlushing = true
p.then(() => {
jobQueue.forEach(job => {
job()
})
}).finally(() => {
isFlushing = false
})
}這里我們的思路是使用微任務隊列來進行優(yōu)化。(11)處我們定義了一個Set作為任務隊列,(12)處我們定義了一個Promise來使用微任務。精彩的部分來了,我們的思路是將副作用函數(shù)放入任務隊列中,由于任務隊列是基于Set實現(xiàn)的,因此,重復的副作用函數(shù)僅會保留一個,這是第一點。接著,我們執(zhí)行flushJob(),這里我們巧妙的設置了一個isFlushing開關,這個開關保證了被微任務包裹的任務隊列在一次事件循環(huán)中只會執(zhí)行一次,而執(zhí)行的這一次會在宏任務完成之后觸發(fā)任務隊列中所有不重復的副作用函數(shù),從而直接拿到最終結果,這是第二點。按照這個思路,我們在effect中添加調(diào)度器。
effect(() => { console.log(obj.foo) }, {
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
})
需要注意的是,瀏覽器在執(zhí)行微任務時,會把微任務隊列中的所有微任務一口氣做完。因此,微任務在執(zhí)行時會阻塞頁面的渲染。所以在實際使用時,要注意避免在微任務隊列中放置過重的任務,以免引起卡頓。
到此這篇關于Vue3計算屬性是如何實現(xiàn)的的文章就介紹到這了,更多相關Vue3計算屬性 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue.js如何在網(wǎng)頁中實現(xiàn)一個金屬拋光質(zhì)感的按鈕
這篇文章主要給大家介紹了關于vue.js如何在網(wǎng)頁中實現(xiàn)一個金屬拋光質(zhì)感的按鈕的相關資料,文中給出了詳細的實例代碼以及圖文將實現(xiàn)的方法介紹的非常詳細,需要的朋友可以參考下2023-04-04
vue中的attribute和property的具體使用及區(qū)別
本文主要介紹了vue中的attribute和property的具體使用及區(qū)別,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09

