<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
前言:
本篇內容基於Vue3響應式物件是如何實現的(2)實現。
Vue3的官方檔案中,對於計算屬性有這樣的描述:
從上面的描述可以明確計算屬性的需求,計算屬性計算的是響應式資料(滿足描述1),且計算結果應當被快取(滿足描述2)。讓我們一個一個來實現,先使用computed
建立一個計算屬性。
function effect(fn) { // 副作用函數 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)處,我們簡單寫了一個計算屬性的功能,為了實現通過sumRes.value
讀取計算屬性值功能,在實現計算屬性時,需要返回一個物件,通過物件內的get
觸發副作用函數。
function computed(getter) { const effectFn = effect(getter) const obj = { get value() { return effectFn() } } return obj }
但這個函數顯然是無法執行的,這是因為前面我們在實現effect
時,需要直接執行副作用函數,不需要提供返回值。沒有返回值,computed
自然無法獲取到effect
的執行結果。因此,當在計算屬性中使用effect
時,需要將副作用函數返回給計算屬性,由計算屬性決定何時執行,而不再由effect
立即執行(即懶執行)。
為了實現這點,就需要向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
開關,不需要懶執行的副作用函數同樣會自動執行。在(1)(2)(5)處返回了副作用函數的結果,供懶執行使用。同時在(3)處向下傳遞了options
,保證在effect
發生巢狀時,也使得副作用函數執行預期的行為。基於上述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)
從上圖中可以看出,我們已經實現了描述1,即使用計算屬性進行響應式資料的計算,當響應式資料的值發生變化時,計算屬性的值也會隨之改變。但觀察上文程式碼的(6)處,不難發現,無論什麼情況下,只要讀取sumRes.value
的值,就會觸發一次副作用函數,使其重新進行可能不必要的執行。所以接著,我們嘗試實現描述2,快取計算屬性的結果。
先從最簡單的入手,我們用一個變數value
來快取上次計算的值,並新增一個dirty
開關,記錄是否需要重新觸發副作用函數。
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 }
修改之後,快取的值就能生效了。但這樣做產生了一個明顯的BUG,當dirty
的值被置為false
時,無法再變為true
,這也就意味著,無論響應式資料obj.bar
與obj.foo
如何變化,計算屬性的值永遠都只能是快取的值value
,如下圖所示。
為了解決這個問題,我們需要一種方式,能夠在obj.bar
或obj.foo
的值變化時,在獲取sumRes.value
之前,將dirty
開關的值置為true
。受前面懶載入的啟發,我們嘗試能不能通過設定options
來實現這個功能。
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 } })
再來回憶一下響應式物件的整個流程,當響應式物件中的資料被修改時,執行了trigger
去觸發收集的副作用函數。而在計算屬性中,我們不再需要自動的觸發副作用函數。所以自然會想到,能否在這個地方將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)處增加了一個判斷,如果副作用函數fn
的設定項options
中含有scheduler
函數,我們就執行scheduler
而非副作用函數fn
。我們稱這裡的scheduler
為排程器。相應的,我們在計算屬性中新增排程器。
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)處我們新增了排程器。新增排程器後,讓我們再來串一下整個流程,當響應式資料被修改時,才會執行trigger
函數。由於我們傳入了排程器,因此trigger
函數在執行時不再觸發副作用函數,轉而執行排程器,此時dirty
開關的值變為了true
。當程式再往下執行時,由於dirty
已經變為true
,副作用函數就可以正常被手動觸發。效果如下圖所示。
到這裡為止,計算屬性在功能上已經實現完畢了,讓我們嘗試完善它。在Vue中,當計算屬性中的響應式資料被修改時,計算屬性值會同步更改,進而重新渲染,並在頁面上更新。渲染函數也會執行effect
,為了說明問題,讓我們使用effect
簡單的模擬一下。
const sumRes = computed(() => obj.foo + obj.bar) effect(() => console.log('sumRes =', sumRes.value))
這裡我們的預期是當obj.foo
或obj.bar
改變時,effect
會自動重新執行。這裡存在的問題是,正常的effect
巢狀可以被自動觸發(這點我們在上一篇部落格中已經實現了),但sumRes
包含的effect
僅會在被讀取value
時才會被get
觸發執行,這就導致響應式資料obj.foo
與obj.bar
無法收集到外部的effect
,收集不到的副作用函數,自然無法被自動觸發。
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)處我們手動收集了副作用函數,並當響應式資料被修改時,觸發它們。
在設計排程器時,我們忽略了一個潛在的問題。
還是先來看一個例子:
effect(() => console.log(obj.foo)) for(let i = 0; i < 1e5; i++) { obj.foo++ }
隨著響應式資料obj.foo
被不停修改,副作用函數也被不斷重複執行,在明顯的延遲之後,控制檯列印出了大量的資料。但在前端的實際開發中,我們往往只關心最終結果,拿到結果顯示在頁面上。在這種條件下,我們如何避免副作用函數被重複執行呢?
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 }) }
這裡我們的思路是使用微任務佇列來進行優化。(11)處我們定義了一個Set
作為任務佇列,(12)處我們定義了一個Promise
來使用微任務。精彩的部分來了,我們的思路是將副作用函數放入任務佇列中,由於任務佇列是基於Set
實現的,因此,重複的副作用函數僅會保留一個,這是第一點。接著,我們執行flushJob()
,這裡我們巧妙的設定了一個isFlushing
開關,這個開關保證了被微任務包裹的任務佇列在一次事件迴圈中只會執行一次,而執行的這一次會在宏任務完成之後觸發任務佇列中所有不重複的副作用函數,從而直接拿到最終結果,這是第二點。按照這個思路,我們在effect
中新增排程器。
effect(() => { console.log(obj.foo) }, { scheduler(fn) { jobQueue.add(fn) flushJob() } })
需要注意的是,瀏覽器在執行微任務時,會把微任務佇列中的所有微任務一口氣做完。因此,微任務在執行時會阻塞頁面的渲染。所以在實際使用時,要注意避免在微任務佇列中放置過重的任務,以免引起卡頓。
到此這篇關於Vue3計算屬性是如何實現的的文章就介紹到這了,更多相關Vue3計算屬性 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45