Vue3源碼解析計劃之計算屬性為什么比普通函數(shù)妙?
本文轉(zhuǎn)載自微信公眾號「前端萬有引力」,作者 一川 。轉(zhuǎn)載本文請聯(lián)系前端萬有引力公眾號。
寫在前面
計算屬性是Vue開發(fā)中一個非常實用的API,它允許用戶自定義一個計算方法,然后根據(jù)一些依賴的響應(yīng)式數(shù)據(jù)計算出新值并返回。當(dāng)依賴發(fā)生變化時,計算屬性會自動重新計算獲取新值,使用方便。我們看出計算屬性本質(zhì)上是對依賴的計算,為什么不直接使用函數(shù)呢?在Vue3中的計算屬性又是如何實現(xiàn)的呢?
計算屬性 computed
我們先簡單看個例子,我們看到再設(shè)置了計算屬性addOne后,直接改變addOne.value的值會報錯,只能通過改變原始值count.value才不會報錯。這是因為:
- 如果傳遞給computed的是一個函數(shù),那就是一個getter函數(shù),只能獲取它的值,而不能直接修改它
- 在getter函數(shù)中,根據(jù)響應(yīng)式對象重新計算出新值,叫做計算屬性,這個響應(yīng)式對象叫做計算屬性的依賴
- const count = ref(1);
- const addOne = computed(()=>count.value+1);
- console.log(addOne.value);//2
- addOne.value++;//error
- count.value++;
- console.log(count.value);//3
那么,我們應(yīng)該如何修改addOne.value值呢?那就是在computed中設(shè)置set函數(shù),進行自定義修改值。
- const count = ref(1);
- const addOne = computed({
- get:()=>count.value+1,
- set:val=>count.value=val-1
- });
- addOne.value = 1;
- console.log(count.value);//0
我們研究源碼:
- export function computed<T>(
- getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
- ) {
- let getter: ComputedGetter<T>
- let setter: ComputedSetter<T>
- // 如果傳入是 function 說明是只讀 computed
- if (isFunction(getterOrOptions)) {
- getter = getterOrOptions
- setter = __DEV__
- ? () => {
- console.warn('Write operation failed: computed value is readonly')
- }
- : NOOP
- } else {
- // 不是方法說明是自定義的 getter setter
- getter = getterOrOptions.get
- setter = getterOrOptions.set
- }
- let dirty = true
- let value: T
- let computed: ComputedRef<T>
- // 創(chuàng)建 effect, 我們在看 effect 源碼時知道了傳入 lazy 代表不會立即執(zhí)行,computed 表明 computed 上游依賴改變的時候,會優(yōu)先 trigger runner effect, scheduler 表示 effect trigger 的時候會調(diào)用 scheduler 而不是直接調(diào)用 effect
- const runner = effect(getter, {
- lazy: true,
- // mark effect as computed so that it gets priority during trigger
- computed: true,
- scheduler: () => {
- // 在觸發(fā)更新時把dirty置為true, 不會立即更新
- if (!dirty) {
- dirty = true
- trigger(computed, TriggerOpTypes.SET, 'value')
- }
- }
- })
- // 構(gòu)造一個 computed 返回
- computed = {
- __v_isRef: true,
- // expose effect so computed can be stopped
- effect: runner,
- get value() {
- // dirty為ture, get操作時,執(zhí)行effect獲取最新值
- //
- if (dirty) {
- value = runner()
- dirty = false
- }
- // dirty為false, 表示值未更新,直接返回
- track(computed, TrackOpTypes.GET, 'value')
- return value
- },
- set value(newValue: T) {
- setter(newValue)
- }
- } as any
- return computed
- }
computed計算屬性有兩個特點:
- 延時計算:只有當(dāng)我們訪問計算屬性時,真正運行computed getter函數(shù)計算
- 緩存:它的內(nèi)部會緩存上次的計算結(jié)果value,而只有dirty為true時才會重新計算,如果訪問計算屬性時dirty為false,那么直接返回這個value
那么,計算屬性的優(yōu)勢是:只要依賴不變化,就可以使用緩存的value而不用每次再渲染組件的時候都執(zhí)行函數(shù)去計算。
做個嵌套計算的小例子,我們看到:對于addOne而言,它收集的依賴是組件副作用渲染函數(shù),而對于count而言,它收集的依賴是addTwo內(nèi)部的runner函數(shù)。當(dāng)我們修改count值,會進行派發(fā)通知,先運行addOne中的setter函數(shù),此時addOne中的dirty值變成true,然后trigger函數(shù)再次派發(fā)通知;接著運行addTwo中的setter函數(shù),此時把addTwo中的dirty值設(shè)置為true;當(dāng)我們再次訪問addTwo中的值時,發(fā)現(xiàn)dirty值為true,就會執(zhí)行addTwo的computed函數(shù),會先去執(zhí)行addOne.value + 1,再去執(zhí)行addOne的computed函數(shù)中的count.value + 1。這樣就得到最后打印出來的值為2。
- const count = ref(0);
- const addOne = computed(()=>{
- return count.value + 1;//1
- })
- const addTwo = computed(()=>{
- return addOne.value + 1;//2
- })
- console.log(addTwo.value);//2
得益于computed計算屬性的巧妙設(shè)計,無論嵌套多少層都能夠正常運行。
- import {ref,computed} from "vue";
- import {effect} from "@vue/reactivity";
- const count = ref(0);
- const addOne = computed(()=>{
- return count.value + 1;
- })
- effect(()=>console.log(addOne.value+count.value))
- function add(){
- count.value++;
- }
- add();
我們看到上面代碼最終輸出結(jié)果是:1 3 3
當(dāng)我們第一次執(zhí)行addOne的computed計算屬性時,count.value值還是0,而addOne.value的值為1,將會觸發(fā)并執(zhí)行effect,此時打印出來還是1。而后執(zhí)行add()函數(shù),會進行修改count.value的值,會觸發(fā)并執(zhí)行effect函數(shù),因為addOne也是effect的依賴,addOne的runners函數(shù)也是count.value的依賴,count.value值的修改會執(zhí)行runners函數(shù),會再次執(zhí)行addOne的依賴,接著會觸發(fā)effect函數(shù),因此會輸出兩次3。
computed函數(shù)返回的對象實際上劫持的是value屬性的getter和setter,但是為什么我們在組件的模板中訪問一個計算屬性變量,不用手動在后面加.value呢?
參考文章
- 《Vue3核心源碼解析》
- 《Vue中文社區(qū)》
- 《Vue3中文文檔》
寫在最后
理解計算屬性的工作機制,能夠搞明白計算屬性嵌套場景代碼的執(zhí)行順序,知道計算屬性的兩個特點--延時計算和緩存,在組件的開發(fā)中合理使用計算屬性。