Plate 性能规则实战:用 SWR 订阅去重全局事件监听器,让 N 个组件共享 1 个监听
Plate 性能规则实战用 SWR 订阅去重全局事件监听器让 N 个组件共享 1 个监听【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文讲解 Plate 仓库内置 Vercel React 最佳实践技能中的一条客户端性能规则——client-event-listeners去重全局事件监听器。它解决的问题是当同一个自定义 Hook如键盘快捷键、视口监听被 N 个组件实例调用时浏览器上会挂载 N 个全局事件监听器读完本文你将掌握模块级回调注册表 useSWRSubscription共享订阅这一标准改造模式并能对照本仓库中真实存在的全局监听 Hook如视口、触摸设备检测、热键判断哪些代码适合应用该规则。规则来源vercel-react-best-practices 技能的规则体系这条规则位于仓库内的 client-event-listeners.md属于vercel-react-best-practices技能的一部分。该技能由 SKILL.md 描述是 Vercel Engineering 维护的一套 React/Next.js 性能优化指南共包含 69 条规则、8 个类别并按影响程度impact划分优先级优先级类别影响级别文件前缀1消除请求瀑布流CRITICALasync-2包体积优化CRITICALbundle-3服务端性能HIGHserver-4客户端数据获取MEDIUM-HIGHclient-5重渲染优化MEDIUMrerender-6渲染性能MEDIUMrendering-7JavaScript 性能LOW-MEDIUMjs-8高级模式LOWadvanced-本条规则的元数据frontmatter完整继承自原规则文件titleDeduplicate Global Event Listeners去重全局事件监听器impactLOWimpactDescription为 single listener for N componentsN 个组件只挂 1 个监听器tagsclient, swr, event-listeners, subscription从client-前缀可以看到它归属第 4 类客户端数据获取。按 SKILL.md 的说法每条规则文件都包含为什么重要、错误示例、正确示例、补充上下文四部分这条规则的核心主张只有一句话使用useSWRSubscription()在多个组件实例之间共享同一个全局事件监听器。规则库本身还带一套构建机制README.md 说明可通过pnpm build将rules/下的规则文件编译成AGENTS.md汇总文档、pnpm validate校验规则文件、pnpm extract-tests抽取 LLM 评测用例文件名前缀决定所属章节规则在章节内按标题字母序排序编号自动生成为。impact 等级从CRITICAL到LOW共六级本文这条被定为LOW——意思是它是渐进式优化通常不是瓶颈主因但在编辑器这类全局监听密集的长页面中值得规范。问题模式N 个实例 N 个全局监听器原规则文件给出的反例是一个典型的useKeyboardShortcutHookfunction useKeyboardShortcut(key: string, callback: () void) { useEffect(() { const handler (e: KeyboardEvent) { if (e.metaKey e.key key) { callback() } } window.addEventListener(keydown, handler) return () window.removeEventListener(handler) // 注意原规则文件此处为 removeEventListener(keydown, handler) }, [key, callback]) }以上为原文件代码的忠实呈现其清理函数写作window.removeEventListener(keydown, handler)本文不改动语义。这段代码的问题在于useEffect在每个组件实例挂载时都会执行一次window.addEventListener(keydown, handler)。如果 10 个组件各自调用useKeyboardShortcutwindow上就会同时挂着 10 个keydown监听器每次按键都要逐一触发、逐一做键名比较。这种每实例一个监听器的模式在本仓库中是真实存在的。从源码结构看几处现有实现都属于该模式use-viewport.tsuseViewportHook 在useEffect中对window.addEventListener(resize, handleResize)注册监听每个使用该 Hook 的组件实例都会独立挂一个resize监听use-is-touch-device.ts同样在 effect 中注册window.addEventListener(resize, onResize)并调用一次初始化多个消费方即多个监听器useHotkeys.tsPlate 的热键底层实现每次useHotkeys调用都会对domNode默认为document执行addEventListener(keydown, handleKeyDown)和addEventListener(keyup, handleKeyUp)见该文件第 210-213 行并在卸载时成对移除。这类监听器单个开销很小这正是该规则 impact 被标为LOW的原因但当编辑器应用里成百上千个节点组件如 Plate 的表格单元格、列表项各自挂监听时事件触发路径会被拉长去重就有意义了。解决方案模块级回调注册表 useSWRSubscription 共享订阅原规则文件给出的正确写法完整继承import useSWRSubscription from swr/subscription // Module-level Map to track callbacks per key const keyCallbacks new Mapstring, Set() void() function useKeyboardShortcut(key: string, callback: () void) { // Register this callback in the Map useEffect(() { if (!keyCallbacks.has(key)) { keyCallbacks.set(key, new Set()) } keyCallbacks.get(key)!.add(callback) return () { const set keyCallbacks.get(key) if (set) { set.delete(callback) if (set.size 0) { keyCallbacks.delete(key) } } } }, [key, callback]) useSWRSubscription(global-keydown, () { const handler (e: KeyboardEvent) { if (e.metaKey keyCallbacks.has(e.key)) { keyCallbacks.get(e.key)!.forEach(cb cb()) } } window.addEventListener(keydown, handler) return () window.removeEventListener(keydown, handler) }) } function Profile() { // Multiple shortcuts will share the same listener useKeyboardShortcut(p, () { /* ... */ }) useKeyboardShortcut(k, () { /* ... */ }) // ... }这个实现由三层组成逐层拆解如下1. 模块级回调注册表。const keyCallbacks new Mapstring, Set() void()声明在组件和 Hook 之外属于模块作用域的单例。它以按键为 keyvalue 是订阅了该按键的回调集合用Set保证同一回调重复注册时不会重复触发。组件挂载时把自己的callback加入集合卸载时从集合中删除当某个按键的集合清空时顺手keyCallbacks.delete(key)把空桶移除避免注册表随会话无限膨胀——这是注册表式架构里防止内存泄漏的关键清理细节。2. 共享订阅键global-keydown。useSWRSubscription(key, setupFn)来自 SWR 的订阅 APIsetupFn返回一个清理函数SWR 保证同一key的订阅在首个订阅者挂载时执行一次 setup、在最后一个订阅者卸载时执行一次 cleanup。因此无论多少组件调用useKeyboardShortcutwindow.addEventListener(keydown, handler)只会真正执行一次——这就是N instances 1 listener的机制来源。对比反例中每次挂载都 addEventListener共享订阅把监听器数量从 N 收敛到 1。3. 单一 handler 内部分发。唯一的监听器收到按键事件后先做廉价的e.metaKey keyCallbacks.has(e.key)判断Map.has是 O(1) 查找这与同技能库中 js-set-map-lookups.md 提倡的用 Set/Map 做 O(1) 查找思想一致再遍历对应Set逐个调用回调。事件触发的路径从N 个监听器各自比较键名变成1 个监听器一次查表 批量分发。需要注意的适用前提该方案依赖 SWR 库swr/subscription是其官方子路径导出。从当前仓库的主package.json、apps/www/package.json中检索不到swr依赖即本仓库源码目前并未引入 SWR这条规则在本仓库的定位是 Vercel 最佳实践技能库沉淀下来的改造准则当项目引入 SWR、或在自建订阅基础设施时可参照该模式落地。与同规则族及现有实现的对照与client-swr-dedup的关系。同目录下有姊妹规则 client-swr-dedup.mdimpact: MEDIUM-HIGH讲的是用useSWR让多个组件实例共享同一份数据请求还给出了useSWRMutation的变更场景示例。两条规则的共同内核是多个实例、单一信源一个作用于网络请求的去重一个作用于 DOM 事件订阅的去重。理解了useSWR的请求去重语义useSWRSubscription的订阅去重就是同一机制在长连接/监听器场景的延伸。与 Plate 热键实现的对照。从源码结构看useHotkeys.ts 代表了另一条工程路线它不共享全局监听器而是通过cbRef见该文件第 84-86 行让回调引用稳定——监听器只在keys/options/scopes变化时重新挂载而回调本身通过 ref 保持最新从而减少 effect 重跑。也就是说它优化的是监听器重挂载的频率而本文规则优化的是监听器实例的总数。两者并不冲突一个编辑器应用可以既有稳定的每实例监听热键需要按元素/作用域精确触发如enableOnContentEditable、scopes等选项都依赖逐实例判断也可以对真正全局的监听如CmdK 打开命令面板采用共享订阅去重。实践要点与限制回调身份要稳定。正确示例中注册 effect 的依赖是[key, callback]。若调用方传入内联匿名函数如useKeyboardShortcut(p, () doSomething())每次渲染都会产生新回调引用导致注册/注销 effect 反复执行——此时应配合useCallback或把最新回调存入 ref注册表里只登记一个稳定句柄。SSR 边界。window只在浏览器存在注册表读写与addEventListener都必须发生在 effect/订阅 setup 内客户端。useSWRSubscription的 setup 函数天然只在客户端执行符合 Next.js SSR/CSR 混合场景这也是该规则 tags 中带client的原因。共享键要按语义划分。示例用global-keydown一个键共享所有快捷键。如果未来需要区分 keydown/keyup 或 modifier 组合通道可以用不同订阅键如global-keyup分别共享而不是退回每实例监听。不要过度套用。该规则 impact 为LOW对于只有一两个消费方的监听比如单例的 use-viewport.ts注册表 订阅的间接层可能比收益更重它最适合大量组件实例重复订阅同一全局事件的场景如列表/表格中每个子项都要响应全局快捷键或窗口事件。注册表必须成对清理。卸载时set.delete(callback)且空集合整体删除是示例中容易被省略却决定长期稳定性的部分只做 add 不做 delete 会让模块级 Map 成为跨路由的内存泄漏点。延伸阅读路径规则原文含完整错误/正确示例client-event-listeners.md技能总览与 8 类规则优先级SKILL.md规则库结构、构建与校验命令README.md姊妹规则SWR 请求去重client-swr-dedup.md客户端其他规则client-passive-event-listeners.md滚动等事件使用 passive 监听、client-localstorage-schema.md仓库内可对照的每实例监听实现use-viewport.ts、use-is-touch-device.ts、useHotkeys.ts【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考