在本篇我们将修复一个小 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 fullName
和 set fullName
这两个获取/设置fullName
值的函数就是对象访问器。这是纯 JavaScript,不是 Vue 的特性。
2. 通过 Object Accessors 定义 Ref
在对象访问器内配合使用我们的track
和trigger
操作,我们可以这样定义 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 源码
网友评论