【Svelte】事件管理
在 Svelte 5 中,由于官方已经彻底废弃createEventDispatcher,推荐使用callback props模式进行组件间通信。但当你的应用逐渐变大、组件层级变深、或者存在“非父子关系”的松耦合模块通信需求时,单纯依赖 callback props 会导致 prop drilling 地狱。
下面我给出几种目前(2025–2026 年社区实践)比较推荐的统一事件管理机制设计方案,按推荐程度从高到低排列,供你根据项目规模选择。
方案对比(快速决策表)
| 方案 | 复杂度 | 适合项目规模 | 跨层级/松耦合 | 类型安全 | 调试友好 | 推荐指数 |
|---|---|---|---|---|---|---|
| 1. Callback Props + Context(最纯净) | ★☆☆☆☆ | 小–中型 | △(需钻透) | 好 | 很好 | ★★★★★(首选) |
| 2. $state + $effect 全局事件总线(Runes 原生) | ★★☆☆☆ | 中–大型 | ◎ | 中等(需自己写类型) | 中等 | ★★★★☆(目前最流行) |
| 3. tiny-emitter / mitt 轻量事件库 | ★★☆☆☆ | 中–大型 | ◎ | 一般(需自己加强类型) | 好 | ★★★★☆ |
| 4. 基于 $effect.root 的独立 EventBus 类 | ★★★☆☆ | 大型/复杂系统 | ◎ | 很好(可强类型) | 很好 | ★★★★☆(推荐企业级) |
| 5. Zustand / nanostores 等外部状态库的事件副作用 | ★★★★☆ | 大型、已用状态管理 | ◎ | 很好 | 极好 | ★★★☆☆(有状态管理时考虑) |
推荐方案 4:基于 $effect.root 的强类型 EventBus(企业级推荐)
// src/lib/events.tsimport{$effect,$state}from'svelte';typeEventCallback<P=any>=(payload:P)=>void|Promise<void>;typeEventMap={// 在这里集中定义所有全局事件(强类型核心)'user:login':{userId:string;role:string};'user:logout':void;'cart:updated':{itemCount:number;total:number};'notification:show':{message:string;type:'success'|'error'|'info'};'modal:open':{component:any;props?:Record<string,any>};'theme:changed':{theme:'light'|'dark'};// ... 继续添加你系统里所有跨模块事件};typeEventName=keyofEventMap;classTypedEventBus{privatelisteners=$state(newMap<EventName,Set<EventCallback>>());on<TextendsEventName>(event:T,callback:EventCallback<EventMap[T]>){if(!this.listeners.has(event)){this.listeners.set(event,newSet());}this.listeners.get(event)!.add(callbackasany);// 返回取消函数(非常重要)return()=>{this.listeners.get(event)?.delete(callbackasany);};}once<TextendsEventName>(event:T,callback:EventCallback<EventMap[T]>){constoff=this.on(event,(payload)=>{callback(payload);off();});returnoff;}emit<TextendsEventName>(event:T,payload:EventMap[T]){constcbs=this.listeners.get(event);if(!cbs)return;// 复制一份防止在回调中 unsubscribe 导致问题[...cbs].forEach((cb)=>{try{cb(payload);}catch(err){console.error(`Event${String(event)}handler error:`,err);}});}clear(event?:EventName){if(event){this.listeners.delete(event);}else{this.listeners.clear();}}}// 单例(也可以做成 Context 注入)exportconstevents=newTypedEventBus();// 可选:开发环境调试辅助if(import.meta.env.DEV){$effect.root(()=>{$effect(()=>{console.log('[EventBus] 当前监听器数量:',events['listeners'].size);});});}使用方式示例
<!-- 某个深层组件 --> <script> import { events } from '$lib/events'; $effect(() => { const off = events.on('notification:show', ({ message, type }) => { // 显示 toast 或 modal toast(message, type); }); return off; // 组件销毁时自动清理,非常重要! }); </script>// 任何地方(action、store、utils、甚至服务层)import{events}from'$lib/events';events.emit('user:login',{userId:'123',role:'admin'});events.emit('notification:show',{message:'保存成功',type:'success'});极简替代方案(适合中小项目)
// src/lib/eventBus.tsimport{$state}from'svelte';exportconstglobalEvents=$state({emit(event:string,payload?:any){// 这里可以换成 mitt 或自己实现window.dispatchEvent(newCustomEvent(event,{detail:payload}));}});// 监听方$effect(()=>{consthandler=(e:CustomEvent)=>{...};window.addEventListener('notification:show',handler);return()=>window.removeEventListener('notification:show',handler);});总结建议(2026 年视角)
- 项目 < 30 个组件→ 尽量只用 callback props + context
- 中大型项目,有明确的跨模块事件→ 强烈推荐TypedEventBus + $effect.root(如方案4)
- 已经深度使用外部状态管理(如 zustand、nanostores)→ 直接在 action / reducer 里触发副作用,通常不需要额外事件总线
- 非常在意 bundle size→ 用 mitt(~200B)或 nanoevents 代替自己写的 EventBus
