一问vue3的双向绑定原理用了什么,大家都会有异口同声的说proxy,但是具体的原理还没有了解过,接下来就带大家了解,并实现一个简易的reactive
vue2/vue3响应式原理区别
vue2的响应式原理是Object.defineProperty(),但是有缺点
- 一次性递归内存开销大,可能导致栈溢出
- 不能监听对象的
新增属性和删除属性 - 无法正确的监听数组的方法
vue3的响应式原理是用proxy,可以劫持整个对象,并返回一个新对象。有多种劫持操作(13 种),但不兼容IE浏览器
接下来我们来看看proxy是否存在vue2的缺点
const obj = {
name: '张三'
}
const proxyObj = new Proxy(obj,{
get(target, key) {
console.log(`获取obj变量里的${key}属性`, target[key])
},
set(target, key, val) {
console.log(`设置obj变量里的${key}属性`)
},
deleteProperty(target, key) {
console.log(`删除obj变量里的${key}属性`)
}
})
proxyObj.name // 获取obj变量里的name属性 张三
proxyObj.age = 18 // 设置obj变量里的age属性
delete proxyObj.name // 删除obj变量里的name属性
proxy确实可以拦截到对象的新增和删除
reactive
插播一个小插曲,在学习proxy的时候,我们发现get的返回的是Reflect,很多人应该会有疑问,为什么不直接返回target[key],而是要用Reflect?
Reflect主要是和Proxy配对使用,提供对象语义的默认行为。
上面的每个字和单词我都认识,为什么拼在一起我就不理解了?
我用下面的几个例子给大家解释一下
为什么要用Reflect
我们在文档里可以看到proxy的get的第三个参数是receiver,很多人把它理解为代理对象,但这是不全面的
const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log(receiver === proxy); // true
return target[key];
},
});
通过上面的例子的输出结果,receiver确实和代理对象proxy相等
我们再来看个例子,看看get方法中返回target[key] 和 Reflect.get(target, key, receiver) 的区别
const obj = {
_name: '张三',
get name() {
return this._name
}
}
const proxyObj = new Proxy(obj,{
get(target, key, receiver) {
return target[key]
// return Reflect.get(target, key)
// return Reflect.get(target, key, receiver)
}
})
const newObj = {
_name: '李四'
}
// newObj的原型链为proxyObj
Object.setPrototypeOf(newObj, proxyObj);
console.log(newObj.name) // 张三
当我们执行上述代码,结果输出张三,我们再把get方法返回Reflect.get(target, key),发现还是输出张三,于是我使出杀手锏,返回Reflect.get(target, key, receiver),发现输出李四,符合我们的内心期望,但是为什么会这样呢?
其实是因为当我们访问对象newObj的name属性时,因为newObj本身没有该属性,所以会去其原型proxyObj对象中找name属性,这个时候receiver就指向了newObj,代表原始的读操作所在的那个对象。
- Proxy 中 get 的 receiver 不仅仅代表的是 Proxy 代理对象本身,同时也会代表继承 Proxy 的那个对象。
- 本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问
言归正传,我们还是继续来实现一下reactive
const isObject = val => val !== null && typeof val === 'object'
let proxyMap = new WeakMap() // 存储响应数据
const baseHandler = {
get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target,key)
// 如果子元素存在引用类型递归处理
return isObject(target[key]) ? reactive(res) : res
},
set (target,key, val, receiver) {
const oldValue = Reflect.get(target, key, receiver)
const res = Reflect.set(target, key, val, receiver)
// 如果新旧值不一致,更新操作
if (oldValue !== val) trigger(target, key, val)
return res
}
}
function reactive(target) {
const existingProxy = proxyMap.get(target)
// 如果proxyMap存在target
if (existingProxy) return existingProxy
const proxy = new Proxy(target, baseHandler)
// 更新proxyMap
proxyMap.set(target, proxy)
return proxy
}
上面的主要代码就是在get的时候进行依赖收集;在set的时候进行更新。可以看出核心重点是track 和 trigger函数
effect
let targetMap = new WeakMap() // 存储响应数据对应effect
let activeEffect // 表示当前正在走的 effect
function effect (fn) {
try {
activeEffect = fn
return fn()
} finally{
activeEffect = null
}
}
const obj = reactive({
name: '张三'
})
effect(() => console.log(`我是{obj.name}`))
以上面的调用例子,我们执行effect函数,先赋值activeEffect,然后执行fn,这个时候需要先获取响应对象obj的name属性,然后就会触发响应数据的get,接着就会调用track进行依赖收集。收集完毕之后就会对activeEffect进行置空。
响应式顺序:effect => track
track
function track (target,key) {
const effect = activeEffect
if (effect) {
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 (!dep.has(effect)){
dep.add(effect)
}
}
}
我们来看看track函数 可以看到有WeakMap,Map,Set,看的头晕眼花,我们用下面例子来看看targetMap长啥样
const obj = reactive({
name: '张三',
age: 18
})
effect(() => console.log(`我叫${obj.name}`))
effect(() => console.log(`今年${obj.age}岁`))
这样子看的话,应该比较清楚一点,代码总体就是收集依赖
为什么要用 WeakMap 来存储
原生的 WeakMap 持有的是每个键对象的 弱引用,这意味着在没有其他引用存在时垃圾回收能正确进行。
WeakMap 的键所指向的对象,不计入垃圾回收机制。
Vue 3 之所以使用 WeakMap 来作为缓冲区就是为了能将 不再使用的数据进行正确的垃圾回收。
trigger
function trigger(target,key,val) {
const depsMap = targetMap.get(target)
// 用set去重
const effects = new Set()
if (!depsMap) return
const deps = depsMap.get(key)
for (const dep of deps) {
if (dep) {
effects.add(dep)
}
}
effects.forEach(effect => {
effect()
})
}
更新操作的流程就是获取响应对象对应的属性关联的effect进行更新
demo
const isObject = val => val !== null && typeof val === 'object'
let proxyMap = new WeakMap() // 存储proxy响应后的值
let targetMap = new WeakMap()
let activeEffect
const baseHandler = {
get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target,key)
return isObject(target[key]) ? reactive(res) : res
},
set (target,key, val, receiver) {
const oldValue = Reflect.get(target, key, receiver)
const res = Reflect.set(target, key, val, receiver)
if (oldValue !== val) trigger(target,key,val)
return res
}
}
function reactive(target) {
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(target, baseHandler)
proxyMap.set(target, proxy)
return proxy
}
function track (target,key) {
const effect = activeEffect
if (effect) {
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 (!dep.has(effect)){
dep.add(effect)
}
}
}
function trigger(target,key,val) {
const depsMap = targetMap.get(target)
// 用set去重
const effects = new Set()
if (!depsMap) return
const deps = depsMap.get(key)
for (const dep of deps) {
if (dep) {
effects.add(dep)
}
}
effects.forEach(effect => {
effect()
})
}
function effect (fn) {
try {
activeEffect = fn
return fn()
} finally{
activeEffect = null
}
}
const obj = reactive({
name: '张三',
age: 12,
info: {
school: 'xx小学',
class: '六年二班'
}
})
effect(() => console.log(`我叫${obj.name}今年${obj.age}岁,就读于${obj.info.school},在${obj.info.class}`))
obj.name = '李四'
obj.info.school = 'yy小学'
执行上述代码可以正常响应变化
本文很多边界情况没有考虑,只实现了一个基本功能的reactive,让大家了解一下其原理