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

相关推荐
Myli_ing29 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维1 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~1 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜2 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4042 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish2 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five2 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序2 小时前
vue3 封装request请求
java·前端·typescript·vue