- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
导读
在 Formily 体系中,@formily/reactive提供了独立于框架的响应式内核,而@formily/reactive-vue则负责把这一内核接入 Vue 的组件生命周期。本文聚焦@formily/reactive-vue的核心 API ——observer:它能把一个 Vue 组件的渲染方法改造成一个Reaction,使得组件每次重新渲染时自动收集所消费的响应式依赖,并在依赖变化时精准触发重渲染。读完本文,你将掌握observer的签名与参数语义、Vue 2 / Vue 3 双版本下的底层实现原理、scheduler调度器的正确用法,以及它与 Vue 原生响应系统的共存方式。
本文基于当前仓库中 packages/reactive/docs/api/vue/observer.md 编写,并以 packages/reactive-vue 包的真实源码与测试用例作为佐证。
一、核心概念:把渲染函数变成 Reaction
observer的官方描述非常凝练:
In Vue, the component rendering method is changed to Reaction, and dependencies are collected every time the view is re-rendered, and dependencies are updated automatically to re-render.
翻译过来即:在 Vue 中,将组件渲染方法变成 Reaction,每次视图重新渲染时收集依赖,依赖更新时自动重渲染。
要理解这句话,需要先了解@formily/reactive中两个核心原语(详见 packages/reactive/src/index.ts 及文档 packages/reactive/docs/api/reaction.md):
- observable:把普通对象包装成可被追踪的响应式数据;
- Tracker / reaction:执行一段函数(effect),在函数执行期间读取到的 observable 属性会被自动登记为依赖,此后任何依赖变化都会触发 effect 重新执行。
observer的工作,就是把 Vue 组件的render逻辑放进这样一个 Tracker 中执行:渲染期间读到的observable属性全部成为该组件的依赖;此后只要任一依赖变化,组件就会自动进入重渲染流程。这正是 Formily 表单元件能以高性能、细粒度方式响应表单状态变化的基础设施。
二、API 签名与参数说明
2.1 完整签名
原文档给出了如下 TypeScript 签名:
interface IObserverOptions { scheduler?: (updater: () => void) => void //调度器,可以手动控制更新时机 name?: string //包装后的组件的name } interface observer<T extends VueComponent> { (component: T, options?: IObserverOptions): T }observer接收一个 Vue 组件(选项对象或类组件)和可选配置,返回一个包装后的等价组件;返回值的类型保持T不变,因此对调用方而言 API 形状完全透明。
该签名与仓库中 packages/reactive-vue/src/types.ts 的定义一一对应:
export interface IObserverOptions { name?: string scheduler?: (updater: () => void) => void }2.2 参数语义
| 参数 | 类型 | 说明 | 默认行为 |
|---|---|---|---|
component | T extends VueComponent | 待包装的 Vue 组件(选项对象或 Vue 类) | 必填 |
options.name | string | 包装后组件的名称 | 优先取显式传入的name,其次回退到组件自身name/_componentTag/ 构造函数名,最终回退到'<component>' |
options.scheduler | (updater: () => void) => void | 调度器,在依赖变化时接管重渲染的时机控制 | 不传则依赖变化后立即同步重渲染 |
关于name的回退优先级,源码 packages/reactive-vue/src/observer/observerInVue2.ts 给出了精确顺序:
const name = observerOptions?.name || (Component as any).name || (Component as any)._componentTag || (Component.constructor && Component.constructor.name) || '<component>'对应的测试 packages/reactive-vue/src/tests/observer.spec.ts 验证了两种边界:无任何名称时最终得到'<component>',显式传入{ name: 'abc' }时得到'abc'。
2.3 scheduler 的用途
scheduler是IObserverOptions中最有实战价值的参数:当依赖更新发生时,observer不会直接执行重渲染,而是把"执行重渲染"这个动作(即updater回调)交给你手动编排。典型场景包括:
- 合并高频更新:在一帧内多次修改响应式数据时,只调度一次渲染;
- 延迟渲染:配合
setTimeout/requestAnimationFrame把渲染推迟到合适时机; - 异步批量刷新:把更新任务推入自己的批处理队列。
测试 packages/reactive-vue/src/tests/observer.spec.ts 给出了一个完整的节流式 scheduler 示例:100ms 内的多次更新被合并为一次渲染。
三、完整用例:与 Vue 响应系统共存
原文档提供了一段开箱即用的示例。一个值得强调的设计点是:observer包装后的组件不排斥 Vue 原生响应系统——data()中既可以是普通 Vue 响应式数据,也可以混入@formily/reactive的observable对象,两者可以在同一组件中并存。
<template> <div> <div> <input :style="{ height: 28, padding: '0 8px', border: '2px solid #888', borderRadius: 3, }" :value="obs.value" @input="(e) => { obs.value = e.target.value }" /> </div> <div>{{obs.value}}</div> </div> </template> <script> import { observable } from '@formily/reactive' import { observer } from '@formily/reactive-vue' export default observer({ data() { // 能与 vue 的响应系统共存 const obs = observable({ value: 'Hello world', }) return { obs, } }, }) </script>运行逻辑拆解:
observable({ value: 'Hello world' })创建一个@formily/reactive响应式对象;- 模板中
:value="obs.value"与{{obs.value}}在渲染阶段被observer追踪,obs.value成为组件依赖; - 输入框
@input事件改写obs.value,依赖变化触发 Tracker 回调,组件自动重渲染,视图同步更新。
3.1 共存机制:collectData 的过滤逻辑
"能与 Vue 响应系统共存"不是一句口号,其底层实现位于 packages/reactive-vue/src/observer/collectData.ts。observer在 Vue 2 分支中会重写data选项,用collectData对data()的返回值做分流处理:
export default function collectData(vm: any, data?: any) { const dataDefinition = typeof data === 'function' ? data.call(vm, vm) : data || {} const filteredData = Object.keys(dataDefinition).reduce( (result: any, field) => { const value = dataDefinition[field] if (isObservable(value)) { Object.defineProperty(vm, field, { configurable: true, get() { return value }, }) } else { result[field] = value } return result }, {} ) return filteredData }关键行为:
- 对
data()返回的每个字段调用isObservable(来自@formily/reactive)做判定; - 若是
observable对象,则不交给 Vue 的响应式系统,而是直接在vm上定义 getter 透传引用,避免 Vue 对已代理对象再次递归代理带来的性能损耗与行为冲突; - 若是普通数据,则保持原样交给 Vue 处理,与 Vue 原生响应系统无缝衔接。
对应的测试 packages/reactive-vue/src/tests/observer.spec.ts 验证了这一点:经过collectData过滤后,model(observable)仍由@formily/reactive追踪并触发 autorun,而普通字段target完全不受@formily/reactive影响。
四、双版本实现:Vue 2 与 Vue 3 的分流
@formily/reactive-vue借助vue-demi同时支持 Vue 2 与 Vue 3。入口 packages/reactive-vue/src/observer/index.ts 根据isVue2标志分发到两套实现:
export function observer<C>(baseComponent: C, options?: IObserverOptions): C { if (isVue2) { return observerV2(baseComponent, options) } else { return observerV3(baseComponent, options) } }依赖关系(见 packages/reactive-vue/package.json):运行时依赖@formily/reactive@2.3.7与vue-demi@>=0.13.6,同时以vue@^2.6.0 || >=3.0.0-rc.0和@vue/composition-api(Vue 2 下使用 Composition API 时需要)作为 peer 依赖。
4.1 Vue 2:重写 $mount/$destroy,接管原生渲染
Vue 2 分支位于 packages/reactive-vue/src/observer/observerInVue2.ts,其核心思路是:
- 复制原始组件选项(避免修改原组件),包装
data为collectData处理后的版本; - 通过
Super.extend(options)生成扩展组件,其中Super取原组件的原型链构造器,规避Vue.extend时生命周期被重复调用的问题; - 重写原型上的
$mount:首次挂载时在batch包裹下用Tracker.track执行原生渲染,并把 Vue 的_watcher.getter替换为reactiveRender,使组件后续每次渲染都重新进入 Tracker 收集依赖; - 重写
$destroy:组件销毁前先disposeTracker,杜绝销毁后的无效追踪。
const reactiveRender = () => { batch(() => { tracker.track(() => { if (!mounted) { $mount.apply(this, args) mounted = true nativeRenderOfVue = this._watcher.getter // rewrite the native render method of vue with our reactive tracker render // thus if component updated by vue watcher, we could re track and collect dependencies by @formily/reactive this._watcher.getter = reactiveRender } else { nativeRenderOfVue.call(this, this) } }) }) return this }而 Tracker 的回调则负责在依赖变化时决定"何时、以何种方式"重渲染:
const tracker = new Tracker(() => { if ( reactiveRender.$vm._isBeingDestroyed || reactiveRender.$vm._isDestroyed ) { return tracker.dispose() } if ( observerOptions?.scheduler && typeof observerOptions.scheduler === 'function' ) { observerOptions.scheduler(reactiveRender) } else { reactiveRender() } })这里有两处值得注意的实现细节:
- 销毁保护:检测到组件处于
_isBeingDestroyed/_isDestroyed状态时直接tracker.dispose()。测试 packages/reactive-vue/src/tests/observer.spec.ts 专门验证了"watcher 销毁后不再触发渲染"这一行为; - scheduler 接管:依赖变化时优先调用用户传入的
scheduler,否则立即reactiveRender()。
4.2 Vue 3:setup 内注入 useObserver
Vue 3 分支位于 packages/reactive-vue/src/observer/observerInVue3.ts,实现更加轻量:包装返回一个带setup的新组件,在用户setup执行前先调用useObserver建立追踪:
export const observer = function (opts: any, options?: IObserverOptions): any { const name = options?.name || opts.name || 'ObservableComponent' return { name, ...opts, setup(props: Record<string, any>, context: any) { useObserver(options) return opts?.setup?.(props, context) }, } }useObserver定义在 packages/reactive-vue/src/hooks/useObserver.ts:在组件实例上拦截effect属性,把组件更新函数包装进Tracker.track,并支持:
onBeforeUnmount时disposeTracker,防止内存泄漏;- 依赖变化时优先走
options.scheduler,否则调用vm.proxy.$forceUpdate()强制重渲染。
需要说明的是,这段实现中有一个对 Formily 官方 issue(#2655)的兼容性处理,其细节随 Vue 版本迭代可能有差异,本文仅陈述源码当前状态。
五、在 Formily 生态中的定位与典型用法
5.1 使用入口
@formily/reactive-vue的公共导出(见 packages/reactive-vue/src/index.ts)包含三部分:
export * from './observer' export * from './hooks' export * from './types'即observer(含collectData)、useObserverhook 以及IObserverOptions类型。安装后在项目中即可直接使用:
# 以 npm 为例 npm install @formily/reactive-vue使用时注意 Vue 2 场景需自行安装vue@^2.6.0并视情况引入@vue/composition-api(Vue 3 则直接使用vue@>=3)。
5.2 何时使用 observer
从源码与测试可以归纳出observer的典型适用场景:
- 组件内消费了
@formily/reactive的 observable 数据(例如 Formily 表单模型Form/Field的value、errors、loading等状态); - 希望渲染粒度精确到组件级:只有真正读取到的依赖变化才触发本组件重渲染,而不是整棵组件树重渲染;
- 需要手动控制更新时机(配合
scheduler)做渲染合并或延迟渲染。
5.3 与 React 版本 observer 的差异
当前仓库同时提供了 React 版本文档 packages/reactive/docs/api/react/observer.md,两者理念相同(把渲染变成 Reaction),但存在明显差异:
| 维度 | @formily/reactive-vue | @formily/reactive-react |
|---|---|---|
| 支持组件形态 | Vue 组件(选项对象 / 类 / 带 setup 的组件) | 仅函数组件(Function Component) |
| 名称配置项 | name | displayName |
| 透传 ref 配置 | 无 | forwardRef |
| 附加能力 | collectData与 Vue 响应系统共存 | Observer组件(Render Props 局部精确渲染) |
这一对照可以帮助你在跨框架复用 Formily 响应式状态时快速对齐 API 语义。
六、常见问题与最佳实践
6.1 依赖收集"每次渲染都发生"是特性而非缺陷
observer的设计是在每次渲染时重新收集依赖。这带来两个好处:
- 条件渲染下的依赖是动态的:
if (obs.a) return obs.b这种写法下,只有当obs.a为真时obs.b才成为依赖,之后obs.b的变化不会在obs.a为假时误触发渲染; - 依赖集合始终精确收敛到"当前视图真正消费的数据"。
代价是每次渲染都需要一次追踪开销,但batch与细粒度收集保证了整体成本可控。
6.2 scheduler 的正确姿势
scheduler的updater就是"执行重渲染"的函数,务必在合适时机调用且不要重复调用。参考测试中的节流写法:
const Component = observer( { data() { return { model } }, render(h) { return h('button', { on: { click: this.model.setAge }, domProps: { textContent: this.model.age }, }) }, }, { scheduler: (update) => { clearTimeout(timer) timer = setTimeout(update, 100) }, } )要点:
- 连续触发时先
clearTimeout再重新计时,实现"最后一次生效"的合并效果; - 组件销毁后 Tracker 已 dispose,不应再调度渲染。
6.3 组件销毁后的安全性
observer两套实现都内置了销毁保护:
- Vue 2:Tracker 回调检测
_isBeingDestroyed/_isDestroyed后自动dispose,$destroy也会显式执行 disposer(见 observerInVue2.ts); - Vue 3:
useObserver在onBeforeUnmount中 dispose Tracker(见 useObserver.ts)。
因此无需在业务代码里手动清理,这也是测试 observer.spec.ts 覆盖的核心保障之一。
6.4 不要混用两套响应式代理同一对象
尽管collectData已自动把observable对象从 Vue 响应式系统中"摘出",但建议在业务层保持清晰边界:同一份状态要么归@formily/reactive管,要么归 Vue 管,避免对同一对象同时施加两套代理导致的行为不确定性。
七、小结
observer是@formily/reactive-vue的基石 API,它把 Vue 组件的渲染生命周期与@formily/reactive的响应式追踪机制打通:
- 每次渲染收集依赖、依赖更新自动重渲染,实现组件级精确更新;
- 通过
collectData与 Vue 原生响应系统和平共存; - Vue 2 分支重写
$mount/$destroy并接管原生渲染,Vue 3 分支通过setup+useObserver注入追踪; scheduler提供手动编排渲染时机的自由,name帮助调试与组件树识别。
深入阅读建议:
- API 文档:packages/reactive/docs/api/vue/observer.md、packages/reactive/docs/api/vue/observer.zh-CN.md
- 实现源码:packages/reactive-vue/src/observer/index.ts、observerInVue2.ts、observerInVue3.ts、collectData.ts、useObserver.ts
- 测试用例:packages/reactive-vue/src/tests/observer.spec.ts
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
Formily 响应式渲染:@formily/reactive-react 的 observer 与 Observer 使用指南
Formily 响应式渲染:@formily/reactive react 的 observer 与 Observer 使用指南 本文是 Formily 官方文
前端UI组件Formily 响应式渲染指南:深入理解 observer HOC 与 Observer 组件
Formily 响应式渲染指南:深入理解 observer HOC 与 Observer 组件 导读 本文以 Formily 官方文档 observer.md
前端UI组件formily Vue 响应式组件:observer API 用法与原理解读
formily Vue 响应式组件:observer API 用法与原理解读 导读 在 Formily 的 Vue 体系中, observer 是让普通 Vue
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考