Vue3响应式个人理解(十一)代理set和map
文章参考了霍春阳的《Vue.js设计与实现》,是自己在阅读过程中的一些思考和理解
文章参考了霍春阳的《Vue.js设计与实现》,是自己在阅读过程中的一些思考和理解
本文紧接《非原始值的响应式方案》和《代理数组》
set和map的代理不同于普通的对象,因为它们自身存在着的方法,独特的结构。
其中set结构的方法包括:size、add、clear、delete、has、keys、values、entries、forEach。
map的方法包括:size、clear、delete、has、get、set、keys、values、entries、forEach。
二者大部分的方法是一致,因此代理的方法也大致相同。
Set的size属性
// 尝试以下代码
// 新建一个Set集合
const s = new Set([1,2,3])
// 代理这个Set
const p = new Proxy(s, {})
// 输出代理的size
console.log(p.size)
会发现出现以下报错:大致意思为:在不兼容的reciver上调用了get Set.prototype.size方法。
通过查看规范,可知Set.prototype.size是一个访问器属性。首先会访问当前this的size属性,接着调用抽象方法RequireInternalSlot(S,[[SetData]])来检查s是否存在内部槽[[SetData]],很明显当this指向代理对象时,是没有这个内部槽的。因此出现错误。所以需要在get函数中新增条件判断,当访问size时,将在原对象上去执行get操作,这样就能正常执行了。代码如下:
// 新增对Set的操作
// 如果是访问size,则直接在原对象上进行操作
if(proxy === 'size') {
return Reflect.get(target, proxy, target)
}
注:这里有个冲突,由于没有调用effect函数,所以activeEffect没有定义,直接返回了。导致测试的时候没有拿到size,但是注释掉就可以正常看到了,算是一个小问题,看源码的时候记得关注一下这个。也可能是代码放的位置没对,总之需要看源码。
Set的delete方法
直接调用delete方法同样会报错,(但是我的报错怎么是delete不是一个function…),反正总而言之,没有正确的执行。书上的报错信息显示delete的报错与size的报错基本一样。但是本质上是不一样的。
由于size时一个访问器属性,而delete是一个方法。当访问t.size的时候,访问器的getter函数会执行,因此可以修改get中this的指向来访问到原来的集合;但是执行delete时,delete方法并没有执行,而执行的是t.delete(1)这句函数调用。因此delete的this一定会指向代理对象,因为是t来调用的。
解决方法:讲delete方法与原始对象绑定即可,这样就会去原始对象上执行,而不是代理对象。代码如下:
// 读取正常属性时,正常操作
const res = target[proxy].bind(target)
建立响应联系
// 例子代码
const p = reactive(new Set([1,2,3]))
// effect中读取了代理对象的大小
effect(() => {
console.log(p.size)
})
// 代理对象新增一个数据时,应该触发副作用函数,因为此时Set集合的大小发生了改变,但是实际上没有重新执行的,因为并没有建立响应式
p.add(4)
因此需要在访问size属性的时候,调用track函数进行依赖追踪,然后在add函数执行时调用trigger函数触发响应式。代码如下:
// 新增对Set的操作
// 如果是访问size,则直接在原对象上进行操作
if(proxy === 'size') {
// 建立响应式联系
track(target, ITERATE_KEY)
return Reflect.get(target, proxy, target)
}
至于这里为什么建立的是target与ITERATE_KEY的联系,因为任何新增、删除操作都会影响size属性,这里需要与前面的for循环联系一下。接着当调用add时要触发响应式,首先要有add函数,这样才能在里面触发,因此需要重写一下add函数。代码如下:
// 集合Set的部分功能重写
const mutableInstrumentations = {
add(key) {
// 由于是代理对象调用的add方法,所以this指向代理对象,通过raw属性拿到原始对象
const target = this.raw
// 在原始对象上执行add函数操作
const res = target.add(key)
// 触发副作用函数,为ADD操作
trigger(target, key, 'ADD')
// 返回结果
return res
}
}
function createReactive(obj, isShallow=false, isReadonly=false) {
return new Proxy(obj, {
get(target, key, receiver) {
if(key === 'raw') {
return target
}
if(key === 'size') {
track(target, ITERATE_KEY)
return Reflect.get(target, key, target)
}
// 返回定义的方法
return mutableInstrumentations[key]
}
})
}
由于不同类型的响应式对象,get拦截时,返回的结果也不一样。所以将会以这样的方式写在这里。add函数可以做一点优化,如果当前要插入的值已经存在于集合中,则不触发响应式。代码如下:
add(key) {
// 由于是代理对象调用的add方法,所以this指向代理对象,通过raw属性拿到原始对象
const target = this.raw
// 先判断要插入的key是否已经存在于Set集合
const isHave = target.has(key)
// 如果不存在,则执行插入操作
if(!isHave) {
// 在原始对象上执行add函数操作
const res = target.add(key)
// 触发副作用函数,为ADD操作
trigger(target, key, 'ADD')
}
// 返回结果
return res
}
有了add函数的基础,delete是同样的。代码如下:
delete(key) {
// 先获取原对象
const target = this.raw
// 判断集合中有没有要删除的元素
const isHave = target.has(key)
// 删除
const res = target.delete(key)
// 如果有要删除的元素,则触发响应式
if(isHave) {
trigger(target, key, 'DELETE')
}
// 返回结果
return res
}
避免污染原始数据
通过Map的set和get来说明什么是污染原始数据。
首先同样是要重写set和get方法,因为这不是属性。代码如下:
get(key) {
// 拿原始对象
const target = this.raw
// 判断当前的key是否存在
const isHave = target.has(key)
// 追踪依赖,建立连接
track(target, key)
// 如果存在,先拿到该value
// 如果value是对象,则需要深度响应式,否则直接返回
if(isHave) {
const res = target.get(key)
return typeof res === 'object' ? reactive(res) : res
}
},
set(key, value) {
const target = this.raw
const isHave = target.has(key)
// 先拿到旧值
const oldVal = target.get(key)
// 设置新值
target.set(key, value)
// 如果不存在key,则说明是新增,执行ADD操作
// 否则说明是修改,且如果新值与旧值不一样才执行,且规避了NAN
if(!isHave) {
trigger(target, key, 'ADD')
} else if(oldVal !== value || (oldVal === oldVal && value === value)) {
trigger(target, key, 'SET')
}
}
问题代码:
// 一个Map
const m = new Map()
// p1代理m
const p1 = reactive(m)
// p2是一个新的Map代理
const p2 = reactive(new Map())
// 在p1中添加p2
p1.set('p2', p2)
// 副作用函数中,在原始对象m中读取p2并访问其size属性
effect(() => {
console.log(m.get('p2').size)
})
// 在原始对象m中读取p2并设置新的映射<foo, 1>
m.get('p2').set('foo', 1)
执行结果:
问题:在原始对象上的读取了size属性,又通过原始对象设置了新的映射。结果为副作用函数重新执行了。也就是原始对象具有了响应式,这是不应该出现的,因为如果原始对象有了响应式,那还要响应式干嘛…代码就会乱套。
分析:这里因为在原始对象中读取一个响应式属性,导致其追踪了这个副作用函数,然后又通过原始对象读取响应式数据来设置,因此触发了副作用函数的重新执行。
在set函数中,我们将value直接set到了target中,如果value是一个响应式数据,则意味着原始对象也成为了一个响应式数据。因此将响应式数据设置到原始数据上的行为称为数据污染。
解决方法:在设置之前判断value是不是一个响应式,如果是,则获取其raw属性,也就是响应式的原始数据设置到原始数据上;不是则直接设置上去。代码如下:
// 设置新值
// 添加响应式数据判断
const newVal = value.raw || value
target.set(key, newVal)
处理forEach
同样以Map为例。forEach接收一个回调函数,回调函数接收三个参数包括value、key和原始Map。遍历操作涉及到键值对的数量。因此当Map执行delete或add操作时,都应该重新执行一次forEach,因此,让forEach与ITERATE_KEY进行绑定,并重写forEach函数。代码如下:
forEach(callback) {
// 先拿原始对象
const target = this.raw
// 追踪依赖, 建立联系
track(target, ITERATE_KEY)
// 通过原始对象来调用forEach,并传入callback
target.forEach(callback)
}
上述代码虽然可以正常工作,但是是有缺陷的。因为我们仍然是在原始对象上执行的forEach函数。这就意味着丢掉了响应式数据。代码如下:
// Map中加入一个<{key: 1}, Set([1,2,3])>的键值对
const p = reactive(new Map([
[{key: 1}, new Set([1,2,3])]
]))
// 副作用函数遍历Map,输出Set集合的size
effect(() => {
p.forEach((value, key) => {
console.log(value.size)
})
})
// 通过key拿到value,然后删除Set集合的1
p.get({key: 1}).delete(1)
执行以上代码会发现副作用函数并没有重新执行。因为forEach中执行的是原始数据value,没有响应式了,但是reactive函数应该是深响应式的,与预期的不符合,因此需要修改forEach代码,让其键值具有响应式。代码如下:
// 同时接收第二个参数,用于指定callback函数执行时的this
forEach(callback, thisArg) {
// 通过wrap来将键值中的Object改为深度响应式
const wrap = val => {
typeof val === "object" ? reactive(val) : val
}
// 先拿原始对象
const target = this.raw
// 追踪依赖, 建立联系
track(target, ITERATE_KEY)
// 通过原始对象来调用forEach,并传入callback
target.forEach((value, key) => {
// 每个键值对执行回调的时候,将其设置为响应式的
// 通过call方法来调用callback,并传递thisArg
callback.call(thisArg, wrap(value), wrap(key), this)
})
}
最后需要考虑的一点是,Map数据结构与普通的集合对象遍历不同。普通对象的for…in循环只接收key值,因此只有在ADD或DELETE操作会影响key的数量时才会重新执行循环。但是Map的forEach循环接收value和key,所以它关心value值,因此Map在执行set操作时也应该重新执行一次循环,因为value值发生了改变。因此需要修改trigger函数,代码如下:
trigger() {
// 只有当为新增或删除操作时,才执行迭代器的副作用函数
if(type === TriggerType.ADD || type === TriggerType.DELETE ||
// 新增对Map结构的SET操作时,也要执行一次循环
(
type === 'SET' && Object.prototype.toString.call(target) === '[Object Map]'
)) {
// 获取与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effect => {
if(effect != activeEffect) {
effectsToRun.add(effect)
}
})
}
}
迭代器方法
集合的迭代器有三个:entries、keys、value。另外,由于Map和Set类型本身部署了Symbol.iterator方法,因此可以直接用for…of进行迭代;同样,可以调用迭代器对象的next方法进行手动迭代。代码如下:
const m = new Map([
[k1, value1],
[k2, value2]
])
const itr = m[Symbol.iterator]()
// 此方法与entries效果相同
console.log(itr.next())
此时如果在effect中使用for…of循环迭代一个Map对象,会返回一个错误:
const t = reactive(new Map([
['k1', 'value1'],
['k2', 'value2']
]))
effect(() => {
for(const [key, value] of t) {
console.log(key, value)
}
})
显示t不是一个可迭代对象。同时发现在代理对象的get函数对Symbol.iterator进行了拦截,说明我们并没有实现这个方法。因此需要在mutableInstrumentations中实现;代码如下:
[Symbol.iterator]() {
// 先拿原对象
const target = this.raw
// 获取其迭代器方法
const itr = target[Symbol.iterator]()
// 返回
return itr
}
可以迭代了
这里存在与forEach相同的问题,就是如果key或value本身是对象,就需要深层的响应式,而在原对象中调用迭代器是没有这样的功能的,同时需要进行依赖追踪,修改代码:
[Symbol.iterator]() {
// 先拿原对象
const target = this.raw
// 获取其迭代器方法
const itr = target[Symbol.iterator]()
// 深层响应式函数
const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
// 依赖追踪
track(target, ITERATE_KEY)
// 返回自定义的迭代器
return {
next() {
const {value, done} = itr.next()
return {
// 如果value不是undefined,则进行深度响应式判断
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
}
}
}
由于对迭代器的操作不涉及内部元素,因此只需要在新增或删除时才触发响应式。至此,对for…of循环的拦截就完成了。不过考虑到entries与Symbol.iterator等价,因此可以使用相同的代码对entires函数进拦截。但是直接将上述函数给到entries会报错。因为这里要考虑到可迭代协议和迭代器协议。
额外知识:可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for…of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))。要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性 。
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。只有实现了一个拥有以下语义(semantic)的 next() 方法,一个对象才能成为迭代器 :(参考JS高级)
因此,迭代器协议就是把数据结构加上统一接口使得该数据结构能够遍历,可迭代协议就是把可迭代对象通过Symbol.iterator迭代器函数转换为迭代器对象,通过迭代器协议对迭代器对象进行迭代。
PS:不可能判断一个特定的对象是否实现了迭代器协议,然而,创造一个同时满足迭代器协议和可迭代协议的对象是很容易的。这样做允许一个迭代器能被各种需要可迭代对象的语法所使用。因此,很少会只实现迭代器协议,而不实现可迭代协议。
因此上述问题的解决方法就是同时实现迭代器协议和可迭代协议。代码如下:
function iteratorMethod() {
// 先拿原对象
const target = this.raw
// 获取其迭代器方法
const itr = target[Symbol.iterator]()
// 深层响应式函数
const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
// 依赖追踪
track(target, ITERATE_KEY)
// 返回自定义的迭代器
return {
next() {
const {value, done} = itr.next()
return {
// 如果value不是undefined,则进行深度响应式判断
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
},
// 可迭代协议
[Symbol.iterator]() {
return this
}
}
}
// 在mutableInstrumentations方法中直接复用
[Symbol.iterator]: iteratorMethod,
entries:iteratorMethod
entries遍历:
直接遍历:
values与keys方法
values与entries方法相似,只不过values遍历的是Map中的value;同时keys与values页相似,只不过keys遍历的是Map中的key。因此基本上,只需要微小的改动就可以复用上一节的代码:
values() {
// 先拿原对象
const target = this.raw
// 获取其迭代器方法
const itr = target.values()
// 深层响应式函数
const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
// 依赖追踪
track(target, ITERATE_KEY)
// 返回自定义的迭代器
return {
next() {
const {value, done} = itr.next()
return {
// 如果value不是undefined,则进行深度响应式判断
value: wrap(value),
done
}
},
// 可迭代协议
[Symbol.iterator]() {
return this
}
}
},
keys() {
// 先拿原对象
const target = this.raw
// 获取其迭代器方法
const itr = target.keys()
// 深层响应式函数
const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
// 依赖追踪
track(target, ITERATE_KEY)
// 返回自定义的迭代器
return {
next() {
const {value, done} = itr.next()
return {
// 如果value不是undefined,则进行深度响应式判断
value: wrap(value),
done
}
},
// 可迭代协议
[Symbol.iterator]() {
return this
}
}
}
变化在原对象方法的调用上,即拦截什么方法就在原对象上使用什么方法;还有就是深度响应式时,由于是单个对象,所以只需要直接对其进行操作就可以。
对于entries和values方法来说,如果有value的修改操作,需要重新执行副作用函数,这里直接是复用了以前的对SET类型的Map判断。代码如下:
// 只有当为新增或删除操作时,才执行迭代器的副作用函数
if(type === TriggerType.ADD || type === TriggerType.DELETE ||
// 新增对Map结构的SET操作时,也要执行一次循环
(
type === 'SET' && Object.prototype.toString.call(target) === '[object Map]'
)) {/**/}
对于新增和删除操作来说,entries、values和keys都需要重新执行,但是对于keys方法来说,value的改变不需要重新执行副作用函数,而且不存在修改key的方法,因此需要一个新的标志来区分keys方法的遍历。代码如下:
// 只有当为新增或删除操作、对象为Map时,执行keys方法所涉及的副作用函数
if((type === TriggerType.ADD || type === TriggerType.DELETE) &&
Object.prototype.toString.call(target) === '[object Map]') {
// 获取与MAP_KEY_ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effect => {
if(effect != activeEffect) {
effectsToRun.add(effect)
}
})
}
验证效果如下,修改value不需要重新执行副作用函数。
更多推荐
所有评论(0)