这一p我们实现Set和Map的响应式实现
代理Map和Set
先看如下代码
1 | const s = new Set([1,2,3]) |
当运行上述代码的时候,在读取代理对象的size
的时候,会得到一个错误.错误的大意是在不兼容的reciver
上调用了get Set.prototype.size
方法
size属性是一个访问器属性,作为方法被调用了.通过查阅规范一可以证实这一点.
在代理对象身上访问size的时候,会调用get Set.prototype.size
方法,但是这个方法被调用的时候,this
指向的是代理对象,而不是原始对象.
显然,代理对象没有[[ SetData ]]
内部槽,所以会抛出错误.
为了修复这个问题,需要修正gettter
函数执行的this
指向为原始对象.
1 | const s = new Set([1,2,3]) |
接着,我们尝试在Set中删除数据
1 | const s = new Set([1,2,3]) |
可以看到,调用delete方法的时候会得到一个报错,与前文的p.size十分相似.
实际上,访问p.size
与访问p.delete
是不同的.size是一个属性,而delete是一个方法.当访问p.size的时候,访问器属性的getter函数会立即执行,此时我们可以修改receiver来改变getter函数的this指向
而当访问delete的时候,delete方法并没有执行.真正让delete方法执行的是p.delete(1)这句函数调用.因此,无论怎么修改receiver,delete方法执行时的this都会指向代理对象,而不会指向原始对象.
想要修复这个问题只需要将delete方法与原始对象绑定即可
1 | const s = new Set([1,2,3]) |
建立响应联系
1 | const p = reactive(new Set([1,2,3])) |
当我们调用add方法的时候,会间接改变size属性值,我们期望副作用函数会重新执行.为了实现这个目标,我们需要在访问size属性值调用track函数进行追踪,然后在add方法执行时调用trigger函数触发响应.
1 | function createReactive(obj, isShallow = false, isReadonly = false) { |
这里需要注意,响应式需要建立在ITERATE_KEY和副作用函数之间.这是因为任何新增,删除的操作都会影响ket属性.接着,我们需要重写一个能触发trigger函数的add方法.
1 | const mutableInstrumentations = { |
在trigger函数的实现中,只有ADD和DELETE操作会触发ITERATE_KEY的副作用函数(在这里指的就是size相关副作用函数)
当然,如果add方法添加的元素已经存在于集合中了,就不用触发响应了,我们可以对代码进行如下性能优化.
1 | const mutableInstrumentations = { |
这样就可以避免不必要的触发响应.
类似的,我们也可以实现delete方法,如下
1 | const mutableInstrumentations = { |
与add方法不同的是,delete方法只有在要删除的元素确实在集合中,才需要触发响应.
避免污染原始数据
本节我们借助Map类型数据中的set和get方法来理解什么是”避免污染原始数据”及其原因.
当调用get读取数据的时候.需要调用track追踪依赖建立响应式联系;当调用set方法的时候,需要调用trigger触发响应.
1 | const p = reactive(new Map([['foo', 1]])) |
下面是get方法的具体实现.
1 | const mutableInstrumentations = { |
接下来我们实现set方法.
1 | const mutableInstrumentations = { |
即使上面的set方法能够正常工作,但它依然存在问题,即set方法会污染原始数据.下面就是个例子.
1 | const m = new Map() |
在这段代码中,我们使用原始数据来读取数据值,有通过原始数据设置数据值,居然发现副作用函数重新执行了.但是我们期望原始数据不具有响应式的特征,导致问题的罪魁祸首就是set方法.
不难发现,target.set(key, value)
并没有对value
是否是响应式数据做出判断.所以在上述代码中,我们将原始数据m的值上设置了一个响应数据p2,我们吧这种将响应式数据设置到原始数据上的行为称为数据污染.
解决这个问题只需要在target.set(key, value)
之前判断value是否是响应式数据即可,如果是,则用raw
获取原型.
1 | const mutableInstrumentations = { |
现在的实现就不会造成数据污染啦.除了get方法,Set类型的add方法、普通对象的写值操作,还有为数组添加元素的方法等,都需要类似的处理.
处理forEach
1 | const m = new Map([ |
以map为例,回调函数接收三个参数,分别是值,键以及原始Map对象.
遍历操作只与键值对的数量有关.因此任何会修改map对象键值对数量的操作都会触发回调函数.例如delete和add方法.所以当forEach函数被调用的时候,我们应该让副作用函数和ITRERATE_KEY
建立响应式联系.
1 | const mutableInstrumentations = { |
这样虽然能够让我们代码按照预期运行.然而上面的forEach函数仍然存在缺陷,我们自定义的forEach方法中,通过原始数据调用了原生的forEach方法,这意味着传递给callback回调函数的参数将是非响应式数据,会导致一下代码不能正常工作.
1 | const key = { key: 1 } |
我们尝试删除Set中的1,但是副作用函数并没有重新执行.这里的问题在于当通过value.size访问size属性的时候,value是原始对象,即new Set([1,2,3])
,因此我们无法在原始数据上建立响应式联系.
但是这很不符合直觉,reactive本身是深响应,forEach方法的回调函数所接收的参数也应该是响应式数据才对.我们需要对函数进行一些修改.
1 | const mutableInstrumentations = { |
至此,我们的工作还没有完成.可以发现,Map类型的forEach方法不仅仅遍历了key,也遍历的value,也就是说不仅仅是DELETE和ADD操作会影响遍历结果,SET操作也会影响遍历结果.
于是我们应该修改trigger方法弥补这一缺陷.
1 | function trigger(target, key, type, newVal) { |
这样就可以保证Map的forEach方法正常运作.
迭代器方法
集合类型有三个迭代方法
- entries
- keys
- value
调用这些方法会得到响应的迭代器,并且可以使用for…of进行迭代.
另外,由于Map和Set类型本身部署了Symbol.iterator
方法,因此它们也可以直接使用for…of进行迭代.
1 | for(const[key,value] of m) { |
当然我们也可以得到迭代器对象以后手动调用next方法,事实上m[Symblo.iterator]
与m.entries()
等价.
现在我们尝试代理迭代器方法.
1 | const p = reactive(new Map([ |
我们尝试调用代理对象p的 for…of方法,得到了p不可以迭代的错误.代理对象上当然没有迭代器方法,这就需要自定义返回原始对象上的迭代器属性.
1 | const mutableInstrumentations = { |
然而事情不可能这么简单,前文提到过,传给callback函数的参数应该但是包装后的响应式数据,同理,使用for…of循环迭代时,如果产生的值是可以被代理的,那么也应该包装成响应式数据.
1 | const mutableInstrumentations = { |
values 与 keys 方法
values方法的实现与entries方法类似,不同的是,当使用dor…of迭代的时候,得到的仅仅是Map数据的值,而非键值对.
1 | const mutableInstrumentations = { |
keys方法的实现与values方法类似,只要更改一行代码即可.
1 | const itr = target.keys() |
但是这样keys方法又一个缺陷,就是当操作类型为SET的时候,也会触发它的副作用函数,显然这是没必要的,遍历键并不关心其所对应的值的变化,所以keys方法可以和另一个KEY建立依赖联系.
1 | track(target, MAP_KEY_ITERATE_KEY) |
这样我们旧实现了依赖收集的分离.SET操作不会触发keys方法的副作用函数.因此我们再修改一下trigger方法.
1 | function trigger(target, key, type, newVal) { |
这样就能避免不必要的更新啦啦啦啦啦啦.
终于终于写完非原始值的响应式了我勒个豆啊累死了.
下一篇启动原始值的响应式
写于西13