ToC
activeEffect
我们将重新创建一个 effect
函数,用它来实现这个逻辑。
1let activeEffect = null2
3function effect(effectFn) {4 activeEffect = effectFn5 activeEffect()6 activeEffect = null7}
同时将原本的 effect
函数更新:
1- function effect() {2- total = product.price * product.quantity3- }4
5+ effect(() => {6+ total = product.price * product.quantity7+ })
当然,这项改动也意味着我们不再需要调用 effect
函数,因为它会在我们传递函数的时候调用。
1+ let activeEffect = null2
3+ function effect(effectFn) {4+ activeEffect = effectFn5+ activeEffect()6+ activeEffect = null7+ }8
9const product = reactive({ price: 5, quantity: 2 })10let total = 015 collapsed lines
11
12- function effect() {13- total = product.price * product.quantity14- }15+ effect(() => {16+ total = product.price * product.quantity17+ })18
19- effect()20
21console.log(`total is ${total}`) // total is 122
23product.price = 1024
25console.log(`total is ${total}`) // total is 20
但是我们现在需要更新追踪函数,那这个时候 activeEffect
变量就会派上用场了。回到 track()
函数上:
1function track(target, key) {2 let depsMap = targetMap.get(target)3
4 // 如果这个对象没有被观察则将它添加到依赖列表中5 if (!depsMap) {6 targetMap.set(target, (depsMap = new Map()))7 }8
9 // 读取对象上的子属性10 let deps = depsMap.get(key)30 collapsed lines
11 if (!deps) {12 depsMap.set(key, (deps = new Set()))13 }14
15 deps.add(effect)16}17
18// 我们需要执行两个操作19// 1. 判断当前是否存在 activeEffect20// 2. 将 activeEffect 替换原本的 effect21function track(target, key) {22 // 如果不存在正在执行的 activeEffect 则不收集此次访问的依赖23 if (!activeEffect) return24
25 let depsMap = targetMap.get(target)26
27 // 如果这个对象没有被观察则将它添加到依赖列表中28 if (!depsMap) {29 targetMap.set(target, (depsMap = new Map()))30 }31
32 // 读取对象上的子属性33 let deps = depsMap.get(key)34 if (!deps) {35 depsMap.set(key, (deps = new Set()))36 }37
38 // 如果是以 effect 类型收集的响应式依赖则将它放置到依赖列表中39 deps.add(activeEffect)40}
我们用一个比较高级一点的测试用例来验证这个过程。
1const targetMap = new WeakMap()2const product = reactive({ price: 5, quantity: 2 })3let total = 04
5let activeEffect = null6
7function effect(effectFn) {8 activeEffect = effectFn9 activeEffect()10 activeEffect = null77 collapsed lines
11}12
13function track(target, key) {14 if (!activeEffect) return15
16 let depsMap = targetMap.get(target)17
18 // 如果这个对象没有被观察则将它添加到依赖列表中19 if (!depsMap) {20 targetMap.set(target, (depsMap = new Map()))21 }22
23 // 读取对象上的子属性24 let deps = depsMap.get(key)25 if (!deps) {26 depsMap.set(key, (deps = new Set()))27 }28
29 deps.add(activeEffect)30}31
32function trigger(target, key) {33 const depsMap = targetMap.get(target)34
35 // 如果整个对象都没有被追踪则直接返回36 if (!depsMap) {37 return38 }39
40 let deps = depsMap.get(key)41 if (!deps) {42 return43 }44
45 deps.forEach(effect => effect())46}47
48let salePrice = 049
50effect(() => {51 total = product.price * product.quantity52})53
54effect(() => {55 salePrice = product.price * 0.956})57
58function reactive(target) {59 const handler = {60 get(target, key, receiver) {61 const result = Reflect.get(target, key, receiver)62 track(target, key) // 设置响应式依赖项63 return result64 },65 set(target, key, value, receiver) {66 const oldValue = Reflect.get(target, key, receiver)67 const result = Reflect.set(target, key, value, receiver)68 // 当值发生变化的时候通知所有依赖项69 if (oldValue !== result) {70 trigger(target, key)71 }72 return result73 }74 }75
76 return new Proxy(target, handler)77}78
79console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)80
81product.quantity = 382
83console.log(`Before updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`)84
85product.price = 1086
87console.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)。那我们的代码将会变成这样:
1let salePrice = ref(0)2
3effect(() => {4 total = salePrice.value * product.quantity5})6
7effect(() => {8 salePrice.value = product.price * 0.99})
现在我们需要考虑如何定义及实现 ref
。第一种方法我们可以使用上面的 reactive
函数来实现它:
1function ref(initialValue) {2 return reactive({ value: initialValue })3}
但是 Vue3 中并不是这么做的,因为 reactive 返回的东西可能会包含其他属性,而 ref 应该只包含值。为了学习 Vue3 如何实现 ref,我们需要了解 对象访问器(getter)
是什么,有时也被称之为 计算属性
。但这里指的并不是 Vue3 中的计算属性,而是 JavaScript 中的计算属性。我们先看个例子:
1const user = {2 firstName: 'Gregg',3 lastName: 'Pollck',4
5 get fullName() [6 return `${this.firstName} ${this.lastName}`7 },8
9 set fullName(value) {10 [this.firstName, this.lastName] = value.split(' ')6 collapsed lines
11 }12}13
14console.log(`Name is ${user.fullName}`) // Gregg Pollck15user.fullName = 'Adam Jahr'16console.log(`Name is ${user.fullName}`) // Adam Jahr
在具备上述知识后,我们去尝试实现它:
1function ref(raw) {2 const result = {3 get value() {4 // 去追踪这个 result.value 变量5 track(result, 'value')6 return raw7 },8 set value(newValue) {9 // 值发生变化时需要去触发所有的依赖项10 if (raw !== newValue) {8 collapsed lines
11 raw = newValue12 trigger(result, 'value')13 }14 }15 }16
17 return result18}
我们尝试去使用它:
1const product = reactive({ price: 5, quantity: 2 })2let salePrice = 03let total = 04
5effect(() => {6 total = salePrice.value * product.quantity7})8
9effect(() => {10 salePrice.value = product.price * 0.911 collapsed lines
11})12
13console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)14
15product.quantity = 316
17console.log(`Before updated total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice}`)18
19product.price = 1020
21console.log(`Before updated total (should be 27) = ${total} salePrice (should be 9) = ${salePrice}`)
computed
其实到了这里,你应该早就想到了,为什么我不直接使用 computed
而是使用 effect
来每次都手动维护变量的值呢?说干就干,我们尝试着修改代码:
1const product = reactive({ price: 5, quantity: 2 })2- let salePrice = 03- let total = 04
5- effect(() => {6- total = salePrice.value * product.quantity7- })8
9- effect(() => {10- salePrice.value = product.price * 0.99 collapsed lines
11- })12
13+ const salePrice = computed(() => {14+ return product.price * 0.915+ })16
17+ const total = computed(() => {18+ return salePrice.value * product.quantity19+ })
那么我们尝试去实现之前,我们要知道我们需要做哪些事情:
- 创建一个响应式引用,我们可以叫它
result
- 在
effect
中运行getter
,因为我们需要监听响应值,然后将getter
赋值于result.value
- 最后我们返回
result
好的,那么我们去实现它:
1function computed(getter) {2 let result = ref()3
4 effect(() => (result.value = getter()))5
6 return result7}
是的,就这么简单,我们将其应用起来,其最终结果应该与之前一致的,只不过在使用的时候需要增加 .value
属性,就像这样:
1console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)2
3product.quantity = 34
5console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)6
7product.price = 108
9console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)
当然得意于 Proxy
的能力,使得我们可以做到在 Vue2
中做不到的事情:为一个没有声明初始值的变量添加响应式。比如说这样:
1product.name = 'Shoes'2
3effect(() => {4 console.log(`Product name is now ${product.name}`)5})6
7product.name = 'Boots'
完整代码
截止到这里,完整代码如下:
1let activeEffect = null2
3function effect(effectFn) {4 activeEffect = effectFn5 activeEffect()6 activeEffect = null7}8
9function track(target, key) {10 if (!activeEffect) return109 collapsed lines
11
12 let depsMap = targetMap.get(target)13
14 // 如果这个对象没有被观察则将它添加到依赖列表中15 if (!depsMap) {16 targetMap.set(target, (depsMap = new Map()))17 }18
19 // 读取对象上的子属性20 let deps = depsMap.get(key)21
22 if (!deps) {23 depsMap.set(key, (deps = new Set()))24 }25
26 deps.add(activeEffect)27}28
29function trigger(target, key) {30 const depsMap = targetMap.get(target)31
32 // 如果整个对象都没有被追踪则直接返回33 if (!depsMap) {34 return35 }36
37 let deps = depsMap.get(key)38 if (!deps) {39 return40 }41
42 deps.forEach(effect => effect())43}44
45function ref(raw) {46 const result = {47 get value() {48 // 去追踪这个 result.value 变量49 track(result, 'value')50 return raw51 },52 set value(newValue) {53 // 值发生变化时需要去触发所有的依赖项54 if (raw !== newValue) {55 raw = newValue56 trigger(result, 'value')57 }58 }59 }60
61 return result62}63
64function computed(getter) {65 let result = ref()66
67 effect(() => (result.value = getter()))68
69 return result70}71
72const targetMap = new WeakMap()73const product = reactive({ price: 5, quantity: 2 })74const salePrice = computed(() => {75 return product.price * 0.976})77
78const total = computed(() => {79 return salePrice.value * product.quantity80})81
82function reactive(target) {83 const handler = {84 get(target, key, receiver) {85 const result = Reflect.get(target, key, receiver)86 track(target, key) // 设置响应式依赖项87 return result88 },89 set(target, key, value, receiver) {90 const oldValue = Reflect.get(target, key, receiver)91 const result = Reflect.set(target, key, value, receiver)92 // 当值发生变化的时候通知所有依赖项93 if (oldValue !== result) {94 trigger(target, key)95 }96 return result97 }98 }99
100 return new Proxy(target, handler)101}102
103console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)104
105product.quantity = 3106
107console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)108
109product.price = 10110
111console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)112
113product.name = 'Shoes'114
115effect(() => {116 console.log(`Product name is now ${product.name}`)117})118
119product.name = 'Boots'
以上。