Libon

《Vue3 响应式系统原理》(下)

10 分钟 #vue
响应式系统 activeEffect & ref & computed

ToC

activeEffect

我们将重新创建一个 effect 函数,用它来实现这个逻辑。

1
let activeEffect = null
2
3
function effect(effectFn) {
4
activeEffect = effectFn
5
activeEffect()
6
activeEffect = null
7
}

同时将原本的 effect 函数更新:

1
- function effect() {
2
- total = product.price * product.quantity
3
- }
4
5
+ effect(() => {
6
+ total = product.price * product.quantity
7
+ })

当然,这项改动也意味着我们不再需要调用 effect 函数,因为它会在我们传递函数的时候调用。

1
+ let activeEffect = null
2
3
+ function effect(effectFn) {
4
+ activeEffect = effectFn
5
+ activeEffect()
6
+ activeEffect = null
7
+ }
8
9
const product = reactive({ price: 5, quantity: 2 })
16 collapsed lines
10
let total = 0
11
12
- function effect() {
13
- total = product.price * product.quantity
14
- }
15
+ effect(() => {
16
+ total = product.price * product.quantity
17
+ })
18
19
- effect()
20
21
console.log(`total is ${total}`) // total is 1
22
23
product.price = 10
24
25
console.log(`total is ${total}`) // total is 20

但是我们现在需要更新追踪函数,那这个时候 activeEffect 变量就会派上用场了。回到 track() 函数上:

1
function track(target, key) {
2
let depsMap = targetMap.get(target)
3
4
// 如果这个对象没有被观察则将它添加到依赖列表中
5
if (!depsMap) {
6
targetMap.set(target, (depsMap = new Map()))
7
}
8
9
// 读取对象上的子属性
31 collapsed lines
10
let deps = depsMap.get(key)
11
if (!deps) {
12
depsMap.set(key, (deps = new Set()))
13
}
14
15
deps.add(effect)
16
}
17
18
// 我们需要执行两个操作
19
// 1. 判断当前是否存在 activeEffect
20
// 2. 将 activeEffect 替换原本的 effect
21
function track(target, key) {
22
// 如果不存在正在执行的 activeEffect 则不收集此次访问的依赖
23
if (!activeEffect) return
24
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
}

我们用一个比较高级一点的测试用例来验证这个过程。

1
const targetMap = new WeakMap()
2
const product = reactive({ price: 5, quantity: 2 })
3
let total = 0
4
5
let activeEffect = null
6
7
function effect(effectFn) {
8
activeEffect = effectFn
9
activeEffect()
78 collapsed lines
10
activeEffect = null
11
}
12
13
function track(target, key) {
14
if (!activeEffect) return
15
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
32
function trigger(target, key) {
33
const depsMap = targetMap.get(target)
34
35
// 如果整个对象都没有被追踪则直接返回
36
if (!depsMap) {
37
return
38
}
39
40
let deps = depsMap.get(key)
41
if (!deps) {
42
return
43
}
44
45
deps.forEach(effect => effect())
46
}
47
48
let salePrice = 0
49
50
effect(() => {
51
total = product.price * product.quantity
52
})
53
54
effect(() => {
55
salePrice = product.price * 0.9
56
})
57
58
function reactive(target) {
59
const handler = {
60
get(target, key, receiver) {
61
const result = Reflect.get(target, key, receiver)
62
track(target, key) // 设置响应式依赖项
63
return result
64
},
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 result
73
}
74
}
75
76
return new Proxy(target, handler)
77
}
78
79
console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)
80
81
product.quantity = 3
82
83
console.log(`Before updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`)
84
85
product.price = 10
86
87
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)。那我们的代码将会变成这样:

1
let salePrice = ref(0)
2
3
effect(() => {
4
total = salePrice.value * product.quantity
5
})
6
7
effect(() => {
8
salePrice.value = product.price * 0.9
9
})

现在我们需要考虑如何定义及实现 ref 。第一种方法我们可以使用上面的 reactive 函数来实现它:

1
function ref(initialValue) {
2
return reactive({ value: initialValue })
3
}

但是 Vue3 中并不是这么做的,因为 reactive 返回的东西可能会包含其他属性,而 ref 应该只包含值。为了学习 Vue3 如何实现 ref,我们需要了解 对象访问器(getter) 是什么,有时也被称之为 计算属性。但这里指的并不是 Vue3 中的计算属性,而是 JavaScript 中的计算属性。我们先看个例子:

1
const user = {
2
firstName: 'Gregg',
3
lastName: 'Pollck',
4
5
get fullName() [
6
return `${this.firstName} ${this.lastName}`
7
},
8
9
set fullName(value) {
7 collapsed lines
10
[this.firstName, this.lastName] = value.split(' ')
11
}
12
}
13
14
console.log(`Name is ${user.fullName}`) // Gregg Pollck
15
user.fullName = 'Adam Jahr'
16
console.log(`Name is ${user.fullName}`) // Adam Jahr

在具备上述知识后,我们去尝试实现它:

1
function ref(raw) {
2
const result = {
3
get value() {
4
// 去追踪这个 result.value 变量
5
track(result, 'value')
6
return raw
7
},
8
set value(newValue) {
9
// 值发生变化时需要去触发所有的依赖项
9 collapsed lines
10
if (raw !== newValue) {
11
raw = newValue
12
trigger(result, 'value')
13
}
14
}
15
}
16
17
return result
18
}

我们尝试去使用它:

1
const product = reactive({ price: 5, quantity: 2 })
2
let salePrice = 0
3
let total = 0
4
5
effect(() => {
6
total = salePrice.value * product.quantity
7
})
8
9
effect(() => {
12 collapsed lines
10
salePrice.value = product.price * 0.9
11
})
12
13
console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)
14
15
product.quantity = 3
16
17
console.log(`Before updated total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice}`)
18
19
product.price = 10
20
21
console.log(`Before updated total (should be 27) = ${total} salePrice (should be 9) = ${salePrice}`)

computed

其实到了这里,你应该早就想到了,为什么我不直接使用 computed 而是使用 effect 来每次都手动维护变量的值呢?说干就干,我们尝试着修改代码:

1
const product = reactive({ price: 5, quantity: 2 })
2
- let salePrice = 0
3
- let total = 0
4
5
- effect(() => {
6
- total = salePrice.value * product.quantity
7
- })
8
9
- effect(() => {
10 collapsed lines
10
- salePrice.value = product.price * 0.9
11
- })
12
13
+ const salePrice = computed(() => {
14
+ return product.price * 0.9
15
+ })
16
17
+ const total = computed(() => {
18
+ return salePrice.value * product.quantity
19
+ })

那么我们尝试去实现之前,我们要知道我们需要做哪些事情:

  1. 创建一个响应式引用,我们可以叫它 result
  2. effect 中运行 getter ,因为我们需要监听响应值,然后将 getter 赋值于 result.value
  3. 最后我们返回 result

好的,那么我们去实现它:

1
function computed(getter) {
2
let result = ref()
3
4
effect(() => (result.value = getter()))
5
6
return result
7
}

是的,就这么简单,我们将其应用起来,其最终结果应该与之前一致的,只不过在使用的时候需要增加 .value 属性,就像这样:

1
console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
2
3
product.quantity = 3
4
5
console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
6
7
product.price = 10
8
9
console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)

当然得意于 Proxy 的能力,使得我们可以做到在 Vue2 中做不到的事情:为一个没有声明初始值的变量添加响应式。比如说这样:

1
product.name = 'Shoes'
2
3
effect(() => {
4
console.log(`Product name is now ${product.name}`)
5
})
6
7
product.name = 'Boots'

完整代码

截止到这里,完整代码如下:

1
let activeEffect = null
2
3
function effect(effectFn) {
4
activeEffect = effectFn
5
activeEffect()
6
activeEffect = null
7
}
8
9
function track(target, key) {
110 collapsed lines
10
if (!activeEffect) return
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
29
function trigger(target, key) {
30
const depsMap = targetMap.get(target)
31
32
// 如果整个对象都没有被追踪则直接返回
33
if (!depsMap) {
34
return
35
}
36
37
let deps = depsMap.get(key)
38
if (!deps) {
39
return
40
}
41
42
deps.forEach(effect => effect())
43
}
44
45
function ref(raw) {
46
const result = {
47
get value() {
48
// 去追踪这个 result.value 变量
49
track(result, 'value')
50
return raw
51
},
52
set value(newValue) {
53
// 值发生变化时需要去触发所有的依赖项
54
if (raw !== newValue) {
55
raw = newValue
56
trigger(result, 'value')
57
}
58
}
59
}
60
61
return result
62
}
63
64
function computed(getter) {
65
let result = ref()
66
67
effect(() => (result.value = getter()))
68
69
return result
70
}
71
72
const targetMap = new WeakMap()
73
const product = reactive({ price: 5, quantity: 2 })
74
const salePrice = computed(() => {
75
return product.price * 0.9
76
})
77
78
const total = computed(() => {
79
return salePrice.value * product.quantity
80
})
81
82
function reactive(target) {
83
const handler = {
84
get(target, key, receiver) {
85
const result = Reflect.get(target, key, receiver)
86
track(target, key) // 设置响应式依赖项
87
return result
88
},
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 result
97
}
98
}
99
100
return new Proxy(target, handler)
101
}
102
103
console.log(`Before updated total (should be 10) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
104
105
product.quantity = 3
106
107
console.log(`Before updated total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`)
108
109
product.price = 10
110
111
console.log(`Before updated total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`)
112
113
product.name = 'Shoes'
114
115
effect(() => {
116
console.log(`Product name is now ${product.name}`)
117
})
118
119
product.name = 'Boots'

以上。


CD ..
回顾上一篇
Vite 虚拟模块