发布订阅
先理解原理和示例,再练习完整口述与追问。
回答要点
订阅、精确取消、once、触发时增删、异常策略。
原理拆解
发布订阅把发送者和接收者通过事件中心解耦。实现需要定义同一回调重复订阅是否允许、触发时增删订阅的语义、once 如何在嵌套 emit 下只执行一次、错误是否隔离,以及如何取消。对订阅集合做快照可以减少遍历期间修改的意外行为。
示例与推演
A、B 两个监听,A 执行时取消 B,当前轮是否继续执行 B 要依契约说明;once 监听最好在执行用户回调前解除,避免回调递归 emit 时再次触发。组件卸载调用 off,测试连续订阅/取消后数量不增长。
易错点
事件总线隐藏依赖,不适合作为所有共享状态的替代。异常吞掉会掩盖故障,直接中断也可能影响后续监听,应明确策略。
参考回答可直接练习口述
用 Map 保存事件对应监听记录,on 返回取消函数,once 在执行前移除以避免递归触发重复执行。emit 使用开始时快照:新增监听下次生效,本轮尚未执行且已被移除的监听跳过。单个回调抛错不阻止其余回调,最后用 AggregateError 汇总抛出。策略说清楚比只写一个 forEach 更重要。
class EventBus {
constructor() { this.events = new Map() }
on(type, fn, once = false) {
if (typeof fn !== 'function') throw new TypeError('监听必须是函数')
if (!this.events.has(type)) this.events.set(type, new Set())
const listeners = this.events.get(type)
const record = { fn, once }
listeners.add(record)
return () => {
listeners.delete(record)
if (!listeners.size && this.events.get(type) === listeners) {
this.events.delete(type)
}
}
}
once(type, fn) { return this.on(type, fn, true) }
off(type, fn) {
const listeners = this.events.get(type)
if (!listeners) return
for (const record of listeners) {
if (record.fn === fn) listeners.delete(record)
}
if (!listeners.size) this.events.delete(type)
}
emit(type, ...args) {
const listeners = this.events.get(type)
if (!listeners) return 0
const errors = []
let count = 0
for (const record of [...listeners]) {
if (!listeners.has(record)) continue
if (record.once) listeners.delete(record)
count++
try { Reflect.apply(record.fn, undefined, args) }
catch (error) { errors.push(error) }
}
if (!listeners.size && this.events.get(type) === listeners) {
this.events.delete(type)
}
if (errors.length) throw new AggregateError(errors, '监听回调执行失败')
return count
}
}
示例:const off=bus.on('save',handler) 后调用 off() 只取消该条记录;bus.off('save',handler) 移除该事件下所有相同函数记录。相同函数可重复订阅。emit 的时间/快照空间为 O(监听数),此总线同步处理回调;异步回调的 Promise 拒绝不在同步 try/catch 捕获范围内,应由调用方处理或另定义 emitAsync。