@formily/reactive 源码解析
从了解 @formily/reactive 源码洞悉 reactive 的原理
从了解 @formily/reactive 源码洞悉 reactive 的原理
首先可以从官方文档和Formily 的 Reactive 的经验汇总认识一下@formily/reactive,了解常规使用方法。作者在文档中其实概括了 reactive 的基本概念:
reaction 在响应式编程模型中,它就相当于是可订阅对象的订阅者,它接收一个 tracker 函数,这个函数在执行的时候,如果函数内部有对 observable 对象中的某个属性进行读操作(依赖收集),那当前 reaction 就会与该属性进行一个绑定(依赖追踪),知道该属性在其他地方发生了写操作,就会触发 tracker 函数重复执行。
可能是 MobX Plus 的响应式状态管理方案这篇文章也写的很好,作者非常详细阐述了@formily/reactive的特点并与mobx进行了对比,可以带着这些特点去源码里看看是如何实现的。
由于 MobX 的代码量过大,并且由于 @formily/reactive 作者同时吸取了 @vue/reactive 和 MobX 的优势,所以暂时从 @formily/reactive 中学习 Reactive 数据流的理念。
@formily/reactive 中有几个出场率很高的全局变量:
{ProxyObj: Obj} 映射,方便查找源数据{Obj: ProxyObj} 映射,方便查找 Proxy 数据{Obj: DataNode} 映射,Node 上记录了 Raw 的路径、值等信息变量的劫持都记录在这些全局变量中,所以我们需要保证同一个应用中,使用的是同一份 reactive 的代码。目前 yarn 安装 node_modules 其实是存在多个依赖版本的问题,就会导致预期外的情况发生。
首先检查 ProxyRaw 中有没有该值,如果有说明已经处理过了,可以直接返回。如果没有时,通过buildDataTree将该 obj 存入RawNode中。此时数据为:
// RawNode
{
[obj]: {
target: undefined,
key: undefined,
value: obj
}
}
对于 PlainObject 和 Array 类型,会再调用 createNormalProxy,对于 Map,Set 类型会调用
createCollectionProxy 处理成 ProxyObj,否则直接返回。
在这两个创建方法中将 obj 通过 Proxy 生成 proxyObj,存入 ProxyRaw 和 RawProxy,并返回了
proxyObj,此时内部数据为:
// ProxyRaw
{
[proxyObj]: obj
}
// RawProxy
{
[obj]: [proxyObj]
}
所以我们通过 observable 将obj在内部进行了处理,存入 RowNode 中,并返回了 Proxy 处理后的
ProxyObj。
当对变量调用 observable 之后,则会通过 observer 方法包装组件,使组件达到响应式的效果。
observer 接受FunctionComponent参数,对 React 组件进行了一些常规处理,如处理 forwardRef,
displayName, 复制组件属性等。然后返回了通过useObserver包装的FunctionComponent。
return useObserver(() => component(props), realOptions)
这个方法算是比较关键的环节,它连接了 React 和 reactive 内部管理的数据,并追踪变化最终起到响应式更新的作用。
Tracker 生成了 tracker 实例tracker.track 包装的组件,通过该方法将当前组件加入 追踪池ProxyObj
(我们后面再来看调用new Proxy时还做了什么)Tracker 接受 scheduler 方法 (可以理解为组件的更新方法) 和组件名称作为构造参数。
_scheduler 属性
将更新视图的方法存到了_scheduler属性上,在 React 中默认为组件的 forceUpdate。
track 方法
在这个方法里,首先会先判断 ReactionStack 是否包含
this.track,这一步是为了处理防止重复多次触发渲染的情况,每次触发 track 方法相当于重新渲染该组件。
然后调用参数进行视图渲染,当进入渲染逻辑时,向 ReactionStack 添加
this.track,从而标记该track正在执行,执行完成后移除。
在这里回头看一下对于创建 observable 对象时,根据不同的原始类型调用不同的
handlers(本文主要以 get/set 举例)
get
当调用get时,说明该对象已经开始被使用,需要在后续的改动中响应它的变化(通过
bindTargetKeyWithCurrentReaction ),同时在本次get中返回当前值。
对
get进行断点 debug 时,比如在访问数组时,会依次访问length、各个序号的key
set
当调用set时,将 target[key] 更新为
newValue,同时检测是否有订阅该key的,有的话需要执行响应。
上面提到,在get方法中,通过 bindTargetKeyWithCurrentReaction
添加订阅。在该方法中,会获取当前执行的 track 方法中接受的参数 tracker,同时将访问当前key的订阅行为记录到
reactionsMap[key] = tracker 存到到 RawReactionsMap[target]。
这样会把视图更新方法与对象的键进行关联,提供给修改对象中的变量时,能直接找到视图的更新方法。
在set方法中,根据target获取RawReactionsMap[target],再根据key获取reactions,便获得了之前加入到订阅中的所有reactions,其实就是tracker[],然后循环调用tracker._scheduler()更新订阅该target[key]的所有视图。
在这里,可以修改ProxyObj的值,并且通过全局对象的记录快速获取视图更新方法,对所有视图进行更新。