@formily/reactive源码解读(2)

1,143 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 来实现。这个机制是在框架层面实现的,这也阐释了数据劫持的含义。

两个函数(observableautorun)在执行后,唯一能够将他们联系起来的只有可观察对象本身。

实际上,也正是 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
}

下方是根据上述代码所做的流程图梳理:

create-obs.png

这部分代码最主要的工作就是:

  1. 检查这个需要被代理的对象是否被创建过
  2. buildDataTree
  3. 检查是否是浅观察(shallow)模式 - 是,则调用 createShallowProxy
  4. 检查需要被代理的对象是否是 NormalType - 是,则调用 createNormalProxy
  5. 检查需要被代理的对象是否是 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
}

这部分的代码很简单,涉及到两个关键点:

  1. 全局变量
    1. ProxyRaw
    2. RawProxy
  2. 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

    1. 获取原对象的属性值
    2. bindTargetKeyWithCurrentReaction({ target, key, receiver, type: 'get' }) 这个函数的就是绑定 reaction 的逻辑,非常关键
    3. 检查获取的属性值是否是 observable 对象,进一步决定是否调用 createObservable 以达到将这个值转换为 observable 的目的。(如果是 deep 模式且支持转换,访问嵌套较深的子对象时,访问路径上的每一层的对象都会被转换为 observable),再换句话说,就是访问父级对象属性时,将嵌套子对象也转换为 observable。
  • set

    1. 获取需要设置的新值(newValue)和旧值(oldValue)
    2. 区分当前属性操作是增添属性还是修改已有属性,分情况调用 runReactionsFromTargetKey 这个函数也非常关键,包含了触发监听函数的逻辑。

    在这里简单梳理一下 baseHandler.set 的逻辑,如下图所示:

set.png

在接下来,我们会继续深入了解`bindTargetKeyWithCurrentReaction``runReactionsFromTargetKey` 这两个关键函数 🧐。