FormConsumer 表单响应消费者:Vue 3 / Vue 2 下的响应式 UI 订阅组件
2026/9/24 5:15:18 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】formily

📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

导读

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>

要点拆解:

  1. form必须来自FormProvider的注入:FormConsumer 本身不接受formprop,它依赖FormProvider通过inject下发的表单上下文。没有 FormProvider 包裹时,useForm()注入到的将是空值,渲染结果不可用。
  2. scoped slot 只暴露一个参数form:slot 内部可以直接访问form.valuesform.mountedform.displayform.pattern等表单状态,也可以调用form.query(...)form.getValuesIn(...)等方法做更细粒度的数据提取。
  3. 渲染内容的定位: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 取表单:useFormFormSymbol

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.valuesform.mountedform.display等属性,会自动建立依赖关系;
  • 任何一处(字段写入、setDisplaysetPattern、字段挂载/卸载等)修改这些状态,都会触发 FormConsumer 的重渲染。

常用状态与方法速查(均可直接在 slot 中使用):

成员类型说明
form.valuesany当前表单值树,字段值写入时自动响应
form.initialValuesany初始值树
form.mounted/form.unmountedboolean表单是否已挂载/卸载
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('{}') })

该用例验证了三件事:

  1. 初始渲染:slot 内读取form.values后,界面正确渲染出{"a":"abc"}
  2. 状态修改:调用form.setDisplay('none')后,表单显示模式被修改,valuesa字段的值变为undefineddisplaynone的字段值会被清空);
  3. 重渲染触发:对 FormConsumer 执行$forceUpdate()后,界面内容同步更新为{}

同文件的第一个用例(form.spec.ts#L44-L61)还验证了「slot 中读取form.mounted时,组件渲染完成后form.mountedtrue」,证明 FormConsumer 与表单生命周期也是联动的。

说明:由于 FormConsumer 采用了异步 scheduler(微任务调度),setDisplay('none')之后不会立刻同步重渲染,测试中通过$forceUpdate()手动触发刷新来验证最终状态——这恰好印证了第 3.3 节对调度行为的分析。

六、进阶实践建议

6.1 控制重渲染范围:最小化依赖读取

FormConsumer 的重渲染粒度取决于 slot 回调内读取的响应式数据。以下几种写法渲染成本不同:

  • 读取整个form.values(如JSON.stringify(form.values)):任何字段值变化都会触发重渲染,适合表单总览、值快照展示场景;
  • 通过form.getValuesIn('a.b')精确读取某个路径:只有该路径的值变化才触发重渲染,适合精细化的局部联动;
  • 读取form.mountedform.display等元状态:仅在这些状态变化时重渲染。

如果某个 UI 片段只需要响应特定字段,优先使用getValuesIn缩小依赖面,避免大范围重渲染。

6.2 与其他响应式组件的搭配

FormConsumer 通常与FieldObjectFieldArrayFieldVoidField等字段组件搭配使用(这些组件同样在 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

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询