Formily 原理浅析

本文旨在帮助理解 Formily 的工作原理,尤其是响应式机制。Formily/reactive 是一个独立的状态管理工具,使用 Proxy 实现响应式更新。Formily/core 负责表单数据状态和生命周期,json-schema 解析 JSON 格式的表单数据,而 Formily 的 UI 框架如 antd 则负责组件渲染。with 语句在 Formily 中用于作用域链扩展,实现字段间的联动操作。

遇见

和 Formily 相识比较偶然,最初是由于 formily/reative 才知道的 Formily。随后便在项目中逐步使用起来,在内部推行过程中发现大家一致觉得 Formily 的上手和理解成本很高,为了帮助大家更好的理解 Formily, 才有了这篇文章

这篇文章适用于对 Formily 和表单有基础使用经验的小伙伴,文章里不会对 Formily 的基础概念做介绍,而是会更侧重于 Formily 的原理层面

Formily 架构

reactive

formily/reactive 是个”独立“的响应式状态管理工具,你可以单独在 vue 或者 react 项目中单独使用,他类比于 mobx 这样的状态管理工具,但是 reactive 本身更简洁和通用,另外 reactive 是 Formily 做到组件级别精准刷新的核心

formily/reactive 的实现思路和 Vue 3 很像,都是借助了 Proxy 这个新特性来实现的,每个状态都会被 Proxy 化,当一个组件使用到这个状态时,这个组件的 render funciton 会被放置到这个状态的『依赖筐』里,当状态改变时,我们会把『依赖筐』里的所有组件的 render funciton 都拿出来执行一遍,从而来实现响应式刷新。

关于响应式感兴趣的小伙伴可以看下这篇文章:从零开始撸一个「响应式」框架

从个人角度来看,formily/reactive 比其他状态管理工具更能让使用方关注业务本身的逻辑。为什么这么说呢?是因为我在用 formily/reactive 时,借助 model 方法,我不用关心 『响应式』声明的,我就是像写普通 JS 一样写我的业务模型,比如:

import { model } from '@formily/reactive';

class Biz {bizData = {};fetchData() {http.call().then((res) => {this.bizData = this.transform(res);})}transform() {}
}

export defautl model(new Biz()) 

如果使用其他状态管理工具,他们会直接或间接的对我 Biz 业务模型的本身做侵入,而且他们的接入方式很复杂,就像是 redux 我总是搞不明白接入它需要几个步骤。

另外 formily/reactivemobx 在设计模式和目标上是十分相像的,他们都致力于帮助使用方搭建模型,引用 mobx 关于领域 store 设计的一篇文章 定义数据存储,感兴趣的小伙伴可以阅读下

core

formily/core 负责以下工作

  • 提供 Form 以及各种类型的 Field 类,用来记录表单数据状态和操作函数
  • 提供 Form、Field 生命周期钩子函数

core 的定位是通用的 JS 表单模型,和任何框架无关,他的职责都是围绕着表单数据状态和生命周期而展开的。这里不会有特别复杂的操作逻辑,也和我们理解整个 Formily 的流程没有太大关系,这里就不展开说明了

json-schema

当我们使用 schema 来描述表单时,我们会把类似下面的 object 传给 SchemaField组件:

const schema = {type: 'object',properties: {field1: {type: 'string',title: '字段1','x-component': 'Input'}}
} 

SchemaField组件内部会使用 JSON-schema来对 schema 进行初始化解析,解析后形成的一个个 item 会被 UI 框架使用(formily/reactformily/vue),如何使用会在下节中讲解,这边你只需要知道 json-schema 是将 schema 对象翻译成组件可识别的内容。 另外,我们经常会在 schema 里做些联动操作,比如像是这样:

'x-reactions': {dependencies: ['maxLength'],fulfill: {state: {selfErrors:'{{$deps[0] && $self.value && $self.value > $deps[0] ? "长度范围不合法" : ""}}',},},
}, 

formily 提供了一种可以在 schema 里写 js 语法的能力 {{ js 语法 }},同时我们能通过一些内置的“全局”变量来获取当前字段的值、依赖的值。formily 的这种能力主要通过 JS 沙箱来实现的,其核心代码就是这样的:

new Function('$root', `with($root) { return (${expression}); }`)(scope
) 

这里使用到了一个 js 里不常用的关键字 with,在 MDN 上的介绍

with

with 语句 扩展一个语句的作用域链。

语法

with (expression) { statement }

  • expression: 将给定的表达式添加到在评估语句时使用的作用域链上。表达式周围的括号是必需的。
  • statement: 任何语句。要执行多个语句,请使用一个块语句 ({ … }) 对这些语句进行分组。

我们经常在 JS 沙箱中使用到 with,因为他能提供一个指定的作用域链以及隔离能力。 回到 fomily 上,我们在上面的 demo 里写的 selfErrors 对应的值,在最终执行时就是 with 里的 statemet,而 experssion 是被 formily 包装后的对象,包括了:

  • $recrod: 当前记录数据
  • $index: 当前记录索引
  • $deps: 字段依赖的值

当然这部分的源码比较复杂,因为 formily 还需要收集各种 experssion,同时执行时还需要和 reactive 结合,感兴趣的小伙伴可以阅读下 json-schema compiler 相关的源码

react

在上面的 json-schema 章节里我们有讲到,SchemaField组件内部会使用 JSON-schema来对 schema 进行初始化解析,然后 SchemaField会根据解析后的 type 进行递归渲染,如下

我们对 schema 解析后根据 type 递归渲染的流程做下细分介绍:

  • object:即 type: 'object'这时 formily 内部会遍历 object 的值,再循环调用 RecursioField组件递归渲染每个 object 下每个 key 对应的 value* array: 即 type: 'array',开发时都会配合 ArrayTableArrayItems这样的高级 Array 组件使用(即 x-component: ArrayTable),formily/react 这时会直接调用 ArrayTable 来做渲染。然后会在ArrayTable 组件里面对 schema.items 进行解析。我们通常会在 items 配置 ArrayTable各个列的 schema,所以 items 本身就是个包含一个或多个表单字段的可递归对象,然后 ArrayTable 便会遍历 items 来分别调用 RecursioField进行递归渲染。> 这里讲的 ArrayTable 就是 formily/antd 这样的,由 formily 二次封装过后的 UI 组件库* void: 即 type: 'void'他是空无字段,即没有具体的业务含义的纯 UI 容器的字段组件,formily 在遇到这种情况时会使用 RecursioField递归渲染 voidproperties 对应的内容* other: 即 type: 'string、number、boolean'这些都是非引用类型,formily 内部会直接调用 Field组件。这里算是所有 type 的最终归宿,Field 组件会稍后给大家介绍整体看下来我们不难发现所有类型的 type 通过递归后最终都会到 other 这个 type 上,并使用 Field 渲染,Field实现很简单,其内部是直接调用 ReactiveField 组件,我们来看下 ReactiveField 这个核心组件的实现逻辑
// 去除了大部分实现逻辑,只看组件流程
const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {const components = useContext(SchemaComponentsContext)if (!props.field) {return <Fragment>{renderChildren(props.children)}</Fragment>}const field = props.field// 这里获取组件的 childrenconst content = mergeChildren(renderChildren(props.children, field, field.form),field.content ?? field.componentProps.children)if (field.display !== 'visible') return nullconst getComponent = (target: any) => {return isValidComponent(target)? target: FormPath.getIn(components, target) ?? target}// 根据 schema 里的 x-decorate 来获取对应组件const renderDecorator = (children: React.ReactNode) => {if (!field.decoratorType) {return <Fragment>{children}</Fragment>}return React.createElement(getComponent(field.decoratorType),toJS(field.decoratorProps),children)}// 根据 schema 里的 x-component 来获取对应组件const renderComponent = () => {if (!field.componentType) return content// 获取组件的 propsreturn React.createElement(getComponent(field.componentType),{// props},content)}return renderDecorator(renderComponent())
}

ReactiveInternal.displayName = 'ReactiveField'

export const ReactiveField = observer(ReactiveInternal, {forwardRef: true,
}) 

我在关键地方做了注释,这里就不详细介绍了。看完 ReactiveField 组件,我们发现他就做了两件事

  • 使用 React.createElement 渲染 x-decoratex-componet对应的组件
  • 使用 reactive-react 将该组件变成了响应式,这也就是为什么 formily 能做到组件级别的精准刷新的原因,因为他的响应式监听是最小粒度的

antd

这里的 formily/antd 泛指被 formily 封装过的 UI 组件库框架,他们主要做了以下几件事情:

  • antd 的每个表单组件变成 formily 的子组件,并通过 mapReadPretty增加组件的阅读态、通过 mapProps 来映射 props
  • 提供高级业务组件, 类似 FormDialog 这样的,聚合了 DialogForm 的高级组件,让开发者使用起来更方便
  • 配合 coreRecursioField做递归渲染,上面已经讲到

其实 formily/antd 本身做的事情不多,大家感兴趣的话可以自行阅读源码

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值