@formily/reactive 源码解读(5)

517 阅读5分钟

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

关键函数-createAnnotation

这个函数从变量名看是创建注解的意思,在代码中多次被其他 API 调用,我们先来看一下这个函数使用的地方。

在项目名为”annotations”的文件夹中,发现 observable 上的方法:box、computed、ref 等都是通过调用 createAnnotation 的方式进行定义的。比如在 shallow 的定义中。

//@formily/reactive/src/annotations/shallow

export const shallowIObservable = createAnnotation(
  ({ target, key, value }) => {
    const store = {
      valuecreateObservable(target, key, target ? target[key] : value, true),
    }
    function get() {
      // 代码省略
    }
    function set(value: any) {
      // 代码省略
    }
    if (target) {
      Object.defineProperty(target, key, {
        set,
        get,
        enumerabletrue,
        configurablefalse,
      })
      return target
    }
    return store.value
  }
)

// @formily/reactive/src/shallow
import * as annotations from './annotations'
observable.shallow = annotations.shallow

而 observable.shallow 存在两种调用方式。

  1. 作为 observable 上的方法输出为外部调用的API。

     import { observable, autorun } from '@formily/reactive'
    
     const obs = observable.shallow({
      aa: {
        bb111,
      },
     })
    
     autorun(() => {
      console.log(obs.aa.bb)
     })
    
     obs.aa.bb = 222 // 不会响应
     obs.aa = { bb333 } // 可以响应
    

    如果是这样的调用方式,shallow 的 maker 函数中,其实 target 和 key 参数都是 null,只是传递了 value 参数(目的在于将 value 改造为浅观察模式的 observable 对象)。

  2. 其次就是在自定义的领域模型中,这一块会在之后的 model & define API 部分讲解,可以有个大概印象,在这种情况下,shallow 的 maker 函数中,target 就是领域模型对象,key 是被标注的变量名,标注(annotation)就是 shallow,需要被改造为 shallow 模式的 observable 就是被标注的变量值。

    这种情况,还有一个地方也值得注意,那就是在这里使用了 defineProperty 对 target 的 get、set分别定义。当使用这个领域模型的被标注的属性时,会触发 get 或者 set。get 和 set 的实现和 createObservable 中代理对象 proxy 的 handler 的实现差不多,这里之所以使用 defineProperty 就是为了在领域对象内外引用时访问的都是其本身。返回 proxy 的方式适用于在被代理对象的外部进行引用。

上面主要简单介绍了 createAnnotation 的使用方式,并且顺带看到了 observable.shallow 的具体实现。

而createAnnotation 的定义如下:

export const createAnnotation = <T extends (visitorIVisitor) => any>(
  maker: T
) => {
  const annotation = (targetany): ReturnType<T> => {
    return maker({ value: target })
  }
  if (isFn(maker)) {
    annotation[MakeObservableSymbol] = maker
  }
  return annotation
}

它接收一个 maker 函数,返回一个(接收单个 target 参数作为 maker 函数参数中的 value 并)执行 maker 的函数。

并且,返回的函数上有一个 symbol 类型的标志属性,属性值存储 maker 函数。这个属性的作用似乎就在讲“我是一个 annotation” 😲,当然,这个标志在之后鉴定 annotation 非常有用。对于这种高阶函数,一定要想想他被调用的过程。那么我们回到 shallow 的定义,可以回答 shallow 是什么,shallow 接受一个 value 参数,使用内部既定的 maker 逻辑,返回 maker 执行的结果

总结一下,

  1. createAnnotation 负责创建 annotation。
  2. observale.shallow、observable.deep 等都是 annotation。
  3. annotation 是一个包含了既定的 maker 逻辑的函数,接受 value 参数,返回 maker 执行结果。
  4. maker 函数是将 value 参数改造为某种形式的 observable 对象。
  5. annotation 函数上包含了一个标志属性,表明这是一个“annotation” 😅。

关键API-batch

从这一部分开始,我们将学习 reactive 提供的关键 API 的实现,首先就从 batch 开始吧。

首先来看一下 example!

import { observable, autorun, batch } from '@formily/reactive'

const obs = observable({})

autorun(() => {
  console.log(obs.aa, obs.bb, obs.cc, obs.dd)
})

batch(() => {
  obs.bb = 321
  obs.dd = 'dddd'
})

这个例子很简单,含义为 batch 中对 observable 对象的操作只会触发一次 autorun 中的执行。其实到这就可以猜到,batch API 的实现应该和源码中的 batchStartbatchEnd 等脱不了关系。

果然,我们可以发现 batch 的定义如下:

// @formily/reactive/src/batch

export const batch: IBatch = createBoundaryAnnotation(batchStart, batchEnd)

至于这个 createBoundaryAnnotation 是什么呢,我们转到其定义。

export const createBoundaryAnnotation = (
  start: (...args: any) => void,
  end: (...args: any) => void
) => {
  const boundary = createBoundaryFunction(start, end)
  const annotation = createAnnotation(({ target, key }) => {
    target[key] = boundary.bound(target[key], target)
    return target
  })
  boundary[MakeObservableSymbol] = annotation
  boundary.bound[MakeObservableSymbol] = annotation
  return boundary
}

首先,函数内部调用 createBoundary 创建了一个 boundary 变量。createBoundary 定义如下:

export const createBoundaryFunction = (
  start: (...args: any) => void,
  end: (...args: any) => void
) => {
  function boundary<F extends (...argsany) => any>(fn?: F): ReturnType<F> {
    let resultsReturnType<F>
    try {
      start()
      if (isFn(fn)) {
        results = fn()
      }
    } finally {
      end()
    }
    return results
  }

  boundary.bound = createBindFunction(boundary)
  return boundary
}

export const createBindFunction = <Boundary extends BoundaryFunction>(
  boundary: Boundary
) => {
  function bind<F extends (...argsany[]) => any>(
    callback?: F,
    context?: any
  ): F {
    return ((...args: any[]) =>
      boundary(() => callback.apply(context, args))) as any
  }
  return bind
}

从而可以解释这个 boundary 是接收一个位于 start 和 end 之间执行的参数函数,并返回该参数函数执行结果的函数。而 batch 函数就是一个 boundary。

因此,我们可以解释上面的例子,在执行被 batch 包裹的这段代码时,

() => {
  obs.bb = 321
  obs.dd = 'dddd'
}

首先会执行 batchStart,此时处于 isBatching 状态,触发的 Reaction 均被存入 PendingReactions 中等待执行。换句话说,修改obs.bb后并不会触发 autorun 。最后执行 batchEnd,将暂存 Reactions 依次取出,由于 ArraySet 的特殊性,两个相同的 Reaction 只有最开始的那次被添加到 PendingReactions 中,所以只存在 autorun 的一次触发。

当然,在 createBoundary 中还有关于 annotation 的额外逻辑。具体而言,就是这个 boundary.bound,绑定 callback 和调用对象。这一部分更适合在 model & define 中讲解,当 batch 用作 annotation 时(一般是在 define 中使用时),这个 annotation 内部的逻辑(也是其 maker )是:

  1. 使用 boundary 的形式调用 callback(包含对 observable 对象属性的修改操作)。什么是 boundary 的形式呢,就是让 callback 的执行位于 start 和 end 函数之间执行。
  2. 调用 callback 的对象 this 指向 annotation 接受的 target 参数。