服务端阅读 05月27日 18:30
MobX 中的中间件和拦截器如何使用?
MobX 生态中有两套不同的拦截与中间件机制:MobX 核心库的 intercept/observe,以及 MobX-State-Tree(MST)的 addMiddleware/onAction。面试中混淆两者是常见的扣分点。下面分别讲解它们的用法、区别和典型场景。核心库:intercept 和 observeintercept 和 observe 是 MobX 核心库提供的底层 API,直接作用于 observable 对象的属性变更。intercept:变更前拦截intercept(target, propertyName?, interceptor) 在变更作用于 observable 之前被调用,可以对变更进行修改、放行或取消。import { 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 变为 5store.count = -1; // 被修改,count 变为 0store.count = 200; // 被取消,count 保持不变disposer(); // 移除拦截器拦截器的返回值决定了变更的命运:返回 change 对象:放行变更修改 change 后返回:修改后放行(常用于数据规范化)返回 null:取消变更,对象不被修改抛出异常:阻止变更并向上传播错误拦截数组和 Mapintercept 也可以作用于 observable 数组和 Map,此时不需要指定属性名:import { 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) 在变更已经生效之后被调用,适合做副作用处理(如日志、同步到外部系统)。import { 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 -> 5disposer();观察数组时的 change 对象包含 added、removed、index 等字段:const items = observable([1, 2, 3]);observe(items, (change) => { if (change.type === 'splice') { console.log('添加:', change.added, '移除:', change.removed); }});items.push(4); // 添加: [4] 移除: []不指定属性名时,可以观察对象所有属性的变化:observe(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:拦截 actionaddMiddleware 可以拦截子树上的任何 action 调用,并能修改参数、中止执行或替换返回值。import { 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:监听 actiononAction 是一个内置的只读中间件,只能监听 action 的调用,不能拦截或修改。import { 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 在数据写入前进行校验和规范化:import { 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 记录变更历史,实现撤销和重做功能:import { 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 统一添加错误处理:import { 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 中间件:性能监控import { 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 的可序列化特性,录制操作并在其他实例上重放:import { 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 中