You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 把iterator劫持成响应式的iteratorfunctionpatchIterator(iterator){constoriginalNext=iterator.nextiterator.next=()=>{let{ done, value }=originalNext.call(iterator)if(!done){value=findReactive(value)}return{ done, value }}returniterator}
也是经典的函数劫持逻辑,把原有的{ done, value }值拿到,把value值定义成响应式proxy。
前言
在本系列的上一篇文章
带你彻底搞懂Vue3的响应式原理!TypeScript从零实现基于Proxy的响应式库。中,
我们详细的讲解了普通对象和数组实现响应式的原理,但是Proxy可以做的远不止于此,对于es6中新增的
Map
、Set
、WeakMap
、WeakSet
也一样可以实现响应式的支持。但是对于这部分的劫持,代码中的逻辑是完全独立的一套,这篇文章就来看一下如何基于函数劫持实现实现这个需求。
阅读本篇需要的一些前置知识:
Proxy
WeakMap
Reflect
Symbol.iterator (会讲解)
为什么特殊
在上一篇文章中,假设我们通过
data.a
去读取响应式数据data
的属性,则会触发Proxy的劫持中的get(target, key)
target就是
data对应的原始对象
,key就是a
我们可以在这时候给key:
a
注册依赖,然后通过Reflect.get(data, key)去读到原始数据返回出去。回顾一下:
而当我们的响应式对象是一个
Map
数据类型的时候,想象一下这个场景:读取数据的方式变成了
data.get('a')
这种形式,如果还是用上一篇文章中的get,会发生什么情况呢?get(target, key)
中的target是map原始对象
,key是get
,通过Reflect.get返回的是
map.get
这个方法,注册的依赖也是通过get
这个key注册的,而我们想要的效果是通过a
这个key来注册依赖。所以这里的办法就是
函数劫持
,就是把对于Map
和Set
的所有api的访问(比如has
,get
,set
,add
)全部替换成我们自己写的方法,让用户无感知的使用这些api,但是内部却已经被我们自己的代码劫持了。实现
我们把上篇文章中的目录结构调整成这样:
入口
首先看一下handlers/index.ts入口的改造
这里定义了一个Map:
handlers
,导出了一个getHandlers
方法,根据传入数据的类型获取Proxy的第二个参数handlers
,baseHandlers
在第一篇中已经进行了详细讲解。这篇文章主要是讲解
collectionHandlers
。collections
先看一下
collections
的入口:我们所有的handlers只有一个
get
,也就是用户对于map或者set上所有api的访问(比如has
,get
,set
,add
),都会被转移到我们自己定义的api上,这其实就是函数劫持的一种应用。那关键就在于
instrumentations
这个对象上,我们对于这些api的自己的实现。劫持api的实现
get和set
核心的
get
和set
方法和上一篇文章中的实现就几乎一样了,get
返回的值通过findReactive
确保进一步定义响应式数据,从而实现深度响应。至此,这样的用例就可以跑通了:
接下来再针对一些特有的api进行实现:
has
add
add就是典型的新增key的流程,会触发循环相关的观察函数。
delete
delete也和上一篇中的deleteProperty的实现大致相同,会触发循环相关的观察函数。
clear
在触发观察函数的时候,针对clear这个type做了一些特殊处理,也是触发循环相关的观察函数。
clear
的时候,把每一个key收集到的观察函数都给拿到,并且把循环的观察函数也拿到,可以说是触发最全的了。逻辑也很容易理解,
clear
的行为每一个key都需要关心,只要在observe函数中读取了任意的key,clear的时候也需要重新执行这个observe的函数。forEach
到了forEach的劫持 就稍微有点难度了。
首先
registerRunningReaction
注册依赖的时候,用的key是iterate
,这个很容易理解,因为这是遍历的操作。这样用户后续对集合数据进行
新增
或者删除
、或者使用clear
操作的时候,会重新触发内部调用了forEach
的观察函数重点看下接下来这两段代码:
wrappedCb包裹了用户自己传给forEach的cb函数,然后传给了集合对象原型链上的forEach,这又是一个函数劫持。用户传入的是map.forEach(cb),而我们最终调用的是map.forEach(wrappedCb)。
在这个wrappedCb中,我们把cb中本应该获得的原始值value通过
findObservable
定义成响应式数据交给用户,这样用户在forEach中进行的响应式操作一样可以收集到依赖了,不得不赞叹这个设计的巧妙。keys && size
由于
keys
和size
返回的值不需要定义成响应式,所以直接返回原值就可以了。values
再来看一个需要做特殊处理的典型
这里有一个知识点需要注意一下,就是集合对象的values方法返回的是一个迭代器对象Map.values,
这个迭代器对象每一次调用
next()
都会返回Map中的下一个值,为了让next()得到的值也可以变成
响应式proxy
,我们需要用patchIterator
劫持iterator
也是经典的函数劫持逻辑,把原有的
{ done, value }
值拿到,把value值定义成响应式proxy
。理解了这个概念以后,剩下相关几个handler也好理解了
entries
对应
entries
也有特殊处理,把迭代器传给patchIterator
的时候需要特殊标记一下这是entries
,看一下patchIterator
的改动:entries操作的每一项是一个[key, val]的数组,所以通过下标[1],只把值定义成响应式,key不需要特殊处理。
Symbol.iterator
这里又是一个比较特殊的处理了,
[Symbol.iterator]
这个内置对象会在for of
操作的时候被触发,具体可以看本文开头给出的mdn文档。所以也要用上面的迭代器劫持的思路。patchIterator的第二个参数,是因为对
Map
数据结构使用for of
操作的时候,返回的是entries结构,所以也需要进行特殊处理。总结
本文的代码都在这个仓库里
https://github.com/sl1673495/proxy-reactive
函数劫持的思路在各种各样的前端库中都有出现,这几乎是进阶必学的一种技巧了,希望通过本文的学习,你可以理解函数劫持的一些强大的作用。也可以想象Vue3里用proxy来实现响应式能力有多么强。
The text was updated successfully, but these errors were encountered: