我们继续。在上一章的结尾中,我们的代码变成了大致这样子
const product = reactive({ price: 5, quantity: 2 })
let total = 0
function effect() {
total = product.price * product.quantity
}
effect()
console.log(`total is ${total}`) // total is 1
product.price = 10
console.log(`total is ${total}`) // total is 20
如果我们以另一种形式添加 get
依赖,比如 production.quantity
的时候,我们的代码将会调用追踪函数:
console.log('Update quantity to = ', product.quantity)
它将会触发 track(product, ‘quantity’) 函数,会遍历 targetMap,还有 deps,以确保当前的 effect
会被保存下来,但这不是我们想要的, 我们应该只在 effect
里调用 track
函数,所以我们需要修改一下代码以适配这个场景。为此,我们引入一个新的变量:activeEffect
,它用于保存当前正在执行的 effect
函数。
activeEffect
我们将重新创建一个 effect
函数,用它来实现这个逻辑。
let activeEffect = null
function effect(effectFn) {
activeEffect = effectFn
activeEffect()
activeEffect = null
}
同时将原本的 effect
函数更新:
- function effect() {
- total = product.price * product.quantity
- }
+ effect(() => {
+ total = product.price * product.quantity
+ })
当然,这项改动也意味着我们不再需要调用 effect
函数,因为它会在我们传递函数的时候调用。
+ let activeEffect = null
+ function effect(effectFn) {
+ activeEffect = effectFn
+ activeEffect()
+ activeEffect = null
+ }
const product = reactive({ price: 5, quantity: 2 })
let total = 0
- function effect() {
- total = product.price * product.quantity
- }
+ effect(() => {
+ total = product.price * product.quantity
+ })
- effect()
console.log(`total is ${total}`) // total is 1
product.price = 10
console.log(`total is ${total}`) // total is 20
但是我们现在需要更新追踪函数,那这个时候 activeEffect
变量就会派上用场了。回到 track()
函数上:
function track(target, key) {
let depsMap = targetMap.get(target)
// 如果这个对象没有被观察则将它添加到依赖列表中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 读取对象上的子属性
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(effect)
}
// 我们需要执行两个操作
// 1. 判断当前是否存在 activeEffect
// 2. 将 activeEffect 替换原本的 effect
function track(target, key) {
// 如果不存在正在执行的 activeEffect 则不收集此次访问的依赖
if (!activeEffect) return
let depsMap = targetMap.get(target)
// 如果这个对象没有被观察则将它添加到依赖列表中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 读取对象上的子属性
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 如果是以 effect 类型收集的响应式依赖则将它放置到依赖列表中
deps.add(activeEffect)
}
我们用一个比较高级一点的测试用例来验证这个过程。
const targetMap = new WeakMap()
const product = reactive({ price: 5, quantity: 2 })
let total = 0
let activeEffect = null
function effect(effectFn) {
activeEffect = effectFn
activeEffect()
activeEffect = null
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
// 如果这个对象没有被观察则将它添加到依赖列表中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 读取对象上的子属性
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
// 如果整个对象都没有被追踪则直接返回
if (!depsMap) {
return
}
let deps = depsMap.get(key)
if (!deps) {
return
}
deps.forEach(effect => effect())
}
let salePrice = 0
effect(() => {
total = product.price * product.quantity
})
effect(() => {
salePrice = product.price * 0.9
})
function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 设置响应式依赖项
return result
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)
// 当值发生变化的时候通知所有依赖项
if (oldValue !== result) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.quantity = 3
console.log(`Before updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.price = 10
console.log(`Before updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`)
通过以上代码我们可以确定现在代码不会再没有 activeEffect
函数的时候执行追踪函数。
ref 的实现
但是光这么做意义还不是很大,为什么我们没有根据销售价格来计算总数呢?比如将 total = product.price * product.quantity
替换为 total = salePrice * product.quantity
。但这并不能正常工作,因为 salePrice
不是响应式的,所以这是使用 ref
的好时机。 ref
接收一个值,并返回一个响应的,可变的 Ref
对象。 Ref
对象只有一个 .value
属性,它指向内部的值,这听起来好像是从一个文件中复制粘贴出来的(hhh)。那我们的代码将会变成这样:
let salePrice = ref(0)
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
现在我们需要考虑如何定义及实现 ref
。第一种方法我们可以使用上面的 reactive
函数来实现它:
function ref(initialValue) {
return reactive({ value: initialValue })
}
但是 Vue3 中并不是这么做的,因为 reactive 返回的东西可能会包含其他属性,而 ref 应该只包含值。为了学习 Vue3 如何实现 ref,我们需要了解 对象访问器(getter)
是什么,有时也被称之为 计算属性
。但这里指的并不是 Vue3 中的计算属性,而是 JavaScript 中的计算属性。我们先看个例子:
const user = {
firstName: 'Gregg',
lastName: 'Pollck',
get fullName() [
return `${this.firstName} ${this.lastName}`
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ')
}
}
console.log(`Name is ${user.fullName}`) // Gregg Pollck
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`) // Adam Jahr
在具备上述知识后,我们去尝试实现它:
function ref(raw) {
const result = {
get value() {
// 去追踪这个 result.value 变量
track(result, 'value')
return raw
},
set value(newValue) {
// 值发生变化时需要去触发所有的依赖项
if (raw !== newValue) {
raw = newValue
trigger(result, 'value')
}
}
}
return result
}
我们尝试去使用它:
const product = reactive({ price: 5, quantity: 2 })
let salePrice = 0
let total = 0
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.quantity = 3
console.log(`Before updated total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.price = 10
console.log(`Before updated total (should be 27) = ${total} salePrice (should be 9) = ${salePrice}`)
computed
其实到了这里,你应该早就想到了,为什么我不直接使用 computed
而是使用 effect
来每次都手动维护变量的值呢?说干就干,我们尝试着修改代码:
const product = reactive({ price: 5, quantity: 2 })
- let salePrice = 0
- let total = 0
- effect(() => {
- total = salePrice.value * product.quantity
- })
- effect(() => {
- salePrice.value = product.price * 0.9
- })
+ const salePrice = computed(() => {
+ return product.price * 0.9
+ })
+ const total = computed(() => {
+ return salePrice.value * product.quantity
+ })
那么我们尝试去实现之前,我们要知道我们需要做哪些事情:
- 创建一个响应式引用,我们可以叫它
result
- 在
effect
中运行getter
,因为我们需要监听响应值,然后将getter
赋值于result.value
- 最后我们返回
result
好的,那么我们去实现它:
function computed(getter) {
let result = ref()
effect(() => (result.value = getter()))
return result
}
是的,就这么简单,我们将其应用起来,其最终结果应该与之前一致的,只不过在使用的时候需要增加 .value
属性,就像这样:
console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
product.quantity = 3
console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
product.price = 10
console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)
当然得意于 Proxy
的能力,使得我们可以做到在 Vue2
中做不到的事情:为一个没有声明初始值的变量添加响应式。比如说这样:
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Boots'
完整代码
截止到这里,完整代码如下:
let activeEffect = null
function effect(effectFn) {
activeEffect = effectFn
activeEffect()
activeEffect = null
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
// 如果这个对象没有被观察则将它添加到依赖列表中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 读取对象上的子属性
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
// 如果整个对象都没有被追踪则直接返回
if (!depsMap) {
return
}
let deps = depsMap.get(key)
if (!deps) {
return
}
deps.forEach(effect => effect())
}
function ref(raw) {
const result = {
get value() {
// 去追踪这个 result.value 变量
track(result, 'value')
return raw
},
set value(newValue) {
// 值发生变化时需要去触发所有的依赖项
if (raw !== newValue) {
raw = newValue
trigger(result, 'value')
}
}
}
return result
}
function computed(getter) {
let result = ref()
effect(() => (result.value = getter()))
return result
}
const targetMap = new WeakMap()
const product = reactive({ price: 5, quantity: 2 })
const salePrice = computed(() => {
return product.price * 0.9
})
const total = computed(() => {
return salePrice.value * product.quantity
})
function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 设置响应式依赖项
return result
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)
// 当值发生变化的时候通知所有依赖项
if (oldValue !== result) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
product.quantity = 3
console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
product.price = 10
console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Boots'
以上。