持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
关键API-model & define
model 和 define 有紧密关系,他们的实现还充分使用了之前提到的 annotation。
对于 model API,在官档中有这样的解释:
model 快速定义领域模型,会对模型属性做自动声明
- getter/setter 属性自动声明 computed
- 函数自动声明 action
- 普通属性自动声明 observable
按照惯例,先来看一下他的使用方式 🧙🏼。
-
model
import { model, autorun } from '@formily/reactive' const obs = model({ aa: 1, bb: 2, get cc() { return this.aa + this.bb }, update(aa, bb) { this.cc }, }) autorun(() => { console.log(obs.cc) }) obs.aa = 3 obs.update(4, 6) -
define
import { define, observable, action, autorun } from '@formily/reactive' class DomainModel { deep = { aa: 1 } shallow = {} box = 0 ref = '' constructor() { define(this, { deep: observable, shallow: observable.shallow, box: observable.box, ref: observable.ref, computed: observable.computed, action, }) } get computed() { return this.deep.aa + this.box.get() } action(aa, box) { this.deep.aa = aa this.box.set(box) } } const model = new DomainModel() autorun(() => { console.log(model.computed) }) model.action(1, 2) model.action(1, 2) // 重复调用不会重复响应 model.action(3, 4)
由上面的例子我们可以看出,model 是一个领域模型,在这个模型对象中,其属性、方法被一定规则改造为 observable ,这个规则在官方文档中也指出了(见上方三条),是固定的。
而 define 可以用来自定义领域模型,define 可以定义一个类,在类的构造器中标注这个类的成员/方法应用了什么样的规则。
其实,到此就可以猜测,model 的源码实现中估计也调用了 define,model API是一个官方版本的领域模型。
ok 😀,现在首先来看一下 model 的源码部分:
// @formily/reactive/src/model
export function model<Target extends object = any>(target: Target): Target {
const annotations = Object.keys(target || {}).reduce((buf, key) => {
const descriptor = Object.getOwnPropertyDescriptor(target, key)
if (descriptor && descriptor.get) {
buf[key] = observable.computed
} else if (isFn(target[key])) {
buf[key] = action
} else {
buf[key] = observable
}
return buf
}, {})
return define(target, annotations)
}
果然,条件分支语句就是针对不同类型属性的“改造”规则了。
在这里生成的 annotations 对象有非常好玩的结构。annotations 的 key 是原对象(target)的 key,对应的值是不同类型的 annotation 函数。建议在这里回到之前的小节,看看有关 annotation 的定义。
最后,如我们所料,调用了 define。
那么,下面就是 define 部分的源码实现了:
export function define<Target extends object = any>(
target: Target,
annotations?: Annotations<Target>
): Target {
if (isObservable(target)) return target
if (!isSupportObservable(target)) return target
buildDataTree(undefined, undefined, target)
ProxyRaw.set(target, target)
RawProxy.set(target, target)
for (const key in annotations) {
const annotation = annotations[key]
if (isAnnotation(annotation)) {
getObservableMaker(annotation)({
target,
key,
})
}
}
return target
}
这部分代码最关键的地方在于遍历 annotations,其流程如下:
- 获取 annotations 中的每个值,即 annotation 。
- 判断 annotation 是否是可执行的 annotation 函数。
- 如果是的话,就从其
MakeObservableSymbol属性中获取observableMaker函数。 - 传入当前的 target 和 key,执行
observableMaker函数。
还有一个地方也值得注意:
ProxyRaw.set(target, target)
RawProxy.set(target, target)
在 createObservable 的实现中,当创建 proxy 后,有类似 set 操作,其目的是为了让原对象和代理对象互为索引。在这里,其实原对象和代理对象都是同一个了,因为在不同类型 annotation 的实现中,使用 defineProperty 实现,返回的仍然是原对象。
关键API-autorun & reaction
autorun
autorun 在前文中已经充分讲解过了,在这里主要探究一下其上的两个方法。
- memo
- effect
相信熟悉 React 的同学看到这里应该有一些联想了,确实,这和 useMemo、useEffect 有一些相似的地方 😲。
在此处贴一下 autorun.memo 的代码
autorun.memo = <T>(callback: () => T, dependencies?: any[]): T => {
if (!isFn(callback)) return
const current = ReactionStack[ReactionStack.length - 1]
if (!current || !current._memos)
throw new Error('autorun.memo must used in autorun function body.')
const deps = toArray(dependencies || [])
const id = current._memos.cursor++
const old = current._memos.queue[id]
if (!old || hasDepsChange(deps, old.deps)) {
const value = callback()
current._memos.queue[id] = {
value,
deps,
}
return value
}
return old.value
}
其作用就是,接收一个回调函数(callback)和一个依赖参数列表,当触发 autorun 时,比较依赖参数是否变化,如果有变化,则会执行 callback。类比一下 React,autorun 就好比组件的 render 方法,autorun.memo 就好比 useMemo,至少从功能上看,用法是比较相似的。
至于其内部是如何实现这样一种依赖收集和比较的过程呢,我理出了 Reaction 上的关键属性 _memos 的结构。
可见,_memos 上有一个 cursor,当传入的 deps 和 cursor 指向的 deps 比较,发现 deps 变化后,就会将新的 deps 放入 _memos.queue 中,并更新 cursor。而 _memos.queue 就是存储每次改变时的 deps 和对应的返回值 value。
Reaction._effects 的实现逻辑也类似,其队列中存储的变量有所不同。
reaction
前文多次剖析了 Reaction,和这里的 reaction API 是完全不一样的。官方对这个 API 的描述如下:
接收一个 tracker 函数,与 callback 响应函数,如果 tracker 内部有消费 observable 数据,数据发生变化时,tracker 函数会重复执行,但是 callback 执行必须要求 tracker 函数返回值发生变化时才执行。
先来看一下 reaction 的使用示例!
import { observable, reaction, batch } from '@formily/reactive'
const obs = observable({
aa: 1,
bb: 2,
})
const dispose = reaction(() => {
return obs.aa + obs.bb
}, console.log)
batch(() => {
//不会触发,因为obs.aa + obs.bb值没变
obs.aa = 2
obs.bb = 1
})
obs.aa = 4
dispose()
从使用示例可以看出,reaction 相较于 autorun 多了 callback 参数,在 callback 中会对 tracker 的返回结果进行校验以决定是否执行。reaction 这部分我只贴出了部分关键代码:
const reaction: Reaction = () => {
if (ReactionStack.indexOf(reaction) === -1) {
releaseBindingReactions(reaction)
try {
ReactionStack.push(reaction)
value.currentValue = tracker()
} finally {
ReactionStack.pop()
}
}
}
reaction._scheduler = (looping) => {
looping()
if (dirtyCheck()) fireAction()
value.oldValue = value.currentValue
}
上面的代码中出现了 reaction._scheduler 的定义,在之前 reaction 执行的代码中,我们见过相关逻辑,如下代码所示。
if (isFn(reaction._scheduler)) {
reaction._scheduler(reaction)
} else {
reaction()
}
因此,在 reaction._scheduler 中,主要的执行流程如下:
-
执行 Reaction, 并记录 tracker 函数的返回值。
-
比较 tracker 执行的新返回值和之前的记录值,决定是否执行 callback。若两个值不一样,则执行以下流程。
- batchStart
- 将新值作为参数传递给 callback 并执行
- batchEnd
-
更新 tracker 执行的记录值。
如此可见,Reaction 可通过 batch 机制和 scheduler 调度 tracker 的执行。