尧图精选

NocoBase RunJS 事件取消订阅指南:ctx.off() 的原理、用法与最佳实践

🕒 发布时间:2026/9/17 15:44:26 📁 来源:尧图网络
NocoBase RunJS 事件取消订阅指南ctx.off() 的原理、用法与最佳实践【免费下载链接】nocobaseNocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobaseRunJS 是 NocoBase 中面向 JS 区块JSBlock、JS 字段JSField / JSEditableField、JS 操作JS Action等场景的 JavaScript 执行环境代码运行在受限沙箱中通过ctx上下文对象安全访问平台能力。本文聚焦其中承担“取消订阅”职责的ctx.off()它用于移除ctx.on()注册的事件监听是防止内存泄漏、避免重复触发、保障页面长期稳定运行的关键 API。读完本文你将掌握ctx.off()的类型签名、适用场景、与ctx.on()/ctx.resource的配对用法以及 handler 引用一致性等底层原理。一、ctx.off() 是什么在 RunJS 中ctx.on(eventName, handler)用于订阅上下文事件如字段值变化、属性变化、资源刷新等而ctx.off(eventName, handler)则用于移除这些监听二者通常配对使用。官方文档对ctx.off()的定义是移除通过ctx.on(eventName, handler)注册的事件监听。常与ctx.on()配合使用在适当时机取消订阅避免内存泄漏或重复触发。从事件分发机制看事件会根据类型映射到两条通道见 ctx.on() 文档以resource:为前缀的事件如resource:refresh、resource:saved走ctx.resource的内部事件总线其余事件如js-field:value-change通常映射为ctx.element渲染容器上的自定义 DOM 事件CustomEvent。ctx.off()需要与这两条通道分别对应DOM 事件用removeEventListener资源事件总线用ctx.resource.off()。后者的底层实现在仓库中可以找到明确证据FlowResource类维护了一个events: Recordstring, Array(...args) void字典on负责追加回调off负责按引用过滤移除emit负责触发见 flowResource.ts。二、适用场景场景说明React useEffect 清理在useEffect的 cleanup 中调用组件卸载时移除监听JSField / JSEditableField字段双向绑定时取消对js-field:value-change的订阅resource 相关取消对ctx.resource.on注册的 refresh、saved 等监听这三类场景分别对应事件源的三条典型链路React 组件生命周期RunJS 代码常写在useEffect内注册与清理天然配对字段值双向绑定JSField 渲染的自定义控件既要监听外部表单联动、默认值更新带来的值变化也要在卸载时解除监听否则组件重新渲染时会出现重复触发数据资源生命周期监听ctx.resource上的refresh刷新完成、saved保存完成等事件在数据更新后执行联动逻辑用完后必须取消。三、类型定义off(eventName: string, handler: (event?: any) void): void;eventName要取消的事件名必须与ctx.on注册时完全一致handler注册时的同一函数引用详见下文“注意事项”返回void无返回值。对应地资源实例上的取消订阅签名来自 runjs-context/contexts/base.ts 中ctx.resource的元数据定义为off(event: string, callback: (...args) void): void;四、事件映射规则与常见事件在讨论示例之前先明确ctx.on()的事件映射规则源自 ctx.on() 文档以resource:为前缀的事件走ctx.resource.on其余通常走ctx.element上的 DOM 事件若存在。事件名说明事件来源js-field:value-change字段值被外部修改如表单联动、默认值更新ctx.element上的 CustomEventev.detail为新值resource:refresh资源数据已刷新ctx.resource事件总线resource:saved资源保存完成ctx.resource事件总线资源事件的实际触发位置可在源码中验证APIResource.refresh()请求成功后调用this.setData(data)并this.emit(refresh)见 apiResource.tsMultiRecordResource、SingleRecordResource在保存/刷新路径上会this.emit(saved, data)或this.emit(refresh)见 multiRecordResource.ts 与 singleRecordResource.ts。这意味着ctx.resource.on(refresh, handler)监听的是资源实例自身维护的事件总线而非浏览器事件。五、示例如何在真实场景中配对使用5.1 React useEffect 中配对使用在 React 组件中useEffect的返回函数cleanup是移除监听的推荐位置React.useEffect(() { const handler (ev) setValue(ev?.detail ?? ); ctx.on(js-field:value-change, handler); return () ctx.off(js-field:value-change, handler); }, []);要点handler定义在useEffect内部ctx.on与ctx.off引用同一个函数对象cleanup 在组件卸载或依赖变更重新执行前被调用保证监听被及时移除ctx.off在部分上下文中可能不存在官方文档建议使用可选链写法ctx.off?.(js-field:value-change, handler)。5.2 字段双向绑定与 js-field:value-changejs-field:value-change是 JSField / JSEditableField 场景中最常用的事件。仓库中的多区块联动示例两个表单块同步值给出了完整用法见 cross-block-linkage.tsx。其核心订阅代码为ctx.element.addEventListener(js-field:value-change, function(ev){ var next ev.detail null ? : String(ev.detail); if (selectEl) { var prev selectEl.value; if (prev ! next) { selectEl.value next; // 根据新值联动刷新子区块 notifyPeer(next); } } });这段示例直接使用原生ctx.element.addEventListener订阅等价于ctx.on的 DOM 事件通道并在变更监听中通过ev.detail读取新值。若要按ctx.on/off风格组织则是React.useEffect(() { const handler (ev) setValue(ev?.detail ?? ); ctx.on?.(js-field:value-change, handler); return () { ctx.off?.(js-field:value-change, handler); }; }, []);两种写法一一对应DOM 原生监听用addEventListener/removeEventListenerctx.on/ctx.off是其封装后的快捷通道ctx.element的文档化属性可在 elementDoc.ts 中查看。5.3 资源事件取消订阅ctx.resource是当前上下文中的 FlowResource 实例提供on(event, callback)/off(event, callback)用于订阅/取消订阅资源事件如refresh、saved。取消订阅示例const handler () { /* ... */ }; ctx.resource?.on(refresh, handler); // 适当时机 ctx.resource?.off(refresh, handler);资源刷新后更新 UI 的完整模式ctx.resource?.on(refresh, () { const data ctx.resource?.getData?.(); // 根据 data 更新渲染 });注意ctx.resource在多数区块表单、表格、详情和弹窗场景下由运行环境预先绑定而在 JSBlock 等默认无 resource 的场景下为undefined需要先调用ctx.initResource(type)初始化详见 ctx.resource 文档。因此取消订阅前应使用可选链ctx.resource?.off(...)做空值防御。5.4 与 ctx.on 的原生 DOM 监听替代方案当ctx.on未提供时可直接使用ctx.element的原生 API// 当 ctx.on 未提供时可直接使用 ctx.element const handler (ev) { if (selectEl) selectEl.value String(ev?.detail ?? ); }; ctx.element?.addEventListener(js-field:value-change, handler); // 清理时ctx.element?.removeEventListener(js-field:value-change, handler);六、与 ctx.on / ctx.resource 的配合要点使用ctx.on注册的监听应在适当时机通过ctx.off移除避免内存泄漏或重复触发在 React 中通常在useEffect的 cleanup 函数中调用ctx.offctx.off可能不存在使用时建议加可选链ctx.off?.(eventName, handler)ctx.resource的on/off是资源事件总线上的对称 API底层见 flowResource.ts除on/off外还提供once一次性监听触发后自动off与emit触发事件。七、注意事项handler 引用一致ctx.off时传入的handler必须与ctx.on时是同一引用否则无法正确移除。这由底层实现决定FlowResource.off通过(this.events[event] || []).filter((fn) fn ! callback)按引用过滤见 flowResource.ts匿名函数每次创建都是新引用无法命中过滤条件。因此切勿写成ctx.on(refresh, () { ... }); ctx.off(refresh, () { ... }); // 无效两个匿名函数引用不同及时清理在组件卸载或 context 销毁前调用ctx.off避免内存泄漏。FlowContext的实例通过_props、_methods、委托链delegate/addDelegate组织属性与方法见 flowContext.tsRunJS 每次执行都会创建上下文若不清理监听事件总线上的回调会持续累积。事件可用性不同 context 类型支持的事件不同具体以各组件文档为准。例如js-field:value-change仅在 JSField / JSEditableField 等字段上下文中被派发而resource:refresh/resource:saved取决于 resource 类型APIResource、MultiRecordResource、SingleRecordResource、SQLResource 的触发点各有差异。配对取消每次ctx.on(eventName, handler)都应有对应的ctx.off(eventName, handler)且传入的handler引用必须一致若需要“只触发一次”优先使用ctx.resource.once()而非手动配对。重复触发风险若在未清理旧监听的情况下重复注册例如useEffect依赖变化后重新执行同一 handler 会被多次追加到事件列表emit时会被多次调用。off只会移除与传入引用匹配的那一个因此依赖数组中请保持 handler 稳定或通过 cleanup 先行清理。八、源码验证refresh 事件的订阅与取消在 apiResource.test.ts 中有针对refresh事件订阅的完整测试可作为理解on/off/emit行为的最小样例it(should fetch data successfully and emit refresh event, async () { const r createAPIResource(); const api { request: vi.fn().mockResolvedValue({ data: { ok: 1 } }), }; const onRefresh vi.fn(); r.on(refresh, onRefresh); r.setAPIClient(api as any); r.setURL(/foo); r.setRequestMethod(get); r.addRequestHeader(Accept, application/json); r.setRequestParameters({ a: 1 }); r.setRequestBody(null); await r.refresh(); // 数据已写入 expect(r.getData()).toEqual({ ok: 1 }); // 无错误 expect(r.getError()).toBeNull(); // refresh 事件被触发一次 expect(onRefresh).toHaveBeenCalledTimes(1); });测试同时验证了失败路径请求失败时数据保持不变、错误被记录为ResourceError、且refresh事件不会被触发onRefresh未被调用。这说明事件仅在refresh()成功路径上由this.emit(refresh)派发监听者无需自行处理失败分支。九、相关文档ctx.on() - 订阅事件ctx.resource - 资源实例及其on/offctx.element - 渲染容器与 DOM 事件ctx.setValue() - 设置字段值会触发js-field:value-changeRunJS 概述 - RunJS 执行环境能力总览【免费下载链接】nocobaseNocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →