持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
主流程01-可观察对象的创建
还是来看看使用 reactive 的最基本的代码。
import { observable, autorun } from '@formly/reactive';
const obs = observable({
a:{b: {1}}
})
autorun(()=>{
console.log(obs.a)
})
obs.a.b = 2
首先我们知道,通过 observable,我们可以一个普通的 JS 对象转换为一个可观察的对象;通过 autorun ,我们可以传入回调函数(即上文所说的 tracker 函数),在对象属性被修改的时机执行 tracker。
基于这个描述,你会怎样去实现这两个最核心的 API 呢,需要思考以下问题:
- 监听函数如何被调用。当 observable 被修改后,我们需要以某种方式调用监听函数。根据某些设计模式,可以直接调用,也可以通过发送消息来通知监听者。
- 监听函数使用何种方式被收集起来。
和一般的观察者或者发布/订阅模式有区别,在 reactive 提供的 API 中,我们并没有发现任何事件绑定或者触发事件的 API。那么,可以推测这个绑定和触发的机制更加隐晦,不需要开发者手动调用特定的 API 来实现。这个机制是在框架层面实现的,这也阐释了数据劫持的含义。
两个函数(observable、autorun)在执行后,唯一能够将他们联系起来的只有可观察对象本身。
实际上,也正是 observable 对象在进行属性访问和属性设置时,实现了监听函数的绑定和触发。非常隐晦,是因为这样的机制是基于 ES 语法层面的,具体就是 Proxy。
那么,ES6 中的 Proxy 提供了哪些能力呢 😲?
简单,官方文档中指出
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
如下面的代码所示,target 是被代理的原对象,handler 是对象属性操作的处理器,返回的是原对象的代理。
const p = new Proxy(target, handler)
当我们通过 proxy 进行对象属性操作时(读操作get、写操作set等),会由定义的 handler 进行统一处理。
自然地,这就启发我们,通过自定义 get 和 set 等属性操作方法,就可以在特定的时机进行监听函数的绑定和触发。相当于,我们使用 get 和 set 的方式和使用钩子函数一样。这样的话,observable 对象可以被描述为代理原对象(raw)且有 handler 中包含了绑定/触发监听函数逻辑的 proxy。
现在,我们就来看一下 observable 是如何被创建的吧!
这部分的关键代码如下:
// @formily/reactive/src/observable
import { createObservable } from './internals'
export function observable<T extends object>(target: T): T {
// 直接调用时,target、key参数均为 null
return createObservable(null, null, target)
}
// @formly/reactive/src/internals
export const createObservable = (
target: any,
key?: PropertyKey,
value?: any,
shallow?: boolean
) => {
if (typeof value !== 'object') return value
const raw = ProxyRaw.get(value)
if (!!raw) {
const node = getDataNode(raw)
if (!node.target) node.target = target
node.key = key
return value
}
if (!isSupportObservable(value)) return value
if (target) {
const parentRaw = ProxyRaw.get(target) || target
const isShallowParent = RawShallowProxy.get(parentRaw)
if (isShallowParent) return value
}
buildDataTree(target, key, value)
if (shallow) return createShallowProxy(value)
if (isNormalType(value)) return createNormalProxy(value)
if (isCollectionType(value)) return createCollectionProxy(value)
return value
}
下方是根据上述代码所做的流程图梳理:
这部分代码最主要的工作就是:
- 检查这个需要被代理的对象是否被创建过
buildDataTree- 检查是否是浅观察(shallow)模式 - 是,则调用
createShallowProxy - 检查需要被代理的对象是否是
NormalType- 是,则调用createNormalProxy - 检查需要被代理的对象是否是
CollectionType- 是,则调用createCollectionProxy
Q:上述描述中,这个**“需要被代理的对象**”在代码中为 value 参数。那么 target 和 key
又分别代表什么呢?在首次调用中为什么都是 null 呢?
A:实际上,target 指的是 value 的父级对象,key 指的是通过父级对象访问 value 的键值。那么在第一次调用中,由于传入的对象就是最顶层的对象(没有父级了),所以 target 和 key 的传值都是 null。而这里使用 target 和 key 主要是在buildDataTree 中使用(可简单理解他的作用是建立嵌套对象之间的树形结构,便于之后的路径查找)。
通过调用 createNormalProxy(value)、createCollectionProxy(value)、createShallowProxy(value) 即可返回新创建的 observable 对象。我们集中看一下 createNormalProxy 的实现:
const createNormalProxy = (target: any, shallow?: boolean) => {
const proxy = new Proxy(target, baseHandlers)
ProxyRaw.set(proxy, target)
if (shallow) {
RawShallowProxy.set(target, proxy)
} else {
RawProxy.set(target, proxy)
}
return proxy
}
这部分的代码很简单,涉及到两个关键点:
- 全局变量
ProxyRawRawProxy
baseHandler的实现
首先,全局变量的使用在 reactive 中非常普遍,我们通过 @formily/reactive/src/environment 文件可以找到所有的全局变量。其中,ProxyRaw 和 RawProxy 是两个 WeakMap 类型的集合(除此外,还有RawShallowProxy)。和它们的变量名非常贴合,ProxyRaw 表示代理对象映射原始对象,RawProxy表示原始对象映射代理对象。通过建立双向映射,极大方便了互相的查找,在文件中会随处见到从这两个 Map 中获取对象的逻辑。
然后,我们再看看 baseHandler 的实现(之所以是 base,是因为这个 handler 只处理普通的对象,不针对 Set、Map等特殊类型的对象),这部分的代码如下所示:
export const baseHandlers: ProxyHandler<any> = {
get(target, key, receiver) {
if (!key) return
const result = target[key] // use Reflect.get is too slow
if (typeof key === 'symbol' && wellKnownSymbols.has(key)) {
return result
}
bindTargetKeyWithCurrentReaction({ target, key, receiver, type: 'get' })
const observableResult = RawProxy.get(result)
if (observableResult) {
return observableResult
}
if (!isObservable(result) && isSupportObservable(result)) {
const descriptor = Reflect.getOwnPropertyDescriptor(target, key)
if (
!descriptor ||
!(descriptor.writable === false && descriptor.configurable === false)
) {
return createObservable(target, key, result)
}
}
return result
},
has(target, key) {
// 代码省略
},
ownKeys(target) {
// 代码省略
},
set(target, key, value, receiver) {
const hadKey = hasOwnProperty.call(target, key)
const newValue = createObservable(target, key, value)
const oldValue = target[key]
target[key] = newValue // use Reflect.set is too slow
if (!hadKey) {
// reaction here is like the effect of the setter.
runReactionsFromTargetKey({
target,
key,
value: newValue,
oldValue,
receiver,
type: 'add',
})
} else if (value !== oldValue) {
runReactionsFromTargetKey({
target,
key,
value: newValue,
oldValue,
receiver,
type: 'set',
})
}
return true
},
deleteProperty(target, key) {
// 代码省略
},
}
这部分代码稍微长一点,我主要摘取了 get 和 set 部分的实现,因为这是两个代表性的属性操作。
从上面的代码稍微总结一下他们分别的逻辑:
-
get
- 获取原对象的属性值
bindTargetKeyWithCurrentReaction({ target, key, receiver, type: 'get' })这个函数的就是绑定 reaction 的逻辑,非常关键- 检查获取的属性值是否是 observable 对象,进一步决定是否调用
createObservable以达到将这个值转换为 observable 的目的。(如果是 deep 模式且支持转换,访问嵌套较深的子对象时,访问路径上的每一层的对象都会被转换为 observable),再换句话说,就是访问父级对象属性时,将嵌套子对象也转换为 observable。
-
set
- 获取需要设置的新值(newValue)和旧值(oldValue)
- 区分当前属性操作是增添属性还是修改已有属性,分情况调用
runReactionsFromTargetKey这个函数也非常关键,包含了触发监听函数的逻辑。
在这里简单梳理一下
baseHandler.set的逻辑,如下图所示:
在接下来,我们会继续深入了解`bindTargetKeyWithCurrentReaction` 和`runReactionsFromTargetKey` 这两个关键函数 🧐。