formily的状态更新机制

简介

本文主要根据我的上篇表单专栏的# 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方法:

  1. 执行绑定到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
}
  1. 然后调用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)
    }
  }
}
  1. 最后调用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设计器还是比较快的,学习曲线也没那么陡峭; 而且对于中后台场景有低码搭建需求的业务来说,是有非常大的帮助的,这个可以从校验的配置、联动配置等等方面都比较方便,具体后面再低代码专栏里再进一步讨论

相关推荐
星环科技10 小时前
数据标准Agent ,让企业数据说同一种语言
java·开发语言·前端
橘子星10 小时前
深入理解 AJAX 中的 JSON 序列化与 JS 异步处理
前端·javascript·后端
旧曲重听110 小时前
2026前端技术从「夯」到「拉」
前端·程序人生·职场和发展·软件工程
Kapaseker10 小时前
我找到了最适合程序员的 PPT 工具 — Slidev
前端
雾削木10 小时前
B语言经典教程现代化重构
java·前端·stm32·单片机·嵌入式硬件
Cobyte10 小时前
20.Vue Vapor 的应用初始化
前端·javascript·vue.js
乘风gg10 小时前
手把手带你实践历时一年总结的 AI Code Review 最佳工作流!
前端·ai编程·cursor
禅思院10 小时前
POST请求发两次?一次讲透CORS预检机制,面试不再翻车
前端·架构·前端框架
IT_陈寒10 小时前
SpringBoot自动配置这么智能,为啥我写的Bean注入不了?
前端·人工智能·后端
LT101579744410 小时前
2026年Web自动化测试工具选型指南:多浏览器兼容解决方案
前端·测试工具·自动化