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)
data.name = 'lisi' console.log(data.name)
|
弊端
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)
proxyData.name = 'lisi'
console.log(proxyData.name)
proxyData.hobby = '打篮球' console.log(proxyData.hobby)
proxyData.hobby = '打游戏'
console.log(proxyData.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 } function track(target, key) { 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) }
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)
|
然后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()) 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) { 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) } 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的概率会大大提高.