Vue 3 响应式原理三 - activeEffect & re

2021-08-25  本文已影响0人  AizawaSayo

在本篇我们将修复一个小 bug 来继续构建我们的响应式代码,然后实现响应式引用。

继续之前的代码:

...
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

// 添加了一段获取响应式对象的属性的代码
console.log('Updated quantity to = ' + product.quantity)
console.log(total)

当我们从响应式对象中获取属性时,问题就出现了:

在新增的console.log访问product.quantity时,track及它里面的所有方法都会被调用,即使这段代码不在effect(就是我们常说的副作用)中。我们只想查找并记录 内部调用了get property (访问属性) 的活跃 effect

activeEffect

为了解决这个问题,我们首先创建一个activeEffect全局变量,用于存储当前运行的effect。然后我们将在一个名为effect的新函数中设置它。

let activeEffect = null // 运行的 active effect
...
function effect(eff) {
  activeEffect = eff  // 把要运行的匿名函数赋给 activeEffect
  activeEffect()      // 运行它
  activeEffect = null // 再把 activeEffect 设置为 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}`)

现在我们不再需要手动调用 effect。它会在我们新的effect函数中自动调用。我们还添加了第二个effect,然后用console.log测试来验证输出。你可以从 GitHub 上获取并尝试所有代码:vue-3-reactivity

到目前为止一切顺利,但我们还需要做一项更改,那就是在track函数中使用我们新的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
  }
}

现在运行我们的代码会输出:

Ref

我们发现使用salePrice而不是price来计算总数应该更准确,于是把第一个effect修改如下:

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

如果我们正在创建一个真实的 store,我们可能会根据salePrice来计算 total。然而,这句代码不会响应式工作。当product.price更新时,它会响应式地重新计算salePrice,因为有这个副作用:

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

但是由于salePrice不是响应式的,所以它的变更不会重新计算 total的影响。我们上面的第一个副作用不会重新运行。我们需要一些方法来使salePrice具有响应性,如果你熟悉 Composition API,你可能认为应该使用ref来创建一个响应式引用,那就这样做吧:

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

根据 Vue 文档,响应性引用采用内部值并返回一个具有响应性和可维护的ref对象。ref对象有一个指向内部值的属性.value。所以我们需要稍微修改一下我们的effect

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

我们的代码现在应该起效了,当salePrice更新时能正确更新total。但是我们仍然需要通过ref定义。这个ref又是怎么实现的呢?我们有两种方式。

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这两个获取/设置fullName值的函数就是对象访问器。这是纯 JavaScript,不是 Vue 的特性。

2. 通过 Object Accessors 定义 Ref

在对象访问器内配合使用我们的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设计的初衷就是为包装一个内部值而服务,如果用reactive包裹的方式封装它,这样的“ref”就允许额外添加属性,违背了最初的目的。所以ref不应该被当作一个reactive对象。另外还有出于性能的考虑,用对象字面量创建ref会更节省性能。

当我们运行下面👇的代码:

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现在是响应式的了,total在它更新时也同步更新了。

Vue 3 响应式原理一 - Vue 3 Reactivity
Vue 3 响应式原理二 - Proxy and Reflect
Vue 3 响应式原理三 - activeEffect & ref
Vue 3 响应式原理四 - Computed Values & Vue 3 源码

上一篇 下一篇

猜你喜欢

热点阅读