5月27日 18:30

MobX 中的中间件和拦截器如何使用?

MobX 生态中有两套不同的拦截与中间件机制:MobX 核心库的 intercept/observe,以及 MobX-State-Tree(MST)的 addMiddleware/onAction。面试中混淆两者是常见的扣分点。下面分别讲解它们的用法、区别和典型场景。

核心库:intercept 和 observe

interceptobserve 是 MobX 核心库提供的底层 API,直接作用于 observable 对象的属性变更。

intercept:变更前拦截

intercept(target, propertyName?, interceptor) 在变更作用于 observable 之前被调用,可以对变更进行修改、放行或取消。

javascript
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 变为 5 store.count = -1; // 被修改,count 变为 0 store.count = 200; // 被取消,count 保持不变 disposer(); // 移除拦截器

拦截器的返回值决定了变更的命运:

  • 返回 change 对象:放行变更
  • 修改 change 后返回:修改后放行(常用于数据规范化)
  • 返回 null:取消变更,对象不被修改
  • 抛出异常:阻止变更并向上传播错误

拦截数组和 Map

intercept 也可以作用于 observable 数组和 Map,此时不需要指定属性名:

javascript
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) 在变更已经生效之后被调用,适合做副作用处理(如日志、同步到外部系统)。

javascript
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 -> 5 disposer();

观察数组时的 change 对象包含 addedremovedindex 等字段:

javascript
const items = observable([1, 2, 3]); observe(items, (change) => { if (change.type === 'splice') { console.log('添加:', change.added, '移除:', change.removed); } }); items.push(4); // 添加: [4] 移除: []

不指定属性名时,可以观察对象所有属性的变化:

javascript
observe(store, (change) => { console.log(`${change.name}: ${change.oldValue} -> ${change.newValue}`); });

intercept 与 observe 的关键区别

对比项interceptobserve
触发时机变更生效前变更生效后
能否修改变更可以不可以
能否取消变更可以(返回 null)不可以
典型用途数据验证、格式化、权限控制日志记录、副作用同步

注意事项

MobX 官方文档明确指出,interceptobserve 是底层工具,在实际项目中应谨慎使用。原因如下:

  • observe 不遵循事务原则,在 action 中间可能触发多次
  • 两者都不支持深层级对象变化的监听
  • 滥用 intercept 容易创建难以调试的隐式数据流

优先使用 reactionautorunwhen 来替代 observe;将数据验证逻辑放在 action 内部而不是 intercept 中。

MobX-State-Tree:中间件体系

MobX-State-Tree(MST)在 MobX 核心之上构建了更完善的中间件系统,通过 addMiddlewareonAction 提供 action 级别的拦截能力。

addMiddleware:拦截 action

addMiddleware 可以拦截子树上的任何 action 调用,并能修改参数、中止执行或替换返回值。

javascript
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:监听 action

onAction 是一个内置的只读中间件,只能监听 action 的调用,不能拦截或修改。

javascript
import { onAction } from 'mobx-state-tree'; const disposer = onAction(store, (call) => { console.log(`Action ${call.name} 被调用,参数:`, call.args); });

onAction 的参数以可序列化格式传递,适合用于:

  • 调试日志
  • 操作录制与重放(配合 applyAction
  • 远程同步

onAction 与 addMiddleware 的区别

对比项addMiddlewareonAction
能否拦截 action可以不可以
能否修改参数可以(克隆后修改)不可以
能否中止执行可以(不调用 next)不可以
参数格式原始参数可序列化格式
典型用途验证、权限控制、错误处理日志、录制、调试

中间件链的执行顺序

多个中间件可以附加到同一个节点上,执行顺序遵循"由内到外"原则:

  • 同一对象上,先注册的中间件先执行
  • 子节点的中间件先于父节点的中间件执行
  • 每个中间件必须调用 next(call) 才能将控制权传递给下一个

实战:典型应用场景

数据验证与格式化

intercept 在数据写入前进行校验和规范化:

javascript
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 记录变更历史,实现撤销和重做功能:

javascript
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 统一添加错误处理:

javascript
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 中间件:性能监控

javascript
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 的可序列化特性,录制操作并在其他实例上重放:

javascript
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 中间多次触发;两者都不支持深层级监听;滥用容易创建隐式的、难以调试的数据流。官方推荐使用 reactionautorunwhen 替代。

MST 的 addMiddleware 和 onAction 有什么区别?

addMiddleware 可以拦截、修改和中止 action,而 onAction 只能监听不能拦截。onAction 的参数以可序列化格式传递,适合录制和重放场景。

MST 中间件链的执行顺序是什么?

同一对象上先注册的中间件先执行,子节点中间件先于父节点中间件执行。每个中间件必须调用 next(call) 才能将控制权传递给下一个。

如何选择使用哪种机制?

  • 需要拦截 observable 属性级别的变更:用核心库 intercept
  • 需要监听属性变更做副作用:优先用 reaction,其次 observe
  • 需要拦截 MST action 级别的调用:用 addMiddleware
  • 只需监听 MST action 调用:用 onAction
  • 需要数据验证:放在 action 逻辑内部,而非 intercept
标签:Mobx