- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
导读
FormConsumer 是 formily 的 Vue 包(@formily/vue)提供的「表单响应消费者」组件:它以 scoped slot 的形式把整个表单模型(Form实例)暴露给模板,当 slot 回调内所依赖的表单数据发生变化时,组件会自动重新渲染回调内容,从而实现「表单数据 → 任意 UI」的响应式联动。本文将基于 form-consumer.md 展开,结合 FormConsumer.ts、@formily/reactive-vue的 observer 机制以及核心包 Form 模型的源码实现,讲清它的使用方式、渲染原理、调度细节,并给出可直接运行的完整用例。
一、组件定位:从 FormProvider 到 FormConsumer
在 formily 的 Vue 体系中,表单上下文通过两层组件协作传递:
- FormProvider:入口组件,负责把通过
createForm()创建的Form实例以provide方式注入到组件树中,是整个表单状态的「通讯枢纽」。源码见 FormProvider.ts,其核心动作是provide(FormSymbol, formRef),FormSymbol定义于 context.ts。 - FormConsumer:响应消费者,位于表单组件树内部,通过
inject取出 FormProvider 注入的 form 实例,并以 scoped slot 参数的形式重新暴露给模板消费。
文档中 Form 模型 API 的完整参考见 Form 模型文档。
两者的分工与 React 版的FormProvider/FormConsumer一一对应,是「响应式状态源(form)」与「UI 消费方(模板)」之间的标准解耦方式。
二、快速上手:一个最小可用用例
FormConsumer 的完整使用方式可以从仓库中的实际 demo 得到验证(form-consumer.vue):
<template> <FormProvider :form="form"> <Field name="input" :component="[Input]" /> <FormConsumer> <template #default="{ form }"> {{ form.values.input }} </template> </FormConsumer> </FormProvider> </template> <script> import { Input } from 'ant-design-vue' import { createForm } from '@formily/core' import { FormProvider, Field, FormConsumer } from '@formily/vue' import 'ant-design-vue/dist/antd.css' export default { components: { FormProvider, Field, FormConsumer }, data() { return { Input, form: createForm(), } }, } </script>要点拆解:
form必须来自FormProvider的注入:FormConsumer 本身不接受formprop,它依赖FormProvider通过inject下发的表单上下文。没有 FormProvider 包裹时,useForm()注入到的将是空值,渲染结果不可用。- scoped slot 只暴露一个参数
form:slot 内部可以直接访问form.values、form.mounted、form.display、form.pattern等表单状态,也可以调用form.query(...)、form.getValuesIn(...)等方法做更细粒度的数据提取。 - 渲染内容的定位:FormConsumer 不产生任何额外的布局副作用——它内部渲染的是一个
display: contents的 div(见下文实现分析),因此 slot 内容会直接以「无包装节点」的方式参与父级布局。
更完整的带字段联动示例可以参考 demo 总入口 demos/index.vue,其中通过JSON.stringify(form.values, null, 2)实时展示整个表单的值快照。
三、FormConsumer 的源码实现剖析
FormConsumer 的完整实现非常精简,全文见 FormConsumer.ts:
import { defineComponent } from 'vue-demi' import { observer } from '@formily/reactive-vue' import { useForm } from '../hooks' import h from '../shared/h' export default observer( defineComponent({ name: 'FormConsumer', inheritAttrs: false, setup(props, { slots }) { const formRef = useForm() return () => { // just like <Fragment> return h( 'div', { style: { display: 'contents' } }, { default: () => slots.default?.({ form: formRef.value, }), } ) } }, }), { // make sure observables updated <cannot be tracked by tests> scheduler: (update) => Promise.resolve().then(update), } )3.1 取表单:useForm与FormSymbol
useForm是 FormConsumer 获取表单上下文的入口,实现于 useForm.ts:
export const useForm = (): Ref<Form> => { const form = inject(FormSymbol, ref()) return form }它从FormSymbol这个 InjectionKey 中取出Ref<Form>。而FormSymbol是由 FormProvider 在 setup 阶段provide的(context.ts):
export const FormSymbol: InjectionKey<Ref<Form>> = Symbol('form')因此 FormConsumer 拿到的formRef.value就是createForm()创建的同一个 Form 实例——这正是「消费者」与「状态源」直接对话的基础。
3.2 响应式渲染:observer 与 Tracker
FormConsumer 被observer(...)包裹,这是来自@formily/reactive-vue的高阶组件封装。observer 根据运行环境自动选择 Vue 2 / Vue 3 的实现(observer/index.ts):
export function observer<C>(baseComponent: C, options?: IObserverOptions): C { if (isVue2) { return observerV2(baseComponent, options) } else { return observerV3(baseComponent, options) } }Vue 3 的实现(observerInVue3.ts)会在组件 setup 阶段调用useObserver(options)。而useObserver(useObserver.ts)的核心机制是创建Tracker并覆写组件的 effect 运行函数:
tracker = new Tracker(() => { if (options?.scheduler && typeof options.scheduler === 'function') { options.scheduler(vmUpdate) } else { vmUpdate() } }) ... tracker?.track(() => { refn = vm['_updateEffectRun'].call(newValue) })关键点:
- 依赖追踪发生在渲染回调执行期间:slot 内容(即
slots.default?.({ form: formRef.value }))在渲染时读取的每一个可观察属性(如form.values)都会被 Tracker 记录为依赖。 - 依赖变化触发重渲染:当被读取的属性变化时,Tracker 触发调度器,最终调用
$forceUpdate()强制当前组件重新渲染,slot 回调随之重新执行,UI 即得到更新。 - 这正是文档中「当回调函数内依赖的数据发生变化时就会重新渲染回调函数」的底层实现依据:重渲染的粒度取决于回调内实际读到了哪些响应式数据。
3.3 scheduler:异步调度保证
FormConsumer 给 observer 传了一个调度器:
scheduler: (update) => Promise.resolve().then(update)含义是:当依赖数据变化时,不立即同步触发更新,而是放入微任务队列(Promise.resolve().then)中批量执行。源码注释明确写到「make sure observables updated」,即等待本次数据变更完全落盘后再统一刷新视图。这种异步调度可以避免在一次表单变更中触发多次重复渲染,对高频输入场景(如打字实时联动)尤为重要。
3.4 无副作用渲染:display: contents
FormConsumer 最终渲染的是一个带display: contents样式的 div(源码注释写的是just like <Fragment>)。display: contents会让该 div 自身不产生任何盒模型,其子节点直接参与父级布局,因此在视觉效果上等同于 Fragment。之所以不用真正的 Fragment 而用 div,是为了兼容 Vue 2 环境下的渲染差异(Vue 2 对 Fragment 支持有限),这一点在 shared/h.ts 的兼容逻辑中也能看到:它会在 Vue 2 下将 Fragment 降级为FragmentComponent。
四、核心表单状态:Form 模型的响应式数据
FormConsumer 暴露的form是@formily/core的 Form 模型,其关键状态字段在构造函数中初始化(Form.ts#L95-L103):
this.initialized = false this.submitting = false this.validating = false this.loading = false this.modified = false this.mounted = false this.unmounted = false this.display = this.props.display || 'visible' this.pattern = this.props.pattern || 'editable'这些字段通过makeObservable()声明为可观察属性(observable.ref/observable.shallow等,见 Form.ts#L122-L140),因此:
- 在 FormConsumer 的 slot 中读取
form.values、form.mounted、form.display等属性,会自动建立依赖关系; - 任何一处(字段写入、
setDisplay、setPattern、字段挂载/卸载等)修改这些状态,都会触发 FormConsumer 的重渲染。
常用状态与方法速查(均可直接在 slot 中使用):
| 成员 | 类型 | 说明 |
|---|---|---|
form.values | any | 当前表单值树,字段值写入时自动响应 |
form.initialValues | any | 初始值树 |
form.mounted/form.unmounted | boolean | 表单是否已挂载/卸载 |
form.display | 'visible' \| 'none' | 表单显示模式,可通过setDisplay()修改 |
form.pattern | 'editable' \| 'disabled' \| 'readOnly' \| 'readPretty' | 交互模式 |
form.getValuesIn(pattern) | any | 按路径读取值,见 Form.ts#L426-L428 |
form.setDisplay(display) | void | 设置显示模式,见 Form.ts#L458-L460 |
form.query(pattern) | Query | 查询字段图 |
五、测试用例验证:重渲染行为
仓库测试 form.spec.ts 中有一个专门针对 FormConsumer 的用例,可以完整验证其响应式行为:
test('FormConsumer', async () => { const form = createForm({ values: { a: 'abc', }, }) const wrapper = mount({ data() { return { form, Input } }, template: `<FormProvider :form="form"> <Field name="a" :component="[Input]" /> <FormConsumer ref="consumer"> <template #default="{ form }"> <div class="consumer">{{JSON.stringify(form.values)}}</div> </template> </FormConsumer> </FormProvider>`, }) expect(form.getValuesIn('a')).toBe('abc') expect(wrapper.find('.consumer').text()).toBe('{"a":"abc"}') form.setDisplay('none') expect(form.getValuesIn('a')).toBeUndefined() const $consumer = wrapper.vm.$refs.consumer as Vue $consumer.$forceUpdate() expect(wrapper.find('.consumer').text()).toBe('{}') })该用例验证了三件事:
- 初始渲染:slot 内读取
form.values后,界面正确渲染出{"a":"abc"}; - 状态修改:调用
form.setDisplay('none')后,表单显示模式被修改,values中a字段的值变为undefined(display为none的字段值会被清空); - 重渲染触发:对 FormConsumer 执行
$forceUpdate()后,界面内容同步更新为{}。
同文件的第一个用例(form.spec.ts#L44-L61)还验证了「slot 中读取form.mounted时,组件渲染完成后form.mounted为true」,证明 FormConsumer 与表单生命周期也是联动的。
说明:由于 FormConsumer 采用了异步 scheduler(微任务调度),
setDisplay('none')之后不会立刻同步重渲染,测试中通过$forceUpdate()手动触发刷新来验证最终状态——这恰好印证了第 3.3 节对调度行为的分析。
六、进阶实践建议
6.1 控制重渲染范围:最小化依赖读取
FormConsumer 的重渲染粒度取决于 slot 回调内读取的响应式数据。以下几种写法渲染成本不同:
- 读取整个
form.values(如JSON.stringify(form.values)):任何字段值变化都会触发重渲染,适合表单总览、值快照展示场景; - 通过
form.getValuesIn('a.b')精确读取某个路径:只有该路径的值变化才触发重渲染,适合精细化的局部联动; - 读取
form.mounted、form.display等元状态:仅在这些状态变化时重渲染。
如果某个 UI 片段只需要响应特定字段,优先使用getValuesIn缩小依赖面,避免大范围重渲染。
6.2 与其他响应式组件的搭配
FormConsumer 通常与Field、ObjectField、ArrayField、VoidField等字段组件搭配使用(这些组件同样在 components/index.ts 中导出)。典型模式是:
- 用
Field负责数据输入; - 用
FormConsumer负责把表单状态同步到非表单 UI(如统计面板、步骤条、提交按钮禁用态、表单摘要等)。
6.3 组件的 Vue 2 / Vue 3 兼容
@formily/vue通过vue-demi同时支持 Vue 2 与 Vue 3。FormConsumer 使用defineComponent编写,slot 语法在两种环境下均可用;Vue 2 项目可从@formily/vue的 vue2 入口导入(见 vue2-components.ts 中 FormConsumer/FormProvider 的注册方式)。测试文件 form.spec.ts 正是基于 Vue 2 环境运行的,验证了跨版本可用性。
七、常见问题
Q1:为什么 FormConsumer 必须放在 FormProvider 内部?因为 FormConsumer 不接收formprop,完全依赖inject(FormSymbol)获取表单。脱离 FormProvider 时注入为空,slot 中的form将是undefined,访问form.values会报错。
Q2:为什么修改表单值后 UI 没有立刻更新?FormConsumer 使用Promise.resolve().then(update)的微任务调度,状态变更后会在当前同步代码执行完毕、进入微任务队列时统一刷新。如果需要观察同步行为,可以留意这一异步特性(测试中也因此使用$forceUpdate()手动触发验证)。
Q3:FormConsumer 会影响布局吗?不会。它渲染的是display: contents的 div,自身不产生盒模型,slot 内容直接参与父级布局,效果等同于 Fragment。
Q4:slot 中能调用 Form 的方法吗?可以。slot 参数就是完整的Form实例,form.query(...)、form.getValuesIn(...)、form.setValues(...)等方法均可直接调用,且方法调用过程中读取的状态同样会被依赖追踪。
总结
FormConsumer 是 formily Vue 体系中连接「表单状态源」与「任意 UI」的轻量桥梁:它通过useForm从 FormProvider 注入的上下文中取出 Form 实例,借助@formily/reactive-vue的 observer/Tracker 机制实现依赖驱动的精准重渲染,并用display: contents保证零布局侵入。理解它的 scoped-slot 参数、依赖读取粒度与异步调度行为,就能在 Vue 2 / Vue 3 项目中轻松实现表单数据到 UI 的实时联动。
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
构建企业级智能数据分析平台:SQLBot架构设计与实战指南
构建企业级智能数据分析平台:SQLBot架构设计与实战指南 在当今数据驱动的商业环境中,企业面临着数据孤岛、分析门槛高、响应速度慢等核心挑战。SQLBot作为一
后端人工智能大模型RAGAI 应用数据可视化前端MCP 服务Vue Data UI 中 VueUiDonut 组件的响应式宽度问题解析
Vue Data UI 中 VueUiDonut 组件的响应式宽度问题解析 在数据可视化开发过程中,Vue Data UI 是一个广受欢迎的 Vue 组件库,它
前端数据可视化UI组件图表库Cycle.js热重载与Vue 2集成:在响应式应用中使用Vue 2
Cycle.js热重载与Vue 2集成:在响应式应用中使用Vue 2 你是否在开发响应式应用时遇到过代码修改后需要手动刷新页面的麻烦?是否想将Vue 2的组件化
前端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考