站在巨人的肩膀上看vue,来自霍春阳的vue设计与实现。作者以问题的形式一步步解读vue3底层的实现。思路非常的巧妙,这里会将书中的主要逻辑进行串联,也是自己读后的记录,希望通过这种形式可以和大家一起交流学习。
第5章 非原始值的响应方案
5.1、理解 Proxy 和 Reflect
Proxy 可以创建一个对象,能够实现对其他对象的代理,需要注意的是Proxy 只能代理对象,无法代理非对象值,比如字符串、布尔值等。
Reflect 是一个全局对象,有着对象一样的方法,但是其接受第三个参数receiver。可以理解为函数调用过程中的this。
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
之前的Proxy的代理时是直接通过对象的方式获取值,这样会出现一个问题,就是get中的this可能会改变导致无法进行响应的依赖收集,因此我们可以通过reflect的特性替换之前的方法
const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
5.2、对象和Proxy的工作原理
对象的实际语义是由对象的内部方法指定的。内部方法指的是当我们对一个对象进行操作时在引擎内部调用的方法。函数对象内部会部署一个内部方法 [[Call]],而普通对象不会。
创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象的内部方法和行为。
5.3、如何代理object
问题1: key in obj 如何拦截
in操作符的运算结果是通过调用一个叫 HasProperty 的抽象方法得到的。对应的拦截函数名为has,因此我们在拦截的时候需要定义has方法
const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
}
})
问题2: for ... in 循环
同理 for...in 对应的拦截对象是 ownKeys, 因此我们在拦截的时候需要定义ownKeys方法。并且定义一个Symbol
存储副作用收集的函数,当trigger 函数执行时,还要把 ITERATE_KEY 相关联的副作用函数取出来执行。
const ITERATE_KEY = Symbol()
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
// trigger 函数
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
问题3:删除属性 delete obj.foo
delete对应的属性deleteProperty。因此我们在拦截的时候需要定义deleteProperty方法,并且删除的时候不需要执行副作用。
deleteProperty(target, key) {
const hadKey = Object.hasOwnProperty.call(target, key)
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
trigger(target, key, 'DELETE')
}
return res
}
5.4、合理低触发响应
问题4:当改变值的没变,不触发响应,,添加比较,处理一下NaN 的情况。
set(target, key, newVal, receiver) {
const oldVal = target[key]
const type = Object.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
return res
},
问题5:原型上继承的属性
const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
Object.setPrototypeOf(child, parent)
effect(() => {
console.log(child.bar)
})
child.bar = 2
// 1
// 2
// 2
副作用执行了两次。
当读取child.bar属性值时,由于child代理的对象obj自身没有bar属性,因此就会获取对象的obj的原型,得到parent.bar 的值。同理设置属性的时候也会寻找原型上的值,这时候,child、parent都建立了响应式,所以副作用执行了两次。
解决方法,只需要判断receiver是否是target的带来对象即可,只有当receiver是target的带来对象时才会触发更新,这样就能够屏蔽了由原型引起的更新。代理对象可以通过raw属性读取原始数据
child.raw === obj // true
parent.raw === proto // true
在set中判断receiver是不是target的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
5.5、浅响应与深响应
当读取属性值是,首先检测该值是否是对象,如果是对象,则递归的调用 reactive函数将其包装成响应式数据并返回,这样就首先了深度响应。通过设置一个isShallow变量,来控制。
function creatReactive(obj, isShallow = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === 'raw') {
return target
}
const res = Reflect.get(target, key, receiver)
if (isShallow) {
return res
}
track(target, key)
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
set(target, key, newVal, receiver) {
//
}
})
}
5.6、只读和浅只读
createReactive函数接收一个 isReadonly 的变量,在 set 的时候判断是否是只读。并且当第一个对象是只读属性时,在 get 中也不需要调用 track 函数追踪响应。
get() {
if (!isReadonly) {
track(target, key)
}
},
set() {
if (isReadonly) {
console.warn(`属性 ${key} 是只读信息`)
return true
}
}
5.7、代理数组
5.7.1、数组的索引与长度