formily2 源码学习

2,328 阅读4分钟

介绍

formily 是解决中台表单场景的JS库。详细查看官网

优势

  1. 解决了更丰富的表单业务场景
    1. 表单联动
    2. 异步数据源
    3. 表单场景化,例如分布表单、卡片等
  2. 性能较好
    1. 依赖于内部@formily/reactive 提供的数据管理,可以精准收集依赖数据的组件,修改数据后,做到精准打击,更新对应的组件。类似 vue 模板 templatedata 依赖收集的过程
  3. 支持 React、Vue
  4. 阿里团队出品,有足够的的后盾维护

架构

官方架构图:

image.png

以 React 为例:

  • Modules - 数据模型。用于描述表单。
  • Reactive - 数据响应。绑定依赖某个数据的表单和数据。
  • React - 渲染胶水层。绑定数据模型和具体组件。
  • Ant Design - 组件库。将 Ant Design 组件注册到 formily 中。

源码解析

以一个 React Demo 为例,介绍整体的执行过程。

formily 在渲染一个表单时,formily 执行过程示意图:

Untitled Diagram.drawio (4).png

对应的代码:

import React from 'react'
import { createForm } from '@formily/core'
import { Field, } from '@formily/react'
import {
  Form,
  FormItem,
  Input,
} from '@formily/antd'

const form = createForm({
  validateFirst: true,
})

export default () => {
  return (
    <Form
      form={form}
      labelCol={5}
      wrapperCol={16}
      onAutoSubmit={console.log}
    >
      <Field name="username"
        title="用户名"
        decorator={[FormItem]}
        component={[Input]}
      />
    </Form>
  )
}

下面针对执行阶段介绍下实现原理

createForm

创建一个 Form 实例,作为 ViewModel 给 UI 框架层消费。 文档源码

class Form {
    constructor () {
        this.values = {} // 所有表单字段值
        this.fields = {} // 所有表单字段组件
        ....
    }
}

Field 组件

FormItem 包裹 Input 渲染, 并传入 valueonChange ,使组件受控,更方便管理组件的状态。

import { Field } from '@formily/core'
import { observer } from '@formily/reactive-react'

const field = new Field(); // field 数据模型。为 component 提供 props

const props = {
    value: field.value,
    onChange() {
        field.onInput(...args)
    },
    ...
}

// observer 返回一个新组件。observer 主要做了两件事
// 1. 提供一个 forceUpdate 的方法,可以主动触发组件更新
// 2. 绑定 组件 和 field 的关系,让 field 的数据修改时,可能组件更新
const finallyComponent = observer(<FormItem>
    <Input {...props} />
</FormItem>)

@formily/core - Field

Field 的数据模型 文档源码

class Field {
  constructor(...) {
    ...
    this.makeObservable()
    ...
  }
  
  protected makeObservable() {
    define(this, {
      ...
      value: observable.computed // 使数据模型的 value 属性实现读取和设置的拦截和自定义
      ...
    })
  }
  
  // 组件的 onChange 会触发 field.onInput 方法,同步 value 属性
  onInput = async (...args: any[]) => {
    const values = getValuesFromEvent(args) // 获取输入的 value 值
    const value = values[0]
    ...
    this.value = value
  }
}

observable.computed 是通过 Proxy 代理 Fieldvalue 属性。

export const computed: IComputed = createAnnotation(
  ({ target, key, value }) => {
    const store: IValue = {}

    const proxy = {}
    const context = target ? target : store
    
    ...

    function get() {
      ...
      // 将当前的 数据对象 和 正在执行渲染的组件 进行关系的绑定
      bindTargetKeyWithCurrentReaction({
        target: context,
        key: property,
        type: 'get',
      })
      return store.value
    }

    function set(value: any) {
      try {
        // 这里修改数据时,会根据 数据对象获取依赖该数据的组件,进行更新。
        batchStart()
        setter?.call?.(context, value)
      } finally {
        batchEnd()
      }
    }
    
    if (target) {
      Object.defineProperty(target, key, {
        get,
        set,
        enumerable: true,
        configurable: false,
      })
      return target
    } else {
      ...
    }
    return proxy
  }
)

@formily/reactive-react - observer

observer 是一个高阶组件,使组件可以将数据和组件关系进行绑定。主要依赖的是两个函数 TrackeruseObserver

Tracker 主要的两个函数:

  1. 绑定数据需要跟踪的组件 track;
  2. 提供更新的组件的方法_scheduler(为了兼容不同框架,实际的更新方法通过 callback 方式实现)。
export class Tracker {
  private results: any
  constructor(
    scheduler?: (reaction: Reaction) => void,
    name = 'TrackerReaction'
  ) {
    // 此方法会触发组件的渲染, callback 为更新组件的函数
    this.track._scheduler = (callback) => {
      if (this.track._boundary === 0) this.dispose()
      if (isFn(callback)) scheduler(callback)
    }
  }

  // 参数 tracker 为一个组件
  track: Reaction = (tracker: Reaction) => {
    if (ReactionStack.indexOf(this.track) === -1) {
      try {
        // 添加当前的跟踪对象到 ReactionStack
        ReactionStack.push(this.track)
        // 渲染组件,读取 field 的属性时,触发属性读取操作的捕捉器。即上文提到的 computed
        this.results = tracker()
      } finally {
        ReactionStack.pop()
      }
    }
    return this.results
  }
  ...
}

useObserver 使组件可被追踪

export const useObserver = (
  view: T,
  options?: IObserverOptions
) => {
  const forceUpdate = useForceUpdate() // 强制更新的方案
  const trackerRef = React.useRef<Tracker>(null)
  if (!trackerRef.current) {
    // 将组件更新的方法传给跟踪函数 Tracker
    trackerRef.current = new Tracker(() => {
      if (typeof options?.scheduler === 'function') {
        options.scheduler(forceUpdate)
      } else {
        forceUpdate()
      }
    }, options?.displayName)
  }

  // 将组件传给跟踪函数,返回一个组件直接结果
  return trackerRef.current.track(view)
}

简单实现

// 模拟 一个组件
const component = (props) => {
  console.log(`组件更新:${props.name}`);
};

// 模拟 ReactionStack 储存渲染的组件列表
const components = [];

// 模拟 Field 数据模型
const values = {
  name: "初始数据",
};

// 模拟 RawReactionsMap 储存数据和组件依赖关系
const dataWithComponentMap = new WeakMap();

// 模拟 computed 为数据添加自定义捕捉器
const proxyValues = new Proxy(values, {
  get(target, key) {
    // 获取数据时,绑定关系
    dataWithComponentMap.set(target, components[components.length - 1]);
    return target[key];
  },

  // 更新数据时,根据绑定关系对象查找组件并更新
  set(target, key, value) {
    target[key] = value;
    const component = dataWithComponentMap.get(target);

    component(target);

    return true;
  },
});

// 模拟 observer
const Wrapper = ({ component }) => {
  return () => {
      components.push(component);
      component(proxyValues);
  }
};

const newComponent = Wrapper({
  component,
});

newComponent()

proxyValues.name = "更新数据";

更多收获

React

Provider

官方介绍。在进行第三方插件开发时,可以利用它很方便实现内部拓展。

例如

三方库 Field

provider.js

import React from 'react';

export const ComponentProvider = React.createContext();
import React, { useContext } from 'react';
import { ComponentProvider } from './provider';

const ComponentProvider = React.createContext();

const Field = ({ type }) => {
  const components = useContext(ComponentProvider)

  const component = components?.[type] || <div>内部组件</div>
    
  return component
}
内部使用
import React, { useContext } from 'react';
import Field, { ComponentProvider } from 'Field';

export default () => {
  // 这里就可以将自定义组件注入到三方库中
  return <ComponentProvider.Provider value={{
    customize: <div>自定义组件</div>
  }}>
    <Field type="customize" />
  </ComponentProvider.Provider>
}

Javascript

Proxy

轻松实现数据的依赖收集。MDN

weekMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被GC回收掉。WeakMap提供的接口与Map相同。

let obj = {}
const normalMap = new Map();
const weakMap = new WeakMap();

normalMap.set(obj, 'normalMap存在')
weakMap.set(obj, 'weakMap存在')

console.log(normalMap.get(obj));
console.log(weakMap.get(obj));

obj = null

console.log(normalMap.size);
console.log(weakMap.size);