简介
本文主要根据我的上篇表单专栏的# antd表单的值控制和渲染性能控制对比来看formily的渲染控制实现部分
设计目标
formily整个设计其实挺复杂的,架构分层比较多,整体看来主要是:框架无关(react、vue均可支持只需要设计上层胶水层即可目前已有),跨端能力(pc、移动同构),另外,作者终极目标其实不只是局限于表单想做一个低代码的能力底座,其实目前也通过designable实现了表单搭建的低码化,之前做的一个公司内的低代码系统也借鉴(抄)了designable源代码结合formily的能力实现一部分功能
formily设计架构图 之所以有低码能力是因为formily的底层schema的设计是可以支持纯UI的,也就是无需插入表单的字段值就单纯当做布局来用都是没任何问题, 实现原理是基于JSON Schema基础上拓展了void类型,具体实现可以参考源码
架构设计
formily的架构和其他表单方案(antd form、react-hook-form等)的核心区别是它使用了响应式的状态更新底座:类似mobox的@formily/reactive和基于MDN标准的schema基础之上定制的Schema协议
响应式保证了表单状态更新的精准性,schema协议则支持了跨端以及搭建拓展,和组件的实现完全解耦
下面重点看下状态更新实现
状态更新主要是在 reactive 和react-reactive两块代码里,前者是数据更新核心库,后者是react胶水层代码
reactive核心实现:
初始化observable
具体调用请参看packages/core/src/models/Field.ts,packages/core/src/models/Form.ts的makeObservable
ts
//packages/reactive/src/observable.ts
export function observable<T extends object>(target: T): T {
return createObservable(null, null, target)
}
创建Proxy
tsx
// packages/reactive/src/internals.ts
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
}
代理具体实现
上面代码中通过baseHandlers来进行设置get和set方法,这样组件初始化时候可以建立起proxy机制
tsx
//packages/reactive/src/handlers.ts
// 重点看get和set方法
export const baseHandlers: ProxyHandler<any> = {
get(target, key, receiver) {
......// 省略代码块
bindTargetKeyWithCurrentReaction({ target, key, receiver, type: 'get' })
...// 省略代码块
},
set(target, key, value, receiver) {
...// 省略代码块
if (!hadKey) {
runReactionsFromTargetKey({
target,
key,
value: newValue,
oldValue,
receiver,
type: 'add',
})
} else if (value !== oldValue) {
runReactionsFromTargetKey({
target,
key,
value: newValue,
oldValue,
receiver,
type: 'set',
})
}
...// 省略代码块
},
}
get核心 :bindTargetKeyWithCurrentReaction, 绑定Field、Form的初始化key,实现监听能力
- Field
- Form
set核心 : runReactionsFromTargetKey,给Field或者Form的绑定字段设置值时调用runReactions方法:
- 执行绑定到reaction上的**_scheduler**
tsx
const runReactions = (target: any, key: PropertyKey) => {
const reactions = getReactionsFromTargetKey(target, key)
const prevUntrackCount = UntrackCount.value
UntrackCount.value = 0
for (let i = 0, len = reactions.length; i < len; i++) {
const reaction = reactions[i]
if (reaction._isComputed) {
reaction._scheduler(reaction)
} else if (isScopeBatching()) {
PendingScopeReactions.add(reaction)
} else if (isBatching()) {
PendingReactions.add(reaction)
} else {
// never reach
if (isFn(reaction._scheduler)) {
reaction._scheduler(reaction)
} else {
reaction()
}
}
}
UntrackCount.value = prevUntrackCount
}
- 然后调用packages/reactive/src/tracker.ts的 Track实例的_scheduler方法,
tsx
export class Tracker {
private results: any
constructor(
scheduler?: (reaction: Reaction) => void,
name = 'TrackerReaction'
) {
this.track._scheduler = (callback) => {
if (this.track._boundary === 0) this.dispose()
if (isFn(callback)) scheduler(callback)
}
}
}
- 最后调用tracker回调函数执行forceUpdate进行组件更新(Field or Form)
tsx
// 关注react-reactive 内实例化的 Tracker类
export const useObserver = <T extends () => any>(
view: T,
options?: IObserverOptions
): ReturnType<T> => {
const forceUpdate = useForceUpdate()
const tracker = useCompatFactory(
() =>
new Tracker(() => {
if (typeof options?.scheduler === 'function') {
options.scheduler(forceUpdate)
} else {
forceUpdate()
}
}, options?.displayName)
)
return tracker.track(view)
}
适用场景/业务选型
双端开发 (h5和pc同构)
中后台业务领域中 pc端和移动端的表单部分一般区别都只是交互层面,而交互层的核心区别又在双端各自的组件实现,所以这点是可以双端解耦的,从表单值的回填提交、校验都是可以复用的,部分结构层的差异也可通过判断逻辑来进行区分
复杂业务(多层嵌套、多联动校验、跨表单联动)
深层嵌套逻辑多联动如果用普通表单解决方案写起来代码会显得比较脏,实现层都在jsx里,而formily的不管是主动联动还是被动联动都可以写在schema内统一管理,组件内Jsx无需关心; 跨表单联动的性能也能精准的O(1)时间复杂度的更新
小结
国内中后台业务react体系主要是用antd组件库系列,而atnd design在 4.0之前的版本表单方案比较拉胯,每个表单项的更新都会引起整个表单的re-render,导致数据量大的情况下基本没法用,4.0版本解决了这个问题后基本上常规表单复杂表单都能覆盖了,所以常规写业务来说也没有什么问题基本上;所以当你没有跨端、没有跨表单的性能要求以及搭建需求的话其实完全够用了。
formily上手的确有一定学习成本对于新手来说,不过借助desinable设计器还是比较快的,学习曲线也没那么陡峭; 而且对于中后台场景有低码搭建需求的业务来说,是有非常大的帮助的,这个可以从校验的配置、联动配置等等方面都比较方便,具体后面再低代码专栏里再进一步讨论