Vue响应式

0.为什么Vue3的响应式优于Vue2?

  • Vue2的响应式基于OPbject.defineProperty实现
  • Vue3的响应式基于ES6的Proxy实现

1.Vue2

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
// 响应式函数
function reactive(obj, key, value) {
Object.defineProperty(data, key, {
get() {
console.log(`访问了${key}属性`)
return value
},
set(val) {
console.log(`将${key}由->${value}->设置成->${val}`)
if (value !== val) {
value = val
}
}
})
}


const data = {
name: 'zhangsan',
age: 22
}
Object.keys(data).forEach(key => reactive(data, key, data[key]))
console.log(data.name)
// 访问了name属性
// zhangsan
data.name = 'lisi' // 将name由->zhangsan->设置成->lisi
console.log(data.name)
// 访问了name属性
// lisi

弊端

1
2
3
4
5
6
// 接着上面代码

data.hobby = '打篮球'
console.log(data.hobby) // 打篮球
data.hobby = '打游戏'
console.log(data.hobby) // 打游戏

data新增了hobby属性,进行访问和设值,但是都不会触发get和set,所以弊端就是:Object.defineProperty只对初始对象里的属性有监听作用,而对新增的属性无效。
这也是为什么Vue2中对象新增属性的修改需要使用Vue.$set来设值的原因。

2.Vue3

Proxy可以对新属性进行响应处理

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
const data = {
name: 'zhagnsan',
age: 22
}

function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log(`访问了${key}属性`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`将${key}由->${target[key]}->设置成->${value}`)
Reflect.set(target, key, value, receiver)
}
}

return new Proxy(target, handler)
}

const proxyData = reactive(data)

console.log(proxyData.name)
// 访问了name属性
// zhagnsan
proxyData.name = 'lisi'
// 将name由->zhagnsan->设置成->lisi
console.log(proxyData.name)
// 访问了name属性
// lisi

//important
proxyData.hobby = '打篮球'
console.log(proxyData.hobby)
// 访问了hobby属性
// 打篮球
proxyData.hobby = '打游戏'
// 将hobby由->打篮球->设置成->打游戏
console.log(proxyData.hobby)
// 访问了hobby属性
// 打游戏

3.Vue3响应式原理

track和trigger

  • track 是一个函数,用来收集依赖变量的effect函数,放到使用Set建立的dep中
  • trigger 是一个触发器,当变量更新时,将通知dep里所有依赖变量的effect函数执行

对于对象

使用Map存储多个dep来对应对象的不同属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const depsMap = new Map()
function track(key) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
if (key === 'name') {
dep.add(effectNameStr1)
dep.add(effectNameStr2)
} else {
dep.add(effectAgeStr1)
dep.add(effectAgeStr2)
}
}
function trigger (key) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}

对于多对象

使用WeakMap存储每个对象对应的Map

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
const person = { name: '林三心', age: 22 }
const animal = { type: 'dog', height: 50 }

const targetMap = new WeakMap()
function track(target, key) {
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())
}
// 这里先暂且写死
if (target === person) {
if (key === 'name') {
dep.add(effectNameStr1)
dep.add(effectNameStr2)
} else {
dep.add(effectAgeStr1)
dep.add(effectAgeStr2)
}
} else if (target === animal) {
if (key === 'type') {
dep.add(effectTypeStr1)
dep.add(effectTypeStr2)
} else {
dep.add(effectHeightStr1)
dep.add(effectHeightStr2)
}
}
}

function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}

Proxy 实现自动收集依赖 自动通知更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function reactive(target){
const handler = {
get(target, key, receiver){
track(receiver, key) //访问时收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, receiver){
Reflect.set(target, key, value, receiver)
trigger(receiver, key) //设置时自动通知更新
}
}

return new Proxy(target, handler)
}
修改effect函数使其执行则自动添加到dep中
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
let activeEffect = null
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null // 执行后立马变成null
}
function track(target, key) {
// 如果此时activeEffect为null则不执行下面
// 这里判断是为了避免例如console.log(person.name)而触发track
if (!activeEffect) return
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())
}
dep.add(activeEffect) // 把此时的activeEffect添加进去
}

// 每个effect函数改成这么执行
effect(effectNameStr1)
effect(effectNameStr2)
effect(effectAgeStr1)
effect(effectAgeStr2)
effect(effectTypeStr1)
effect(effectTypeStr2)
effect(effectHeightStr1)
effect(effectHeightStr2)
当前流程
  • 执行effect
  • 触发get
  • 触发track()
  • 此时activeEffect为effect()被收集

实现ref

在Vue3中是这么使用ref

1
2
let num = ref(5)
console.log(num.value) // 5

然后num就会成为一个响应式的数据,而且使用num时需要这么写num.value才能使用

实现ref其实很简单,咱们上面已经实现了reactive,只需要这么做就可以实现ref

1
2
3
4
5
function ref (initValue) {
return reactive({
value: initValue
})
}

实现computed

1
2
3
4
5
function computed(fn) {
const result = ref()
effect(() => result.value = fn()) // 执行computed传入函数
return result
}

4.最终代码

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) {
// 如果此时activeEffect为null则不执行下面
// 这里判断是为了避免例如console.log(person.name)而触发track
if (!activeEffect) return
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())
}
dep.add(activeEffect) // 把此时的activeEffect添加进去
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(receiver, key) // 访问时收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key) // 设值时自动通知更新
}
}

return new Proxy(target, handler)
}
let activeEffect = null
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null
}
function ref(initValue) {
return reactive({
value: initValue
})
}
function computed(fn) {
const result = ref()
effect(() => result.value = fn())
return result
}

5. Why Proxy with Reflect?

Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

1
var proxy = new Proxy(target, handler)

参数

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

handler解析

关于handler拦截属性,有如下:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截Object.keys(proxy)for...in等循环,返回一个数组
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作

Reflect

若需要在Proxy内部调用对象的默认行为,建议使用Reflect,其是ES6中操作对象而提供的新 API

基本特点:

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
  • Object操作都变成函数行为

Why Proxy with Reflect

因为Proxy和Reflect的方法都是一一对应的,在Proxy里使用Reflect会提高语义化

  • Proxy的get对应Reflect.get
  • Proxy的set对应Reflect.set
  • 还有很多其他方法我就不一一列举,都是一一对应的

还有一个原因就是,尽量把this放在receiver上,而不放在target

为什么要尽量把this放在代理对象receiver上,而不建议放原对象target上呢?因为原对象target有可能本来也是是另一个代理的代理对象,所以如果this一直放target上的话,出bug的概率会大大提高.