Vue 3 響應(yīng)式原理三 - activeEffect & ref

在本篇我們將修復(fù)一個小 bug 來繼續(xù)構(gòu)建我們的響應(yīng)式代碼,然后實現(xiàn)響應(yīng)式引用。

繼續(xù)之前的代碼:

...
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
  total = product.price * product.quantity
}
effect() // 活躍 effect
console.log(total)
product.quantity = 3

// 添加了一段獲取響應(yīng)式對象的屬性的代碼
console.log('Updated quantity to = ' + product.quantity)
console.log(total)

當(dāng)我們從響應(yīng)式對象中獲取屬性時,問題就出現(xiàn)了:

在新增的console.log訪問product.quantity時,track及它里面的所有方法都會被調(diào)用,即使這段代碼不在effect(就是我們常說的副作用)中。我們只想查找并記錄 內(nèi)部調(diào)用了get property (訪問屬性) 的活躍 effect

activeEffect

為了解決這個問題,我們首先創(chuàng)建一個activeEffect全局變量,用于存儲當(dāng)前運行的effect。然后我們將在一個名為effect的新函數(shù)中設(shè)置它。

let activeEffect = null // 運行的 active effect
...
function effect(eff) {
  activeEffect = eff  // 把要運行的匿名函數(shù)賦給 activeEffect
  activeEffect()      // 運行它
  activeEffect = null // 再把 activeEffect 設(shè)置為 null
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
effect(() => {
  total = product.price * product.quantity
})
effect(() => {
  salePrice = product.price * 0.9
})
console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.quantity = 3
console.log(`After updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.price = 10
console.log(`After updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`)

現(xiàn)在我們不再需要手動調(diào)用 effect。它會在我們新的effect函數(shù)中自動調(diào)用。我們還添加了第二個effect,然后用console.log測試來驗證輸出。你可以從 GitHub 上獲取并嘗試所有代碼:vue-3-reactivity

到目前為止一切順利,但我們還需要做一項更改,那就是在track函數(shù)中使用我們新的activeEffect。

function track(target, key) {
  if (activeEffect) { // <------ Check to see if we have an activeEffect
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map())) 
    }
    let dep = depsMap.get(key) 
    if (!dep) {
      depsMap.set(key, (dep = new Set())) // Create a new Set
    }
    dep.add(activeEffect) // <----- Add activeEffect to dependency map
  }
}

現(xiàn)在運行我們的代碼會輸出:

Ref

我們發(fā)現(xiàn)使用salePrice而不是price來計算總數(shù)應(yīng)該更準(zhǔn)確,于是把第一個effect修改如下:

effect(() => {
  total = salePrice * product.quantity
})

如果我們正在創(chuàng)建一個真實的 store,我們可能會根據(jù)salePrice來計算 total。然而,這句代碼不會響應(yīng)式工作。當(dāng)product.price更新時,它會響應(yīng)式地重新計算salePrice,因為有這個副作用:

effect(() => {
  salePrice = product.price * 0.9
})

但是由于salePrice不是響應(yīng)式的,所以它的變更不會重新計算 total的影響。我們上面的第一個副作用不會重新運行。我們需要一些方法來使salePrice具有響應(yīng)性,如果你熟悉 Composition API,你可能認為應(yīng)該使用ref來創(chuàng)建一個響應(yīng)式引用,那就這樣做吧:

let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0

根據(jù) Vue 文檔,響應(yīng)性引用采用內(nèi)部值并返回一個具有響應(yīng)性和可維護的ref對象。ref對象有一個指向內(nèi)部值的屬性.value。所以我們需要稍微修改一下我們的effect。

effect(() => {
  total = salePrice.value * product.quantity
})
effect(() => {
  salePrice.value = product.price * 0.9
})

我們的代碼現(xiàn)在應(yīng)該起效了,當(dāng)salePrice更新時能正確更新total。但是我們?nèi)匀恍枰ㄟ^ref定義。這個ref又是怎么實現(xiàn)的呢?我們有兩種方式。

1. 通過 Reactive 定義 Ref

簡單地通過reactive包裝

function ref(intialValue) {
  return reactive({ value: initialValue })
}

然而,這不是 Vue 3 用真正原始定義 ref 的方式

理解 JavaScript Object Accessors - 對象訪問器

首先需要確保先熟悉對象訪問器(object accessors),有時也稱為 JavaScript 的 computed 屬性(不要和 Vue 的計算屬性混淆)。
下面??是 Object Accessors 的一個簡單示例:

let user = {
  firstName: 'Gregg',
  lastName: 'Pollack',
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  },
  set fullName(value) {
    [this.firstName, this.lastName] = value.split(' ')
  },
}
console.log(`Name is ${user.fullName}`)
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`)

get fullNameset fullName這兩個獲取/設(shè)置fullName值的函數(shù)就是對象訪問器。這是純 JavaScript,不是 Vue 的特性。

2. 通過 Object Accessors 定義 Ref

在對象訪問器內(nèi)配合使用我們的tracktrigger操作,我們可以這樣定義 ref:

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}

這就是全部了。

這樣做是因為:ref設(shè)計的初衷就是為包裝一個內(nèi)部值而服務(wù),如果用reactive包裹的方式封裝它,這樣的“ref”就允許額外添加屬性,違背了最初的目的。所以ref不應(yīng)該被當(dāng)作一個reactive對象。另外還有出于性能的考慮,用對象字面量創(chuàng)建ref會更節(jié)省性能。

當(dāng)我們運行下面??的代碼:

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
  total = salePrice.value * product.quantity
})
effect(() => {
  salePrice.value = product.price * 0.9
})
console.log(
  `Before updated quantity total (should be 9) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
  `After updated quantity total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
  `After updated price total (should be 27) = ${total} salePrice (should be 9) = ${salePrice.value}`
)

能夠得到我們所期望的:

Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated total (should be 27) = 27 salePrice (should be 9) = 9

salePrice現(xiàn)在是響應(yīng)式的了,total在它更新時也同步更新了。

Vue 3 響應(yīng)式原理一 - Vue 3 Reactivity
Vue 3 響應(yīng)式原理二 - Proxy and Reflect
Vue 3 響應(yīng)式原理三 - activeEffect & ref
Vue 3 響應(yīng)式原理四 - Computed Values & Vue 3 源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容