大家好,我是若川。持續組織了8個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列
前言
在 Vue 中,computed 是一個非常好用的 API,用于處理派生狀態,又叫“計算屬性”。網上將其用于性能優化的場景比比皆是。
但它也有嚴重影響性能的一面,本文主要是聊聊這種場景。
聊之前,我們先看看它為什么能夠做到性能優化。
computed 的兩個特點
緩存結果:只有依賴項變化的時候才會重新計算,否則復用上一次計算的結果。
惰性求值:只有在真正讀取它的 value 時,才會進行計算求值。
緩存結果
const?todos?=?reactive([{?title:?'點贊',?done:?true},{?title:?'關注',?done:?false?}
])const?openTodos?=?computed(()?=>?todos.filter(todo?=>?!todo.done)
)
在上圖的例子中,只要 todos 不變,多次使用 openTodos 將返回相同的值,不會重新計算。
當 todos 變化時,openTodos 會被標記為 dirty
,下次取值時才會進行重新計算。
這點對計算量開銷較大的場景非常有用,確保了只有在必要時才會重新計算。
惰性求值
只有在使用 computed 時,它才會進行計算。如果一個計算屬性,計算開銷非常非常大,但它沒有被任何地方使用,也不會進行求值。
computed 提升性能的場景
如上所說,computed 的延遲計算通常是一件好事:它確保了必要時才會進行計算。
下面是一個簡單的 Todo 例子,簡單說明一下:
有一個 todos 列表
showList
用于控制是否展示列表openTodos
是計算未完成 todos 列表
的計算屬性,依賴 todoshasOpenTodos
是計算是否有未完成 todos
的計算屬性,依賴 openTodos
<template><button?@click="showList?=?!showList">點擊展示/隱藏</button><template?v-if="showList"><template?v-if="hasOpenTodos"><h2>{{?openTodos.length?}}?Todos:</h2>?<ul><li?v-for="todo?in?openTodos">{{?todo.title?}}</li></ul></template><span?v-else>沒有任何?Todo</span><input?type="text"?v-model="newTodo"><button?type="button"?v-on:click="addTodo">添加?Todo</button></template>
</template><script?setup>
const?showList?=?ref(false)const?todos?=?reactive([{?title:?'點贊',?done:?true},{?title:?'關注',?done:?false?}
])
const?openTodos?=?computed(()?=>?todos.filter(todo?=>?!todo.done)
)
const?hasOpenTodos?=?computed(()?=>?!!openTodos.value.length
)const?newTodo?=?ref('')
function?addTodo()?{todos.push({title:?newTodo.valuedone:?false,})
}
</script>
因為 showList 最初是 false,所以模板中不會讀取 openTodos,不會產生計算。并且無論是一開始還是添加了新的 todo。只有在 showList 設置為 true 之后,模板中才會讀取 openTodos,這才會觸發相應的計算。
這對于開銷大的計算屬性來說,是有很大好處的。
computed 影響性能的場景
惰性求值也會帶來一個缺點:計算屬性的返回結果,只有在對它進行計算后才會知道。
聽起來可能比較難以理解,同樣用一個例子來說明:
有一個 list 列表
一個增加 count 的按鈕
一旦 count 超過 100(
isOver100
),就反向展示 list
當然這里反向展示 list計算量并不大。我們將它想象成一次開銷很大的計算
<template><button?@click="increase">添加計數</button><h3>列表</h3><ul><li?v-for="item?in?sortedList">{{?item?}}</li></ul><>
</template><script?setup>
import?{?ref,?reactive,?computed,?onUpdated?}?from?'vue'const?list?=?reactive([1,2,3,4,5])const?count?=?ref(0)
function?increase()?{count.value++
}const?isOver100?=?computed(()?=>?count.value?>?100)const?sortedList?=?computed(()?=>?{//?這里比較簡單,可以將它想象成開銷大的計算return?isOver100.value???[...list].reverse()?:?[...list]
})onUpdated(()?=>?{//?組件重新渲染時觸發console.log('count?is',?count.value)console.log('component?re-rendered!')
})
</script>
預期情況:
當我們點擊 1-100 次時,因為 count 小于 100,list 不會反向展示,組件不會重新渲染
當點擊第 101 次時, list 才會反向展示,組件這時候才重新渲染
因此,預期總計重新渲染 1 次。
但運行結果告訴我們,組件會重新渲染 101 次!!
讓我們一步一步來看發生了什么。
依賴關系如圖:
點擊按鈕,計數增加。由于模板中沒有使用 count,理論上不會重新渲染。
但 count 改變后,依賴 count 的計算屬性
isOver100
被標記為dirty
,在下次使用時需要重新計算,并告知了它的訂閱者。因為 sortedList 依賴 isOver100,收到 isOver100 的通知后,它也被標記為
dirty
,在下次使用時需要重新計算,并告知了它的訂閱者。最終模板中使用了 sortedList,所以收到 sortedList 的更新通知后,組件重新渲染了。
重新渲染時,計算 sortedList,接著計算 isOver100,但現在由于 count 不到 100,isOver100 仍然返回 false。
最終 sortedList 計算結果與原來一致,重新渲染后 DOM 無任何改變,但是我們卻運行了多次大開銷的 sortedList 計算!
簡單來說:因為 count 變了,所以 isOver100
“覺得”自己變了,需要重新算(但其實沒有),就讓依賴它的 sortedList
重新計算了。
根本原因就是 isOver100
,它是一個頻繁計算且計算非常簡單的 computed,多次計算返回值也與之前相同(都為 false)。它只發揮了 computed 狀態派生的作用。
但在計算開銷大的 sortedList
中,依賴了廉價的 isOver100
,因為 computed 是惰性求值的,isOver100 的計算結果只能在渲染時重新計算才會知道,所以 sortedList 也只能在渲染時等待它的計算結果再重新計算,哪怕最終結果一致。導致觸發了不必要的重新渲染,用的不好會嚴重影響性能。
所以影響性能的 computed,通常都有這樣的特征:
一個計算簡單的 computed,頻繁觸發計算,并且返回值通常變化不大,比如上面的
boolean
類型另一個計算開銷大的 computed,依賴這個廉價的 computed
如何解決
我們發現根因是由于 computed 的惰性求值,讓 isOver100 "覺得"自己變了,需要重新計算,導致了這一系列的連鎖反應。但因為它的計算是廉價的,頻繁計算也不會影響性能。
有沒有辦法不要 computed 的延遲計算呢?在 isOver100 "覺得"自己變了的時候馬上就能知道是不是真的變了。在發現自己其實沒變后,不再通知訂閱者,也就沒有了后續的重新渲染。
我們可以將它的計算提前,在依賴變化時就立刻計算得到結果。
設計一種立刻求值的 eagerComputed
:
Vue 的響應式系統為我們提供了構建自定義 computed 所必要的工具。手動點贊👍
如果項目已經引入了 vueuse,可直接使用
import?{?watchEffect,?shallowRef,?readonly?}?from?'vue'export?function?eagerComputed(fn)?{const?result?=?shallowRef()watchEffect(()?=>?{result.value?=?fn()},?{flush:?'sync'?//?使用同步觀察器,依賴變化時立刻求值})return?readonly(result)
}
eagerComputed
與 computed 用法一致,只是行為上不同,在依賴變化時,它會立刻進行求值。
什么時候使用 computed 和 eagerComputed
?
復雜的計算使用 computed,可以受益于緩存結果和惰性求值。
簡單的計算使用
eagerComputed
,因為每次依賴項變化時它都會重新計算。
回到上面的例子中,我們將 isOver100 改為 eagerComputed
:
const?isOver100?=?eagerComputed(()?=>?count.value?>?100);
現在,當點擊按鈕 101 次時,組件才會重新渲染。
我們打個接地氣的比方:
比方 | 代碼 |
---|---|
大家吃飯后都會“覺得”自己體重漲了 | isOver100 "覺得"自己變了 |
為了驗證漲沒漲,需要下樓去稱體重,因為家里沒有 | 要讓 sortedList 重新計算 |
而下樓是個麻煩的事情 | sortedList 的計算開銷很大 |
稱完發現并沒漲,白跑一趟 | isOver100 其實沒變 |
為了不每次都白跑一趟,我們可以在家里買個秤 | 使用 eagerComputed ,變沒變馬上就知道 |
結語
我們深入了解了 computed 的工作原理與兩大特性:緩存結果和惰性求值。掌握了什么場景會優化性能,什么場景會影響性能,對于影響性能的場景,可以使用 eagerComputed
避免不必要的響應式更新來解決性能問題。
參考
https://dev.to/linusborg/vue-when-a-computed-property-can-be-the-wrong-tool-195j
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~