MobX 中的中间件和拦截器如何使用?
MobX 生态中有两套不同的拦截与中间件机制:MobX 核心库的 intercept/observe,以及 MobX-State-Tree(MST)的 addMiddleware/onAction。面试中混淆两者是常见的扣分点。下面分别讲解它们的用法、区别和典型场景。
核心库:intercept 和 observe
intercept 和 observe 是 MobX 核心库提供的底层 API,直接作用于 observable 对象的属性变更。
intercept:变更前拦截
intercept(target, propertyName?, interceptor) 在变更作用于 observable 之前被调用,可以对变更进行修改、放行或取消。
javascriptimport { observable, intercept } from 'mobx'; const store = observable({ count: 0, items: [] }); // 拦截 count 属性的变化 const disposer = intercept(store, 'count', (change) => { // 1. 修改变更:不允许负数 if (change.newValue < 0) { change.newValue = 0; } // 2. 取消变更:超过上限直接返回 null if (change.newValue > 100) { return null; } // 3. 放行变更:返回 change 对象 return change; }); store.count = 5; // 正常设置,count 变为 5 store.count = -1; // 被修改,count 变为 0 store.count = 200; // 被取消,count 保持不变 disposer(); // 移除拦截器
拦截器的返回值决定了变更的命运:
- 返回
change对象:放行变更 - 修改
change后返回:修改后放行(常用于数据规范化) - 返回
null:取消变更,对象不被修改 - 抛出异常:阻止变更并向上传播错误
拦截数组和 Map
intercept 也可以作用于 observable 数组和 Map,此时不需要指定属性名:
javascriptimport { observable, intercept } from 'mobx'; const items = observable([1, 2, 3]); intercept(items, (change) => { if (change.type === 'add' && typeof change.newValue !== 'number') { throw new Error('只允许添加数字'); } return change; }); const map = observable(new Map()); intercept(map, (change) => { if (change.name === 'secret') { return null; // 禁止设置 secret 键 } return change; });
observe:变更后观察
observe(target, propertyName?, listener) 在变更已经生效之后被调用,适合做副作用处理(如日志、同步到外部系统)。
javascriptimport { observable, observe } from 'mobx'; const store = observable({ count: 0 }); const disposer = observe(store, 'count', (change) => { console.log(`count: ${change.oldValue} -> ${change.newValue}`); }); store.count = 5; // 输出: count: 0 -> 5 disposer();
观察数组时的 change 对象包含 added、removed、index 等字段:
javascriptconst items = observable([1, 2, 3]); observe(items, (change) => { if (change.type === 'splice') { console.log('添加:', change.added, '移除:', change.removed); } }); items.push(4); // 添加: [4] 移除: []
不指定属性名时,可以观察对象所有属性的变化:
javascriptobserve(store, (change) => { console.log(`${change.name}: ${change.oldValue} -> ${change.newValue}`); });
intercept 与 observe 的关键区别
| 对比项 | intercept | observe |
|---|---|---|
| 触发时机 | 变更生效前 | 变更生效后 |
| 能否修改变更 | 可以 | 不可以 |
| 能否取消变更 | 可以(返回 null) | 不可以 |
| 典型用途 | 数据验证、格式化、权限控制 | 日志记录、副作用同步 |
注意事项
MobX 官方文档明确指出,intercept 和 observe 是底层工具,在实际项目中应谨慎使用。原因如下:
observe不遵循事务原则,在 action 中间可能触发多次- 两者都不支持深层级对象变化的监听
- 滥用
intercept容易创建难以调试的隐式数据流
优先使用 reaction、autorun 或 when 来替代 observe;将数据验证逻辑放在 action 内部而不是 intercept 中。
MobX-State-Tree:中间件体系
MobX-State-Tree(MST)在 MobX 核心之上构建了更完善的中间件系统,通过 addMiddleware 和 onAction 提供 action 级别的拦截能力。
addMiddleware:拦截 action
addMiddleware 可以拦截子树上的任何 action 调用,并能修改参数、中止执行或替换返回值。
javascriptimport { addMiddleware, flow } from 'mobx-state-tree'; const disposer = addMiddleware(store, (call, next) => { // call 包含: name, args, type, context, tree 等 console.log(`[Action] ${call.name} 被调用,参数:`, call.args); // 前置逻辑:验证参数 if (call.name === 'removeItem' && call.args[0] < 0) { return next({ ...call, args: [0] }); // 修改参数后传递 } // 调用 next 继续执行链 const result = next(call); // 后置逻辑:记录结果 console.log(`[Action] ${call.name} 完成,结果:`, result); return result; });
中间件处理函数必须调用 next(call) 让 action 继续执行,或者通过返回值中止 action。不调用 next 会导致 action 被静默取消。
onAction:监听 action
onAction 是一个内置的只读中间件,只能监听 action 的调用,不能拦截或修改。
javascriptimport { onAction } from 'mobx-state-tree'; const disposer = onAction(store, (call) => { console.log(`Action ${call.name} 被调用,参数:`, call.args); });
onAction 的参数以可序列化格式传递,适合用于:
- 调试日志
- 操作录制与重放(配合
applyAction) - 远程同步
onAction 与 addMiddleware 的区别
| 对比项 | addMiddleware | onAction |
|---|---|---|
| 能否拦截 action | 可以 | 不可以 |
| 能否修改参数 | 可以(克隆后修改) | 不可以 |
| 能否中止执行 | 可以(不调用 next) | 不可以 |
| 参数格式 | 原始参数 | 可序列化格式 |
| 典型用途 | 验证、权限控制、错误处理 | 日志、录制、调试 |
中间件链的执行顺序
多个中间件可以附加到同一个节点上,执行顺序遵循"由内到外"原则:
- 同一对象上,先注册的中间件先执行
- 子节点的中间件先于父节点的中间件执行
- 每个中间件必须调用
next(call)才能将控制权传递给下一个
实战:典型应用场景
数据验证与格式化
用 intercept 在数据写入前进行校验和规范化:
javascriptimport { observable, intercept } from 'mobx'; const form = observable({ email: '', age: 0 }); intercept(form, 'email', (change) => { if (change.newValue && !change.newValue.includes('@')) { return null; // 不写入无效邮箱 } return change; }); intercept(form, 'age', (change) => { change.newValue = Math.max(0, Math.floor(change.newValue)); return change; });
撤销/重做(Undo/Redo)
利用 observe 记录变更历史,实现撤销和重做功能:
javascriptimport { observable, observe, action, makeAutoObservable } from 'mobx'; class UndoManager { past = []; future = []; constructor(target) { this.target = target; makeAutoObservable(this); // 监听目标对象的属性变化 Object.keys(target).forEach((key) => { observe(target, key, (change) => { this.past.push({ key, oldValue: change.oldValue, newValue: change.newValue }); this.future = []; }); }); } @action undo() { if (this.past.length === 0) return; const entry = this.past.pop(); this.future.push(entry); this.target[entry.key] = entry.oldValue; } @action redo() { if (this.future.length === 0) return; const entry = this.future.pop(); this.past.push(entry); this.target[entry.key] = entry.newValue; } get canUndo() { return this.past.length > 0; } get canRedo() { return this.future.length > 0; } }
MST 中间件:统一的错误处理
在 MST 中用 addMiddleware 为所有 action 统一添加错误处理:
javascriptimport { addMiddleware } from 'mobx-state-tree'; addMiddleware(store, (call, next) => { try { const result = next(call); // 异步 action 需要特殊处理 if (result && typeof result.then === 'function') { return result.catch((error) => { console.error(`[Error] ${call.name} 失败:`, error); store.setError(error.message); throw error; }); } return result; } catch (error) { console.error(`[Error] ${call.name} 失败:`, error); store.setError(error.message); throw error; } });
MST 中间件:性能监控
javascriptimport { addMiddleware } from 'mobx-state-tree'; const metrics = {}; addMiddleware(store, (call, next) => { const start = performance.now(); const result = next(call); const duration = performance.now() - start; if (!metrics[call.name]) { metrics[call.name] = { count: 0, totalTime: 0, maxTime: 0 }; } const m = metrics[call.name]; m.count++; m.totalTime += duration; m.maxTime = Math.max(m.maxTime, duration); if (duration > 100) { console.warn(`[性能] ${call.name} 耗时 ${duration.toFixed(2)}ms`); } return result; });
操作录制与重放
利用 onAction 的可序列化特性,录制操作并在其他实例上重放:
javascriptimport { onAction, applyAction } from 'mobx-state-tree'; // 录制端 const recordedActions = []; onAction(sourceStore, (call) => { recordedActions.push(call); }); // 重放端 recordedActions.forEach((action) => { applyAction(targetStore, action); });
这种模式在协作编辑、时间旅行调试和测试中非常有用。
面试高频问题
intercept 和 observe 的区别是什么?
intercept 在变更生效前触发,可以修改或取消变更;observe 在变更生效后触发,只能被动接收。前者适合数据校验和格式化,后者适合日志和副作用同步。
为什么 MobX 官方建议慎用 intercept 和 observe?
因为 observe 不遵循事务原则,可能在一个 action 中间多次触发;两者都不支持深层级监听;滥用容易创建隐式的、难以调试的数据流。官方推荐使用 reaction、autorun 或 when 替代。
MST 的 addMiddleware 和 onAction 有什么区别?
addMiddleware 可以拦截、修改和中止 action,而 onAction 只能监听不能拦截。onAction 的参数以可序列化格式传递,适合录制和重放场景。
MST 中间件链的执行顺序是什么?
同一对象上先注册的中间件先执行,子节点中间件先于父节点中间件执行。每个中间件必须调用 next(call) 才能将控制权传递给下一个。
如何选择使用哪种机制?
- 需要拦截 observable 属性级别的变更:用核心库
intercept - 需要监听属性变更做副作用:优先用
reaction,其次observe - 需要拦截 MST action 级别的调用:用
addMiddleware - 只需监听 MST action 调用:用
onAction - 需要数据验证:放在 action 逻辑内部,而非
intercept中