介绍
formily 是解决中台表单场景的JS库。详细查看官网
优势
- 解决了更丰富的表单业务场景
- 表单联动
- 异步数据源
- 表单场景化,例如分布表单、卡片等
- 等
- 性能较好
- 依赖于内部@formily/reactive 提供的数据管理,可以精准收集依赖数据的组件,修改数据后,做到精准打击,更新对应的组件。类似 vue 模板
template
对data
依赖收集的过程
- 依赖于内部@formily/reactive 提供的数据管理,可以精准收集依赖数据的组件,修改数据后,做到精准打击,更新对应的组件。类似 vue 模板
- 支持 React、Vue
- 阿里团队出品,有足够的的后盾维护
- 等
架构
官方架构图:
以 React 为例:
- Modules - 数据模型。用于描述表单。
- Reactive - 数据响应。绑定依赖某个数据的表单和数据。
- React - 渲染胶水层。绑定数据模型和具体组件。
- Ant Design - 组件库。将 Ant Design 组件注册到 formily 中。
源码解析
以一个 React Demo 为例,介绍整体的执行过程。
formily 在渲染一个表单时,formily 执行过程示意图:
对应的代码:
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
渲染, 并传入 value
和 onChange
,使组件受控,更方便管理组件的状态。
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
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
代理 Field
的 value
属性。
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
是一个高阶组件,使组件可以将数据和组件关系进行绑定。主要依赖的是两个函数 Tracker
和 useObserver
。
Tracker 主要的两个函数:
- 绑定数据需要跟踪的组件
track
; - 提供更新的组件的方法
_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);