@formily/reactive 源码解读(6)

929 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

关键API-model & define

model 和 define 有紧密关系,他们的实现还充分使用了之前提到的 annotation。

对于 model API,在官档中有这样的解释:

model 快速定义领域模型,会对模型属性做自动声明

  • getter/setter 属性自动声明 computed
  • 函数自动声明 action
  • 普通属性自动声明 observable

按照惯例,先来看一下他的使用方式 🧙🏼。

  1. model

    import { model, autorun } from '@formily/reactive'
    
    const obs = model({
      aa1,
      bb2,
      get cc() {
        return this.aa + this.bb
      },
      update(aa, bb) {
        this.cc
      },
    })
    
    autorun(() => {
      console.log(obs.cc)
    })
    
    obs.aa = 3
    
    obs.update(46)
    
  2. 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(12)
    model.action(12// 重复调用不会重复响应
    model.action(34)
    

由上面的例子我们可以看出,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,其流程如下:

  1. 获取 annotations 中的每个值,即 annotation 。
  2. 判断 annotation 是否是可执行的 annotation 函数。
  3. 如果是的话,就从其 MakeObservableSymbol 属性中获取 observableMaker 函数。
  4. 传入当前的 target 和 key,执行 observableMaker 函数。

还有一个地方也值得注意:

ProxyRaw.set(target, target)
RawProxy.set(target, target)

在 createObservable 的实现中,当创建 proxy 后,有类似 set 操作,其目的是为了让原对象和代理对象互为索引。在这里,其实原对象和代理对象都是同一个了,因为在不同类型 annotation 的实现中,使用 defineProperty 实现,返回的仍然是原对象。

关键API-autorun & reaction

autorun

autorun 在前文中已经充分讲解过了,在这里主要探究一下其上的两个方法。

  • memo
  • effect

相信熟悉 React 的同学看到这里应该有一些联想了,确实,这和 useMemouseEffect 有一些相似的地方 😲。

在此处贴一下 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 中,主要的执行流程如下:

  1. 执行 Reaction, 并记录 tracker 函数的返回值。

  2. 比较 tracker 执行的新返回值和之前的记录值,决定是否执行 callback。若两个值不一样,则执行以下流程。

    1. batchStart
    2. 将新值作为参数传递给 callback 并执行
    3. batchEnd
  3. 更新 tracker 执行的记录值。

如此可见,Reaction 可通过 batch 机制和 scheduler 调度 tracker 的执行。