一窥 vue3 响应式系统

Vue 中,响应式系统采取的是发布订阅的模式,Vue2 中主要依赖 ES5 Object.defineProperty 的 API,在 vue3 中重写了响应式系统,使用的是 ES6 Proxy Reflect,在它们不受支持的情况下会降级使用 Object.defineProperty。今天我们来实现一个极其简易版的响应式系统。

前提

JS 本身并没有响应式的特性,那 Vue 是如何实现这一魔法的呢,我们先来看下:

1
2
3
4
5
6
7
8
let product = {
name: '西葫芦',
price: 10,
quantity: 1,
}
let total = product.quantity * product.price // 10
product.price = 12
console.log('total :>> ', total); // 10

total 并没有自动更新为 12。我们可以修改下:

1
2
3
4
5
6
7
8
9
10
11
12
let product = {
name: '西葫芦',
price: 10,
quantity: 1,
}
let total = 0;
const effect = () => {
total = product.quantity * product.price
}
effect() //total: 10
product.price = 12
effect() //total: 12

但此时我们仍然需要手动执行下 effect 才能让 total 的值变成”响应式”,如何能够自动更新呢?我们可以先修改下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const targetMap = new WeakMap();
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)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => effect())
}
}

let product = {
name: '西葫芦',
price: 10,
quantity: 1,
}
let total = 0;
const effect = () => {
total = product.quantity * product.price
}
track(product, 'price')
product.price = 12
trigger(product, 'price') //total: 12

如上,我们收集了依赖,当它被改变时,触发更新,使得 total 更新成功,这里使用到了 ES6 中的WeakMap Map Set

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意。WeakMap 的 key 是不可枚举的,因为 WeakMap 中每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象时这个对象将会被垃圾回收(相应的key则变成无效的)。

简易版本响应式

如何能让依赖发生变更时自动更新呢,我们更新如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const targetMap = new WeakMap();
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)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => effect())
}
}

const reactive = (target) => {
const handler = (target, {
get(target, key, receiver) {
console.log('get key :>> ', key);
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set key :>> ', key);
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
return new Proxy(target, handler)
}

let product = reactive({
name: '西葫芦',
price: 10,
quantity: 1,
})
let total = 0;
const effect = () => {
total = product.quantity * product.price
}
effect() // 建立 quantity 和 price 的依赖和副作用
console.log('total :>> ', total); // 10
product.price = 12
console.log('total :>> ', total); // 12

成功了🙌,但是呢,如果我们在末尾加上一个

1
console.log('name :>>', product.name)

此时,由于读取了 name ,由于拦截会重新进入到 getter 中,会重新 track,会为 name 建立多余的依赖和副作用 effect,虽然副作用本身和 name 并无关系。所以,我们可以在此基础上优化之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let activeEffect = null;
const targetMap = new WeakMap();

function effect(eff) {
activeEffect = eff;
eff();
activeEffect = null;
}
function track(target, key) {
if (activeEffect) {
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
}
const deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => effect())
}
}

const reactive = (target) => {
const handler = (target, {
get(target, key, receiver) {
console.log('get key :>> ', key);
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set key :>> ', key);
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
return new Proxy(target, handler)
}

let product = reactive({
name: '西葫芦',
price: 10,
quantity: 1,
})
let total = 0;
effect(() => {
total = product.quantity * product.price
})
console.log('total :>> ', total); // 10
product.price = 12
console.log('total :>> ', total); // 12

如上,已经比较好的实现了我们想要的效果。让我们来看看下面的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let product = reactive({
name: '西葫芦',
price: 10,
quantity: 1,
})
let total = 0;
let saleTotal = 0;
effect(() => {
total = product.quantity * product.price
})
effect(() => {
saleTotal = total * 0.8
})
console.log('total,saleTotal :>> ', total, saleTotal); //10 8
product.price = 12
console.log('total,saleTotal :>> ', total, saleTotal); //12 8

total 确实响应式更新了,但是 saleTotal 并没有相对应更新,这是因为这里的 total 并不是响应式数据,那如何让它变为响应式数据呢,

  1. 我们可以借助已经实现的 reactive ,包装如下,不过有些繁琐🤣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ref = (initVal) => {
return reactive({ value: initVal })
}
let product = reactive({
name: '西葫芦',
price: 10,
quantity: 1,
})
let total = ref(0);
let saleTotal = 0;
effect(() => {
total.value = product.quantity * product.price
})
effect(() => {
saleTotal = parseFloat(total.value * 0.8).toFixed(2) / 1
})
console.log('total,saleTotal :>> ', total.value, saleTotal); //10 8
product.price = 12
console.log('total,saleTotal :>> ', total.value, saleTotal); //12 9.6
  1. 使用 ES5 提供的对象访问器或者 JS 的计算属性 Object Accessors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const ref = (raw) => {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
console.log('newVal,raw :>> ', newVal, raw);
if (newVal !== raw) {
raw = newVal
trigger(r, 'value')
}

}
}
return r
}
let product = reactive({
name: '西葫芦',
price: 10,
quantity: 1,
})
let total = ref(0);
let saleTotal = 0;
effect(() => {
total.value = product.quantity * product.price
})
effect(() => {
saleTotal = parseFloat(total.value * 0.8).toFixed(2) / 1
})
console.log('total,saleTotal :>> ', total.value, saleTotal); //10 8
product.price = 12
console.log('total,saleTotal :>> ', total.value, saleTotal); //12 9.6

接下来实现 Vue3中的 真正的计算属性computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const computed = (getter) => {
const result = ref()
effect(() => result.value = getter())
return result
}
let product = reactive({
name: '西葫芦',
price: 10,
quantity: 1,
})
const total = computed(() => product.price * product.quantity)
const saleTotal = computed(() => parseFloat(total.value * 0.8).toFixed(2) / 1)
console.log('total,saleTotal :>> ', total.value, saleTotal.value); //10 8
product.price = 12
console.log('total,saleTotal :>> ', total.value, saleTotal.value); //12 9.6