溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue3計算屬性是怎么實現(xiàn)的

發(fā)布時間:2022-04-14 09:03:23 來源:億速云 閱讀:292 作者:iii 欄目:編程語言

今天小編給大家分享一下Vue3計算屬性是怎么實現(xiàn)的的相關(guān)知識點,內(nèi)容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

Vue3計算屬性是怎么實現(xiàn)的

計算屬性是 Vue.js 開發(fā)中一個非常實用的 API ,它允許用戶定義一個計算方法,然后根據(jù)一些依賴的響應(yīng)式數(shù)據(jù)計算出新值并返回。當(dāng)依賴發(fā)生變化時,計算屬性可以自動重新計算獲取新值,所以使用起來非常方便。

在 Vue.js 2.x 中,相信你對計算屬性的應(yīng)用已經(jīng)如數(shù)家珍了,我們可以在組件對象中定義 computed 屬性。到了 Vue.js 3.0 ,雖然也可以在組件中沿用 Vue.js 2.x 的使用方式,但是我們也可以單獨使用計算屬性 API。

計算屬性本質(zhì)上還是對依賴的計算,那么為什么我們不直接用函數(shù)呢?在 Vue.js 3.0 中計算屬性的 API 又是如何實現(xiàn)呢?本文就來分析下計算屬性的實現(xiàn)原理。

計算屬性API:computed

Vue.js 3.0 提供了一個 computed 函數(shù)作為計算屬性 API,我們先來看看它是如何使用的。

我們舉個簡單的例子:

const count = ref(1) 
const plusOne = computed(() => count.value + 1) 
console.log(plusOne.value) // 2 
plusOne.value++ // error 
count.value++ 
console.log(plusOne.value) // 3

從代碼中可以看到,我們先使用 ref API 創(chuàng)建了一個響應(yīng)式對象 count,然后使用 computed API 創(chuàng)建了另一個響應(yīng)式對象 plusOne,它的值是 count.value + 1,當(dāng)我們修改 count.value 的時候, plusOne.value 就會自動發(fā)生變化。

注意,這里我們直接修改 plusOne.value 會報一個錯誤,這是因為如果我們傳遞給 computed 的是一個函數(shù),那么這就是一個 getter 函數(shù),我們只能獲取它的值,而不能直接修改它。

在 getter 函數(shù)中,我們會根據(jù)響應(yīng)式對象重新計算出新的值,這也就是它被叫做計算屬性的原因,而這個響應(yīng)式對象,就是計算屬性的依賴。

當(dāng)然,有時候我們也希望能夠直接修改 computed 的返回值,那么我們可以給 computed 傳入一個對象:

const count = ref(1) 
const plusOne = computed({ 
  get: () => count.value + 1, 
  set: val => { 
    count.value = val - 1 
  } 
}) 
plusOne.value = 1 
console.log(count.value) // 0

在這個例子中,結(jié)合上述代碼可以看到,我們給 computed 函數(shù)傳入了一個擁有 getter 函數(shù)和 setter 函數(shù)的對象,getter 函數(shù)和之前一樣,還是返回 count.value + 1;而 setter 函數(shù),請注意,這里我們修改 plusOne.value 的值就會觸發(fā) setter 函數(shù),其實 setter 函數(shù)內(nèi)部實際上會根據(jù)傳入的參數(shù)修改計算屬性的依賴值 count.value,因為一旦依賴的值被修改了,我們再去獲取計算屬性就會重新執(zhí)行一遍 getter,所以這樣獲取的值也就發(fā)生了變化。

好了,我們現(xiàn)在已經(jīng)知道了 computed API 的兩種使用方式了,接下來就看看它是怎樣實現(xiàn)的:

function computed(getterOrOptions) { 
  // getter 函數(shù) 
  let getter 
  // setter 函數(shù) 
  let setter 
  // 標(biāo)準(zhǔn)化參數(shù) 
  if (isFunction(getterOrOptions)) { 
    // 表面?zhèn)魅氲氖?nbsp;getter 函數(shù),不能修改計算屬性的值 
    getter = getterOrOptions 
    setter = (process.env.NODE_ENV !== 'production') 
      ? () => { 
        console.warn('Write operation failed: computed value is readonly') 
      } 
      : NOOP 
  } 
  else { 
    getter = getterOrOptions.get 
    setter = getterOrOptions.set 
  } 
  // 數(shù)據(jù)是否臟的 
  let dirty = true 
  // 計算結(jié)果 
  let value 
  let computed 
  // 創(chuàng)建副作用函數(shù) 
  const runner = effect(getter, { 
    // 延時執(zhí)行 
    lazy: true, 
    // 標(biāo)記這是一個 computed effect 用于在 trigger 階段的優(yōu)先級排序 
    computed: true, 
    // 調(diào)度執(zhí)行的實現(xiàn) 
    scheduler: () => { 
      if (!dirty) { 
        dirty = true 
        // 派發(fā)通知,通知運行訪問該計算屬性的 activeEffect 
        trigger(computed, "set" /* SET */, 'value') 
      } 
    } 
    }) 
  // 創(chuàng)建 computed 對象 
  computed = { 
    __v_isRef: true, 
    // 暴露 effect 對象以便計算屬性可以停止計算 
    effect: runner, 
    get value() { 
      // 計算屬性的 getter 
      if (dirty) { 
        // 只有數(shù)據(jù)為臟的時候才會重新計算 
        value = runner() 
        dirty = false 
      } 
      // 依賴收集,收集運行訪問該計算屬性的 activeEffect 
      track(computed, "get" /* GET */, 'value') 
      return value 
    }, 
    set value(newValue) { 
      // 計算屬性的 setter 
      setter(newValue) 
    } 
  } 
  return computed 
}

從代碼中可以看到,computed 函數(shù)的流程主要做了三件事情:標(biāo)準(zhǔn)化參數(shù),創(chuàng)建副作用函數(shù)創(chuàng)建 computed 對象。 我們來詳細分析一下這幾個步驟。

首先是標(biāo)準(zhǔn)化參數(shù)。computed 函數(shù)接受兩種類型的參數(shù),一個是 getter 函數(shù),一個是擁有 getter 和 setter 函數(shù)的對象,通過判斷參數(shù)的類型,我們初始化了函數(shù)內(nèi)部定義的 getter 和 setter 函數(shù)。

接著是創(chuàng)建副作用函數(shù) runner。computed 內(nèi)部通過 effect 創(chuàng)建了一個副作用函數(shù),它是對 getter 函數(shù)做的一層封裝,另外我們這里要注意第二個參數(shù),也就是 effect 函數(shù)的配置對象。其中 lazy 為 true 表示 effect 函數(shù)返回的 runner 并不會立即執(zhí)行;computed 為 true 用于表示這是一個 computed effect,用于 trigger 階段的優(yōu)先級排序,我們稍后會分析;scheduler 表示它的調(diào)度運行的方式,我們也稍后分析。

最后是創(chuàng)建 computed 對象并返回,這個對象也擁有 getter 和 setter 函數(shù)。當(dāng) computed 對象被訪問的時候會觸發(fā) getter,然后會判斷是否 dirty,如果是就執(zhí)行 runner,然后做依賴收集;當(dāng)我們直接設(shè)置 computed 對象時會觸發(fā) setter,即執(zhí)行 computed 函數(shù)內(nèi)部定義的 setter 函數(shù)。

計算屬性的運行機制

computed 函數(shù)的邏輯會有一點繞,不過不要緊,我們可以結(jié)合一個應(yīng)用 computed 計算屬性的例子,來理解整個計算屬性的運行機制。分析之前我們需要記住 computed 內(nèi)部兩個重要的變量,第一個 dirty 表示一個計算屬性的值是否是“臟的”,用來判斷需不需要重新計算,第二個 value 表示計算屬性每次計算后的結(jié)果。

現(xiàn)在,我們來看這個示例:

<template> 
  <div> 
    {{ plusOne }} 
  </div> 
  <button @click="plus">plus</button> 
</template> 
<script> 
  import { ref, computed } from 'vue' 
  export default { 
    setup() { 
      const count = ref(0) 
      const plusOne = computed(() => { 
        return count.value + 1 
      }) 

      function plus() { 
        count.value++ 
      } 
      return { 
        plusOne, 
        plus 
      } 
    } 
  } 
</script>

可以看到,在這個例子中我們利用 computed API 創(chuàng)建了計算屬性對象 plusOne,它傳入的是一個 getter 函數(shù),為了和后面計算屬性對象的 getter 函數(shù)區(qū)分,我們把它稱作 computed getter。另外,組件模板中引用了 plusOne 變量和 plus 函數(shù)。

組件渲染階段會訪問 plusOne,也就觸發(fā)了 plusOne 對象的 getter 函數(shù):

get value() { 
  // 計算屬性的 getter 
  if (dirty) { 
    // 只有數(shù)據(jù)為臟的時候才會重新計算 
    value = runner() 
    dirty = false 
  } 
  // 依賴收集,收集運行訪問該計算屬性的 activeEffect 
  track(computed, "get" /* GET */, 'value') 
  return value 
}

由于默認(rèn) dirty 是 true,所以這個時候會執(zhí)行 runner 函數(shù),并進一步執(zhí)行 computed getter,也就是 count.value + 1,因為訪問了 count 的值,并且由于 count 也是一個響應(yīng)式對象,所以就會觸發(fā) count 對象的依賴收集過程。

請注意,由于是在 runner 執(zhí)行的時候訪問 count,所以這個時候的 activeEffect 是 runner 函數(shù)。runner 函數(shù)執(zhí)行完畢,會把 dirty 設(shè)置為 false,并進一步執(zhí)行 track(computed,"get",'value') 函數(shù)做依賴收集,這個時候 runner 已經(jīng)執(zhí)行完了,所以 activeEffect 是組件副作用渲染函數(shù)。

所以你要特別注意這是兩個依賴收集過程:對于 plusOne 來說,它收集的依賴是組件副作用渲染函數(shù);對于 count 來說,它收集的依賴是 plusOne 內(nèi)部的 runner 函數(shù)。

然后當(dāng)我們點擊按鈕的時候,會執(zhí)行 plus 函數(shù),函數(shù)內(nèi)部通過 count.value++ 修改 count 的值,并派發(fā)通知。請注意,這里不是直接調(diào)用 runner 函數(shù),而是把 runner 作為參數(shù)去執(zhí)行 scheduler 函數(shù)。我們來回顧一下 trigger 函數(shù)內(nèi)部對于 effect 函數(shù)的執(zhí)行方式:

const run = (effect) => { 
  // 調(diào)度執(zhí)行 
  if (effect.options.scheduler) { 
    effect.options.scheduler(effect) 
  } 
  else { 
    // 直接運行 
    effect() 
  } 
}

computed API 內(nèi)部創(chuàng)建副作用函數(shù)時,已經(jīng)配置了 scheduler 函數(shù),如下:

scheduler: () => { 
  if (!dirty) { 
    dirty = true 
    // 派發(fā)通知,通知運行訪問該計算屬性的 activeEffect 
    trigger(computed, "set" /* SET */, 'value') 
  } 
}

它并沒有對計算屬性求新值,而僅僅是把 dirty 設(shè)置為 true,再執(zhí)行 trigger(computed, "set" , 'value'),去通知執(zhí)行 plusOne 依賴的組件渲染副作用函數(shù),即觸發(fā)組件的重新渲染。

在組件重新渲染的時候,會再次訪問 plusOne,我們發(fā)現(xiàn)這個時候 dirty 為 true,然后會再次執(zhí)行 computed getter,此時才會執(zhí)行 count.value + 1 求得新值。這就是雖然組件沒有直接訪問 count,但是當(dāng)我們修改 count 的值的時候,組件仍然會重新渲染的原因。

通過下圖可以直觀的展現(xiàn)上述過程:

Vue3計算屬性是怎么實現(xiàn)的

通過以上分析,我們可以看出 computed 計算屬性有兩個特點:

  • 延時計算,只有當(dāng)我們訪問計算屬性的時候,它才會真正運行 computed getter 函數(shù)計算;

  • 緩存,它的內(nèi)部會緩存上次的計算結(jié)果 value,而且只有 dirty 為 true 時才會重新計算。如果訪問計算屬性時 dirty 為 false,那么直接返回這個 value。

現(xiàn)在,我們就可以回答開頭提的問題了。和單純使用普通函數(shù)相比,計算屬性的優(yōu)勢是:只要依賴不變化,就可以使用緩存的 value 而不用每次在渲染組件的時候都執(zhí)行函數(shù)去計算,這是典型的空間換時間的優(yōu)化思想。

嵌套計算屬性

計算屬性也支持嵌套,我們可以針對上述例子做個小修改,即不在渲染函數(shù)中訪問 plusOne,而在另一個計算屬性中訪問:

const count = ref(0) 
const plusOne = computed(() => { 
  return count.value + 1 
}) 
const plusTwo = computed(() => { 
  return plusOne.value + 1 
}) 
console.log(plusTwo.value)

從代碼中可以看到,當(dāng)我們訪問 plusTwo 的時候,過程和前面都差不多,同樣也是兩個依賴收集的過程。對于 plusOne 來說,它收集的依賴是 plusTwo 內(nèi)部的 runner 函數(shù);對于 count 來說,它收集的依賴是 plusOne 內(nèi)部的 runner 函數(shù)。

接著當(dāng)我們修改 count 的值時,它會派發(fā)通知,先運行 plusOne 內(nèi)部的 scheduler 函數(shù),把 plusOne 內(nèi)部的 dirty 變?yōu)?true,然后執(zhí)行 trigger 函數(shù)再次派發(fā)通知,接著運行 plusTwo 內(nèi)部的 scheduler 函數(shù),把 plusTwo 內(nèi)部的 dirty 設(shè)置為 true。

然后當(dāng)我們再次訪問 plusTwo 的值時,發(fā)現(xiàn) dirty 為 true,就會執(zhí)行 plusTwo 的 computed getter 函數(shù)去執(zhí)行 plusOne.value + 1,進而執(zhí)行 plusOne 的 computed gette 即 count.value + 1 + 1,求得最終新值 2。

得益于 computed 這種巧妙的設(shè)計,無論嵌套多少層計算屬性都可以正常工作。

計算屬性的執(zhí)行順序

我們曾提到計算屬性內(nèi)部創(chuàng)建副作用函數(shù)的時候會配置 computed 為 true,標(biāo)識這是一個 computed effect,用于在 trigger 階段的優(yōu)先級排序。我們來回顧一下 trigger 函數(shù)執(zhí)行 effects 的過程:

const add = (effectsToAdd) => { 
  if (effectsToAdd) { 
    effectsToAdd.forEach(effect => { 
      if (effect !== activeEffect || !shouldTrack) { 
        if (effect.options.computed) { 
          computedRunners.add(effect) 
        } 
        else { 
          effects.add(effect) 
        } 
      } 
    }) 
  } 
} 
const run = (effect) => { 
  if (effect.options.scheduler) { 
    effect.options.scheduler(effect) 
  } 
  else { 
    effect() 
  } 
} 
computedRunners.forEach(run) 
effects.forEach(run)

在添加待運行的 effects 的時候,我們會判斷每一個 effect 是不是一個 computed effect,如果是的話會添加到 computedRunners 中,在后面運行的時候會優(yōu)先執(zhí)行 computedRunners,然后再執(zhí)行普通的 effects。

那么為什么要這么設(shè)計呢?其實是考慮到了一些特殊場景,我們通過一個示例來說明:

import { ref, computed } from 'vue' 
import { effect } from '@vue/reactivity' 
const count = ref(0) 
const plusOne = computed(() => { 
  return count.value + 1 
}) 
effect(() => { 
  console.log(plusOne.value + count.value) 
}) 
function plus() { 
  count.value++ 
} 
plus()

這個示例運行后的結(jié)果輸出:

1 
3 
3

在執(zhí)行 effect 函數(shù)時運行 console.log(plusOne.value + count.value),所以第一次輸出 1,此時 count.value 是 0,plusOne.value 是 1。

后面連續(xù)輸出兩次 3 是因為, plusOne 和 count 的依賴都是這個 effect 函數(shù),所以當(dāng)我們執(zhí)行 plus 函數(shù)修改 count 的值時,會觸發(fā)并執(zhí)行這個 effect 函數(shù),因為 plusOne 的 runner 也是 count 的依賴,count 值修改也會執(zhí)行 plusOne 的 runner,也就會再次執(zhí)行 plusOne 的依賴即 effect 函數(shù),因此會輸出兩次。

那么為什么兩次都輸出 3 呢?這就跟先執(zhí)行 computed runner 有關(guān)。首先,由于 plusOne 的 runner 和 effect 都是 count 的依賴,當(dāng)我們修改 count 值的時候, plusOne 的 runner 和 effect 都會執(zhí)行,那么此時執(zhí)行順序就很重要了。

這里先執(zhí)行 plusOne 的 runner,把 plusOne 的 dirty 設(shè)置為 true,然后通知它的依賴 effect 執(zhí)行 plusOne.value + count.value。這個時候,由于 dirty 為 true,就會再次執(zhí)行 plusOne 的 getter 計算新值,拿到了新值 2, 再加上 1 就得到 3。執(zhí)行完 plusOne 的 runner 以及依賴更新之后,再去執(zhí)行 count 的普通effect 依賴,從而去執(zhí)行 plusOne.value + count.value,這個時候 plusOne dirty 為 false, 直接返回上次的計算結(jié)果 2,然后再加 1 就又得到 3。

如果我們把 computed runner 和 effect 的執(zhí)行順序換一下會怎樣呢?我來告訴你,會輸出如下結(jié)果:

1 
2 
3

第一次輸出 1 很好理解,因為流程是一樣的。第二次為什么會輸出 2 呢?我們來分析一下,當(dāng)我們執(zhí)行 plus 函數(shù)修改 count 的值時,會觸發(fā) plusOne 的 runner 和 effect 的執(zhí)行,這一次我們先讓 effect 執(zhí)行 plusOne.value + count.value,那么就會訪問 plusOne.value,但由于 plusOne 的 runner 還沒執(zhí)行,所以此時 dirty 為 false,得到的值還是上一次的計算結(jié)果 1,然后再加 1 得到 2。

接著再執(zhí)行 plusOne 的 runner,把 plusOne 的 dirty 設(shè)置為 true,然后通知它的依賴 effect 執(zhí)行 plusOne.value + count.value,這個時候由于 dirty 為 true,就會再次執(zhí)行 plusOne 的 getter 計算新值,拿到了 2,然后再加上 1 就得到 3。

知道原因后,我們再回過頭看例子。因為 effect 函數(shù)依賴了 plusOne 和 count,所以 plusOne 先計算會更合理,這就是為什么我們需要讓 computed runner 的執(zhí)行優(yōu)先于普通的 effect 函數(shù)。

以上就是“Vue3計算屬性是怎么實現(xiàn)的”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI