面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 05月27日 18:16

MobX 性能优化的最佳实践有哪些?

MobX 本身已经做了大量性能优化——细粒度依赖追踪、自动批处理、computed 缓存,大多数场景下开箱即用就够了。真正需要手动优化的,集中在三件事上:computed 被滥用或用错了、observable 追踪了不该追踪的东西、组件粒度太粗导致重渲染范围过大。核心思路:减少追踪范围(只让真正会变的状态变 observable)、减少计算次数(用 computed 缓存派生值)、减少渲染范围(拆小组件、延迟间接引用)。追问computed 和普通 getter 有什么区别?什么时候该用 computed?computed 会缓存结果,只在依赖的 observable 变化时重新计算;普通 getter 每次访问都执行。当你需要从 observable 数据派生新值时用 computed——过滤列表、拼接字符串、计算汇总。一个值如果会被多处读取,computed 的缓存收益更大。注意:computed 里不能有副作用(发请求、改状态),它必须是纯函数,否则缓存一致性无法保证。observable 的深度怎么选?什么时候用 shallow?默认 observable 会递归把对象所有层级都变成响应式,适合嵌套深、内部属性需要单独追踪的场景。observable.shallowObject 只让第一层变成响应式,内部对象保持原样。实际项目中,列表数据用 shallow 就够了——你通常关心的是"列表变了"而不是"某个用户的名字变了"。只有确实需要追踪深层属性变化时才用深度 observable。对于不会变的配置项(API 地址、超时时间),压根不要加 observable,纯常量没必要追踪。action 里还需要包 runInAction 吗?不需要。action 本身就会批量处理里面的状态变更,在 action 内再套 runInAction 是多余的。runInAction 的真正用途是异步回调中修改状态——await 之后的赋值已经不在 action 作用域内,必须用 runInAction 包起来。@actionasync fetchData() { this.loading = true; const data = await api.getData(); // 这里已经不在 action 作用域了 runInAction(() => { this.data = data; this.loading = false; });}observer 组件拆多细合适?看组件里读了几种不同的 observable。一个组件同时读 user.name、settings.theme、data.list,任何一个变化都会触发整个组件重渲染。拆成三个小组件,各自只读自己关心的数据,交叉触发就消失了。判断标准:observable 依赖越集中越好。一块 UI 只依赖 store 的一小块数据,就值得单独抽成 observer 组件。如果整个页面只读一个 observable,拆不拆无所谓。另外,不读 observable 的组件(纯展示的 Header、Footer)不要加 observer,加了反而增加追踪开销。autorun、reaction、when 怎么选?autorun:立即执行一次,之后依赖变化就重新执行。适合日志、同步等"每次变了都要做某事"的场景。reaction:只追踪数据表达式,数据变了才执行副作用回调,默认不立即执行。比 autorun 更可控,优先用 reaction。when:条件满足时执行一次就自动销毁。适合"等数据到了再做某事"的一次性逻辑,比在 autorun 里写 if 判断更清晰。三者的返回值都是 dispose 函数,组件卸载时一定要调用,否则内存泄漏。数组操作有什么性能坑?避免整体重新赋值(this.items = [...this.items, item]),MobX 会对整个数组重新建立追踪。用 push、splice 等变异方法直接操作,MobX 只追踪变化的部分。批量替换用 replace(newArray),比重新赋值高效,MobX 内部会做差异更新而不是重建整个 observable 结构。怎么排查 MobX 的性能问题?用 trace() 定位是哪个 computed 或 reaction 导致了多余计算。在组件 render 里调用 trace(true),控制台会输出完整的依赖链和触发原因。用 MobX DevTools 观察每次状态变更触发了哪些 reaction,找到重渲染次数异常的组件。如果某个 computed 计算太频繁,检查它的依赖范围是不是比预期的大——可能是间接引用了一个大对象,MobX 会追踪这个对象上所有被读取的属性。用 computed 预处理数据,把 map/filter 的结果缓存起来,避免在 observer 组件的 render 里直接遍历大列表。写段代码// makeAutoObservable 一键搞定 observable/computed/action 标记class Store { items = []; filter = 'all'; constructor() { makeAutoObservable(this); } get filteredItems() { return this.filter === 'all' ? this.items : this.items.filter(i => i.active); } setFilter(f) { this.filter = f; }}
前端阅读 05月27日 18:16

MobX 组件不更新、异步报错怎么办?常见坑和解决方案

MobX 的响应式机制看起来很简单——observable 包数据、observer 包组件、action 改状态,三件套一配就完事了。但实际项目里踩坑的地方不少:组件明明包了 observer 却不更新、异步操作改了状态没反应、computed 值死活不刷新。这篇文章把最常遇到的坑按出现频率排了一遍,每个坑讲清楚为什么掉进去以及怎么爬出来。组件包了 observer 却不更新这是用 MobX 最容易遇到的问题,没有之一。现象很明确:数据变了,组件纹丝不动。原因通常就那么几个。没有真正访问 observable 属性。 observer 只追踪 render 过程中实际读取的 observable 属性。如果你在 render 外面把值解构出来,MobX 根本不知道这个组件依赖那个属性:// 不会更新——render 里没有直接访问 observableconst { count } = store;return <div>{count}</div>;// 会更新——render 过程中访问了 store.countreturn <div>{store.count}</div>;用了普通对象而非 observable。 这看起来很低级,但在项目里经常出现——某个同事新加了一个对象,忘了用 observable 包一下,组件读它当然不会更新。MobX 6 没开 action 就改状态。 MobX 6 默认 enforceActions: 'observed',意味着所有 observable 状态的修改必须在 action 里进行。在 action 外直接 this.count++ 会报错。如果你为了省事关了这个检查(configure({ enforceActions: 'never' })),表面上看不出问题,但 MobX 内部的批量更新机制会失效,导致多次修改触发多次渲染。class Store { count = 0; constructor() { makeAutoObservable(this); } increment() { this.count++; // makeAutoObservable 自动把方法变成 action }}用 makeAutoObservable 就不用手动标注 action 了,它会自动推断。render 里创建了新的引用类型。 每次 render 都 new 一个对象或数组,即使内容一样引用也不同,React 做 shallow compare 会认为 props 变了,触发不必要的子组件重渲染。更隐蔽的问题是,如果你把这个新对象传给另一个 observer 组件,MobX 会误以为依赖变了。异步操作改了状态不生效@action 只能同步地修改状态。一旦遇到 await,action 的边界就断了——await 之后的代码不在 action 作用域内,MobX 会在控制台疯狂报警告。三种解决方案,推荐程度从高到低:用 flow(最推荐)。 flow 是 MobX 专门为异步设计的,用 generator 函数写,每个 yield 之间的代码自动包裹在 action 里,心智负担最小:class Store { data = null; loading = false; constructor() { makeAutoObservable(this); } fetchData = flow(function* () { this.loading = true; try { const res = yield fetch('/api/data'); this.data = yield res.json(); } catch (e) { console.error(e); } finally { this.loading = false; } });}用 runInAction。 如果不想用 generator,在 await 之后手动把状态修改包进 runInAction:async fetchData() { this.loading = true; try { const res = await fetch('/api/data'); runInAction(() => { this.data = res.json(); }); } finally { runInAction(() => { this.loading = false; }); }}注意 runInAction 需要从 mobx 导入,而且每次修改状态都要包一次,容易漏。用 action 包裹整个 async 函数再配合 runInAction。 这是上面两种的混合,不推荐,代码更啰嗦。computed 值不更新或更新不对computed 有两个大坑:一个是在里面搞副作用,一个是依赖追踪丢了。computed 里搞副作用。 computed 本质是一个缓存计算值,MobX 期望它是纯函数——给同样的输入返回同样的输出,不做任何额外的事。如果你在 computed 里调接口、打日志、改其他状态,MobX 的缓存策略会乱套:// 大错特错get filteredList() { console.log(this.items.length); // 副作用 fetch('/api/track', { body: this.query }); // 副作用 return this.items.filter(i => i.active);}// 正确——纯计算get filteredList() { return this.items.filter(i => i.active);}需要副作用的场景用 autorun 或 reaction,别用 computed。依赖追踪丢了。 MobX 的响应式只在属性被实际读取时才建立追踪。常见写法是解构了 observable 再用,或者条件分支里读了一个属性但返回值没用到:// 不会追踪 this.dataget bad() { const data = this.data; // 读了但没用 return this.items.length;}// 会正确追踪两个依赖get good() { return this.data.length + this.items.length;}内存泄漏:reaction 没清理autorun、reaction、when 这些函数调用后都会返回一个 disposer,组件卸载时必须调用。忘了清理的话,组件都销毁了,reaction 还在跑,轻则内存泄漏,重则操作已卸载组件的 DOM 报错。React 项目里用 useEffect 的 cleanup 来处理:useEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => dispose();}, []);when 也要清理——虽然 when 会在条件满足后自动清理,但组件卸载时条件可能还没满足,reaction 还在等。性能问题:渲染太频繁MobX 的 observer 做得已经很精细了——只有组件实际读取的 observable 变了才会重渲染。但还是会遇到性能问题,常见原因:单个组件读太多 observable。 一个大组件读了 store 里的十几个属性,其中任何一个变了都会重渲染整个组件。解法是拆组件——每个小组件只读自己关心的那几个属性:// 一个大组件读了很多数据,任何一个变了都重渲染const Dashboard = observer(() => ( <div> <p>{store.user.name}</p> <p>{store.settings.theme}</p> <p>{store.stats.count}</p> </div>));// 拆成小组件,各管各的const UserName = observer(() => <p>{store.user.name}</p>);const Theme = observer(() => <p>{store.settings.theme}</p>);const Count = observer(() => <p>{store.stats.count}</p>);列表渲染没做细化。 如果列表组件整体用 observer 包裹,修改一条数据的某个字段会导致整个列表重渲染。给每条数据单独包一个 observer 组件,MobX 就能做到只更新变化的那一条。装饰器配置问题装饰器 @observable、@action、@computed 需要 Babel 或 TypeScript 的装饰器插件支持,配置稍微不对就报错。而且 ECMAScript 装饰器提案改了好几版,Babel 的 legacy: true 对应的是旧版提案,和 TypeScript 的 experimentalDecorators 也不是完全一回事。MobX 6 开始官方推荐 makeAutoObservable / makeObservable,不再需要装饰器:class Store { count = 0; list = []; constructor() { makeAutoObservable(this); } // 普通方法自动变成 action increment() { this.count++; } // getter 自动变成 computed get double() { return this.count * 2; }}makeAutoObservable 能推断大多数场景,但有个限制:子类继承时需要手动在子类构造函数里再调一次。makeObservable 更灵活但需要手动标注每个属性。数组操作踩坑MobX 6 默认用 Proxy 实现 observable,数组操作基本和原生数组行为一致,push、splice、map、filter 都能正常触发更新。但有几个细节:直接赋值替换数组。 在 MobX 6 的 Proxy 模式下直接赋值是可以的(this.items = newArray),但如果你在用旧版 MobX 或者关了 Proxy(useProxies: 'never'),需要用 replace():@actionreplaceItems(newItems) { this.items.replace(newItems); // 旧版 MobX 安全写法}传给非 MobX 库时要转原生。 有些第三方库(如 Lodash 的某些方法、antd 的 Table dataSource)对 observable 数组的兼容性不好,传之前用 .slice() 或 toJS() 转成普通数组:import { toJS } from 'mobx';lodashChain(toJS(this.items));Array.isArray 在旧版返回 false。 Proxy 模式下没问题,但旧版 observable 数组不是真数组,Array.isArray(observable([1,2,3])) 返回 false。用 isObservableArray 检测或 .slice() 转换。循环依赖导致无限更新两个 store 的 computed 互相依赖对方的数据,改一个触发另一个重算,另一个重算又触发第一个重算,死循环。MobX 会检测到循环依赖并抛出错误,但有时候循环不是那么明显——比如 A 的 reaction 修改了 B 的数据,B 的 reaction 又修改了 A 的数据,这种间接循环更难定位。解法是理清数据流方向,让依赖关系变成单向的。如果两个 store 确实需要共享数据,抽出一个更高层的 store 来管理共享状态,让两个子 store 都依赖父 store 而不是互相依赖。调试手段MobX 提供了几个调试工具,按实用程度排序:trace():放在 computed 或 observer 的 render 里,控制台会打印这个计算/渲染依赖了哪些 observable,以及是否在某个 observable 变化时重新计算。trace(true) 会在 debugger 断点停下,方便逐步排查。spy():全局监听所有 MobX 事件(action 执行、observable 修改、computed 重算、reaction 触发),适合定位"到底是什么触发了这次渲染":import { spy } from 'mobx';spy((event) => { if (event.type === 'action') { console.log('Action:', event.name); }});getDependencyTree / getObserverTree:程序化地获取依赖关系树,可以判断某个 computed 依赖了哪些 observable,或者某个 observable 被哪些 observer 观察。MobX DevTools:浏览器扩展,可视化展示 observable 树和依赖关系。功能不如 Redux DevTools 丰富,但对于排查响应式链路断裂够用了。TypeScript 类型问题makeObservable 的泛型参数容易写错。MobX 6 要求传入类型参数来推断 this 的类型:class Store { count: number = 0; constructor() { makeObservable<Store>(this, { count: observable, increment: action, }); } increment() { this.count++; }}如果用 makeAutoObservable,大多数情况不需要手动传泛型,TypeScript 能自动推断。但 makeAutoObservable 不支持子类,子类需要在构造函数里手动调 makeObservable 并列出所有需要观测的属性。另一个常见问题是 observable 的类型推断——observable({ list: [] }) 里的 list 会被推断为 never[] 而不是 any[],需要手动标注类型:class Store { list: Item[] = []; constructor() { makeAutoObservable(this); }}
前端阅读 05月27日 18:14

MobX 中的 toJS、toJSON 和 observable.shallow 有什么区别?

MobX 中的 observable 数据结构和原生 JavaScript 对象之间存在一道墙:observable 对象被 Proxy 包裹,不能直接用于 API 请求、localStorage 存储或传给不兼容 MobX 的第三方库。toJS、toJSON 和 observable.shallow 分别从不同角度处理这道墙——前两者负责"拆墙",后者负责"少砌墙"。下面逐一拆解。toJS:深度剥离 observable,拿到纯 JS 对象toJS 是 MobX 官方推荐的转换方法,它会递归遍历整个 observable 树,把每一层 Proxy 都剥掉,返回一个完全脱离 MobX 追踪的普通 JavaScript 对象。import { observable, toJS, isObservable } from 'mobx';const store = observable({ user: { name: 'Alice', address: { city: 'Shanghai', country: 'China' } }, tags: ['mobx', 'react']});const plain = toJS(store);// 转换结果完全脱离 observableisObservable(plain); // falseisObservable(plain.user); // falseisObservable(plain.tags); // false// 修改 plain 不会触发任何 MobX 响应plain.user.name = 'Bob'; // store.user.name 仍然是 'Alice'什么场景下必须用 toJS向 API 发送数据——后端不需要 MobX 的 Proxy 包装写入 localStorage / sessionStorage——JSON.stringify 能直接序列化 observable,但如果需要先加工再存储,toJS 拿到的是真正的普通对象,操作更安全传递给不兼容 Proxy 的第三方库——一些老库碰到 Proxy 会报错调试——在控制台直接 console.log(toJS(store)) 看到的就是干净的数据结构// 发送到 API@actionasync saveProfile() { const payload = toJS(this.profile); await api.updateProfile(payload);}// 写入 localStorage@actionpersistState() { const snapshot = toJS(this); localStorage.setItem('app-state', JSON.stringify(snapshot));}toJS 的性能代价toJS 是深度递归拷贝,数据量大时开销不小。不要在 render 或 computed 里频繁调用,只在真正需要脱离 observable 的边界处使用。// 反面案例:每次渲染都 toJSconst BadComponent = observer(() => { const data = toJS(store.items); // 每次响应式更新都深拷贝一次 return <List items={data} />;});// 正确做法:直接使用 observableconst GoodComponent = observer(() => { return <List items={store.items} />; // MobX 追踪读取,不产生额外拷贝});toJSON:序列化专用的遗留接口这里有一个关键事实需要澄清:toJSON 作为 MobX 的独立导出函数在 MobX 2.2 就已经被重命名为 toJS,后续版本不再导出 toJSON 函数。 现在提到 toJSON,通常指的是两种场景:场景一:Observable Map 上的 toJSON 方法MobX 的 Observable Map 实例上仍保留了 toJSON() 方法,但它只做浅层转换——把 Map 转成普通对象,内部嵌套的 observable 不会被剥离。import { observable } from 'mobx';const map = observable.map({ name: 'Alice', meta: observable.map({ role: 'admin' }) });map.toJSON();// { name: 'Alice', meta: ObservableMap } — 内层仍是 observable如果你需要完整的普通对象,还是应该用 toJS:toJS(map);// { name: 'Alice', meta: { role: 'admin' } } — 完全剥离场景二:自定义 toJSON 控制序列化输出JavaScript 原生的 JSON.stringify 在序列化对象时会调用该对象的 toJSON() 方法(如果定义了的话)。你可以在 MobX store 类上自定义 toJSON,来控制 JSON.stringify 的输出内容——这在脱敏场景下很实用。class UserStore { name = 'Alice'; email = 'alice@example.com'; password = 's3cret'; token = 'abc123'; toJSON() { // 序列化时排除敏感字段 return { name: this.name, email: this.email }; }}const user = new UserStore();JSON.stringify(user);// {"name":"Alice","email":"alice@example.com"} — password 和 token 被过滤toJS 与 toJSON 的核心区别| 维度 | toJS | 自定义 toJSON ||------|------|--------------|| 来源 | MobX 导出函数 | 对象实例方法 || 深度 | 递归剥离所有层 | 取决于你的实现 || 触发方式 | 显式调用 toJS(obj) | JSON.stringify(obj) 自动调用 || 返回值 | 普通对象 | 由你决定返回什么 || 主要用途 | 彻底脱离 observable | 控制序列化输出格式 |observable.shallow:只追踪顶层,嵌套对象不管observable.shallow 创建的 observable 只对顶层属性的变化做出响应,嵌套的对象和数组不会被 MobX 接管。这是一个重要的性能优化手段。import { observable } from 'mobx';// 默认是 observable.deep — 递归到每一层const deepStore = observable({ user: { name: 'Alice' }, items: [1, 2, 3]});deepStore.user.name = 'Bob'; // 触发响应deepStore.items.push(4); // 触发响应// observable.shallow — 只看顶层const shallowStore = observable.shallow({ user: { name: 'Alice' }, items: [1, 2, 3]});shallowStore.user.name = 'Bob'; // 不触发!user 引用没变shallowStore.items.push(4); // 不触发!items 引用没变shallowStore.user = { name: 'Bob' }; // 触发!顶层属性被替换了shallowStore.items = [1, 2, 3, 4]; // 触发!顶层属性被替换了什么时候用 shallow大型列表/数组——几千条数据的列表,如果每条都变成 observable,依赖追踪的开销很大。用 shallow 只追踪数组引用的替换,性能好得多第三方数据结构——如果嵌套对象有自己的变更机制(如 Immutable.js),不需要 MobX 再接管只关心"整体换掉"的场景——分页数据每次整批替换,不需要逐条追踪class ArticleStore { items = observable.shallow.array(); // 或 @observable.shallow items = []; @action async loadPage(page) { const data = await api.getArticles(page); this.items = data; // 整体替换,触发更新 }}shallow 的坑observable.shallow 最大的坑就是容易忘记它的限制:嵌套属性的修改是静默的,不会触发任何响应。如果你发现某个 observable 改了但 UI 没更新,先检查是不是用了 shallow 但代码里又在改嵌套属性。const store = observable.shallow({ config: { theme: 'dark' } });store.config.theme = 'light'; // 不会触发更新!// 正确做法:替换整个顶层属性store.config = { theme: 'light' }; // 触发更新observable.deep:默认行为,递归到每一层observable.deep 就是 observable() 的默认行为,所有嵌套属性都会被自动转换为 observable。大多数情况下你不需要显式写 observable.deep,但了解它的存在有助于和 shallow 对比。const store = observable.deep({ user: { name: 'Alice', address: { city: 'Shanghai' } }, items: [1, 2, 3]});// 任何层级的修改都会触发响应store.user.address.city = 'Beijing'; // 触发store.items.push(4); // 触发三者对比总结| 维度 | toJS | 自定义 toJSON | observable.shallow ||------|------|--------------|-------------------|| 定位 | 转换工具 | 序列化钩子 | 创建策略 || 做了什么 | 深度剥离 observable | 控制序列化输出 | 只让顶层变成 observable || 返回值 | 普通对象 | 由实现决定 | observable 对象(浅层) || 是否脱离响应式 | 是 | 取决于实现 | 否 || 典型用途 | API 请求、存储 | 脱敏序列化 | 大数据列表性能优化 |性能实践用 shallow 替代 deep 处理大数组// 问题:深度追踪几千个元素class TodoStore { @observable todos = []; // 每条 todo 的每个字段都是 observable}// 解决:只追踪数组引用class TodoStore { @observable.shallow todos = []; // 数组内元素保持原样 @action setTodos(data) { this.todos = data; // 整体替换触发更新 }}不要在 computed 或 render 里调 toJS// 反面:computed 里 toJS 产生不必要的深拷贝@computed get activeItems() { return toJS(this.items).filter(i => i.active);}// 正确:直接操作 observable@computed get activeItems() { return this.items.filter(i => i.active);}边界处才 toJS,内部直接用 observableclass DataStore { @observable data = {}; // 边界:发送给后端时才 toJS @action async save() { await api.save(toJS(this.data)); } // 内部:直接读 observable,零开销 @computed get summary() { return `${this.data.items.length} items`; }}常见误区误区一:认为 JSON.stringify(observable) 不行实际上 JSON.stringify 对 MobX observable 是能正常工作的,MobX 内部实现了序列化支持。但如果你想先修改数据再序列化,或者需要排除某些字段,才需要先 toJS 或自定义 toJSON。误区二:把 shallow 当万能性能优化observable.shallow 适合数据整体替换的场景。如果你的业务逻辑需要追踪嵌套属性的变化(比如编辑表单里单个字段),用 shallow 反而会导致 UI 不更新。误区三:混淆 toJS 和 toJSON 的职责toJS 是 MobX 提供的工具函数,负责把 observable 变成普通对象;toJSON 是 JavaScript 序列化协议的钩子方法,负责控制 JSON.stringify 的输出。两者解决的是不同层面的问题,不要混用。总结toJS——在 observable 与外部世界(API、存储、第三方库)的边界处使用,深度剥离 Proxy,返回纯 JS 对象。不要在 computed 或 render 里频繁调用。toJSON——不是 MobX 的独立函数,而是 JavaScript 序列化协议的钩子。在 store 类上自定义 toJSON 可以控制 JSON.stringify 的输出,常用于排除敏感字段。observable.shallow——只让顶层属性变成 observable,适合大型数据列表的整体替换场景。记住嵌套属性修改不会触发更新,选错场景反而踩坑。
前端阅读 05月27日 18:12

MobX autorun、reaction 和 when 有什么区别?

三个都是 MobX 的 reaction 工具,区别在于追踪粒度和执行策略:autorun 自动追踪所有依赖并立即执行,reaction 手动指定追踪数据并延迟执行,when 只在条件满足时执行一次就自动清理。autorun 最"懒人"——写一个函数,里面用到的 observable 变了它就重跑,创建时还会先跑一次。适合同步状态到 localStorage、更新 document.title 这类"有依赖就响应"的场景。缺点是容易多追踪,函数里不小心读了个不相关的 observable,它也会跟着重跑。reaction 把"追踪什么"和"做什么"拆成了两个函数,第一个函数返回值变了才触发第二个。默认不会立即执行(除非设 fireImmediately: true),而且第二个函数里读的 observable 不会被追踪。适合需要精确控制触发条件的情况,比如只监听 userId 变化去加载用户数据,而不想因为 user 对象其他字段变化而重复请求。when 是一次性的——条件函数返回 true 时执行效果函数,然后自动 dispose。适合等待初始化完成、等待数据加载这类"到了就执行,执行完就拉倒"的逻辑。如果用 autorun 或 reaction 模拟这个行为,你得手动判断条件再 dispose,容易忘。追问reaction 的 fireImmediately 和 autorun 有什么区别?fireImmediately 让 effect 函数在创建时执行一次,但追踪范围仍然是第一个函数指定的,不会追踪 effect 函数里的 observable。autorun 则是把整个函数里的 observable 都追踪了。所以 fireImmediately 只是改了执行时机,没改追踪逻辑。项目里 reaction 忘记 dispose 会怎样?和 useEffect 忘记清理一样——组件卸载后 reaction 还在跑,继续占用内存,observable 变了还会触发回调,可能操作已卸载的组件状态,导致内存泄漏甚至报错。autorun 和 when 同理,都必须在组件卸载时调用返回的 disposer。when 的条件一直不满足怎么办?when 会一直监听,永不执行 effect。可以配合 setTimeout 手动调用 disposer 来设超时,或者用 when 返回的 Promise(MobX 6+)配合 Promise.race 做超时控制:await when(() => store.loaded);// 或者带超时await Promise.race([ when(() => store.loaded), delay(5000).then(() => { throw new Error('timeout') })]);autorun 里访问数组长度和访问数组元素,追踪行为有区别吗?有。store.items.length 只追踪 length,store.items[0] 追踪具体下标,store.items.map(...) 追踪整个数组。用 reaction 可以避免这个问题——在 data 函数里只返回你需要的数据。写段代码// autorun: 自动追踪,立即执行autorun(() => { document.title = `${store.count} items`;});// reaction: 精确追踪,延迟执行reaction( () => store.userId, (id, prevId) => { loadProfile(id); });// when: 条件满足后执行一次when( () => store.initialized, () => { startApp(); });
前端阅读 05月27日 18:11

MobX 中 makeObservable、makeAutoObservable 和装饰器有什么区别?

三者的核心区别在于声明方式:makeObservable 需要显式标注每个成员的类型,makeAutoObservable 自动推断成员类型,装饰器用 @ 语法标记但需要编译器支持。MobX 6 之后官方推荐函数式 API(makeObservable / makeAutoObservable),装饰器变为可选项。传统装饰器(legacy decorators)永远不会成为 JS 标准的一部分,MobX 7 将移除对它们的支持。如果你还在用 @observable 写法,迁移计划该提上日程了。makeObservable:精确控制每个属性class TodoStore { todos = []; loading = false; constructor() { makeObservable(this, { todos: observable.shallow, // 浅层观察,数组引用变才触发 loading: observable, unfinishedCount: computed, addTodo: action, fetchTodos: flow // flow 处理 async/await }); } get unfinishedCount() { return this.todos.filter(t => !t.done).length; } addTodo(text) { this.todos.push({ text, done: false }); } *fetchTodos() { this.loading = true; try { const res = yield fetch("/api/todos"); this.todos = yield res.json(); } finally { this.loading = false; } }}makeObservable 最大的价值是精细控制。observable.shallow 只观察引用变化,数组内部对象的改动不会触发响应——这在列表渲染场景下能避免大量不必要的 re-render。observable.ref 只观察赋值,不做深度转换,适合存不可变数据。flow 专门标注 generator 函数处理异步流程,自动管理 pending/error 状态。缺点也明显:每个属性都要手动标注,漏写一个就丢响应性,而且这类 bug 不会报错,只是默默不更新。makeAutoObservable:自动推断,省心省力class TodoStore { todos = []; loading = false; constructor() { makeAutoObservable(this); } get unfinishedCount() { return this.todos.filter(t => !t.done).length; } addTodo(text) { this.todos.push({ text, done: false }); }}推断规则很直接:字段 → observable,getter → computed,方法 → action。一个 makeAutoObservable(this) 就完事。如果某个成员不想被自动推断,可以覆盖:constructor() { makeAutoObservable(this, { todos: observable.shallow, // 覆盖:用浅层观察 helper: false, // 排除:不使其可观察 fetchTodos: flow // 覆盖:generator 用 flow });}以 _ 开头的属性默认不会被自动推断,这是 MobX 的约定。如果你有内部辅助字段不想暴露为响应式,加个下划线前缀就行。注意:makeAutoObservable 不能用在有超类的类上。子类继承时会报错,因为自动推断无法正确处理继承链上的属性。这种场景必须用 makeObservable。装饰器:语法糖,有前提条件class TodoStore { @observable todos = []; @observable loading = false; @computed get unfinishedCount() { return this.todos.filter(t => !t.done).length; } @action addTodo(text) { this.todos.push({ text, done: false }); }}装饰器写法最直观,属性和类型标注在一起,读起来很清晰。但有两个前提条件经常被忽略:必须配置编译器。TypeScript 需要在 tsconfig.json 中启用 experimentalDecorators,Babel 需要 @babel/plugin-proposal-decorators。没配对就报错,配错了行为也可能不一致。传统装饰器 vs 标准装饰器。MobX 6 同时支持两种,但行为不同。传统装饰器(legacy)用 @observable x = value,标准装饰器(Stage 3)用 @observable accessor x = value。2023 年之后 TC39 确定的标准写法是后者,传统写法已被废弃。另外,用了装饰器不代表可以省掉 makeObservable。MobX 6 中,即使类上写了 @observable,构造函数里还是得调用 makeObservable(this),否则装饰器不生效。这一点很多人踩坑。怎么选?| 场景 | 推荐 | 原因 ||------|------|------|| 新项目,没有装饰器依赖 | makeAutoObservable | 最少代码,自动推断 || 需要浅层观察或排除某些属性 | makeAutoObservable + 覆盖 | 覆盖写法比全手动省事 || 有继承关系的 Store | makeObservable | makeAutoObservable 不支持继承 || 需要 observable.shallow / observable.ref | makeObservable | 精细控制每个属性 || 项目已有装饰器配置,团队习惯 | 装饰器 + makeObservable(this) | 不用为了迁移而迁移 |一句话:默认用 makeAutoObservable,碰到继承或需要精细控制时换 makeObservable,装饰器只在已有项目依赖时继续用。追问makeAutoObservable 和 makeObservable 可以混用吗?不行。一个类里只能选一个。但 makeAutoObservable 的第二个参数本身就是覆盖写法,本质上就是 makeAutoObservable + 部分手动标注的混合体。装饰器写的老项目怎么迁移到 makeAutoObservable?分两步:先把 @observable / @computed / @action 标注转为 makeObservable(this, {...}) 的写法,确认行为一致后,再考虑能否简化为 makeAutoObservable。迁移过程中最容易漏的是 makeObservable(this) 这个调用——老代码用了装饰器但忘记在构造函数里调用它,迁移时同样容易忘。observable.shallow 和 observable 有什么区别?observable 会递归地把对象内部所有嵌套属性都变成可观察的,observable.shallow 只观察第一层引用。对于数组,observable.shallow 只在数组引用变化时触发响应,数组内部元素的属性变化不会触发。列表渲染场景用 observable.shallow 能显著减少不必要的更新。为什么 makeAutoObservable 不支持继承?因为自动推断在遍历 this 上的所有属性时,无法区分哪些是从父类继承的、哪些是子类自己的。父类可能已经对自己的属性做了 makeAutoObservable,子类再调一次就会重复处理。所以 MobX 直接禁止了这种用法,有继承需求的必须用 makeObservable 显式标注。写段代码// 实际项目中常见的模式:// 基类用 makeObservable,子类也用 makeObservable + overrideclass BaseStore { loading = false; constructor() { makeObservable(this, { loading: observable, }); }}class TodoStore extends BaseStore { todos = []; constructor() { super(); makeObservable(this, { loading: override, // 继承的属性用 override todos: observable.shallow, addTodo: action.bound, // 自动绑定 this }); } addTodo(text) { this.todos.push({ text, done: false }); }}
前端阅读 05月27日 18:11

什么是 MobX,它的核心概念和工作原理是什么?

MobX 是一个响应式状态管理库,它的核心思路只有一句话:任何源自状态的内容都应该自动派生。你修改了状态,依赖这个状态的视图、计算值、副作用全部自动更新,不需要手动触发。Observable:让状态可被追踪用 observable 标记一个值,MobX 就会开始追踪它的读写:import { observable } from 'mobx';const store = observable({ count: 0, name: 'MobX'});store.count++; // MobX 知道这件事发生了底层实现上,MobX 劫持了对象属性的 getter/setter(数组则代理了变异方法)。当属性被读取时,当前正在运行的 derivation 会被记录为该属性的依赖;当属性被写入时,所有依赖它的 derivation 会被加入更新队列。Computed:按需计算的派生值computed 用来定义从 observable 派生出来的值,它有两个关键特性:惰性求值和缓存。import { observable, computed } from 'mobx';const store = observable({ firstName: '张', lastName: '三'});const fullName = computed(() => store.firstName + store.lastName);console.log(fullName.get()); // "张三"只有当有人读取 fullName 且 firstName 或 lastName 发生了变化时,才会重新计算。中间没有消费者读取的话,即使依赖变了也不会执行计算逻辑,这就是惰性求值。Action:状态修改的唯一入口MobX 6 默认要求所有状态修改都在 action 内完成:import { action } from 'mobx';const increment = action(() => { store.count++;});action 的作用不只是语义上的约束。MobX 会在 action 执行期间批量收集变更,等 action 结束后才统一通知 reaction 和 observer,避免中间状态触发多余渲染。action.bound 还会自动绑定 this,适合作为类方法使用。Reaction:响应状态变化的副作用Reaction 是"状态变了就做事"的机制,三种常用形式各有侧重:autorun:立即执行一次,之后依赖变化就重新执行。适合日志、同步等无条件响应的场景。reaction:分两步——先运行数据追踪函数,再运行副作用函数。只有追踪函数返回的值变化时才触发副作用,控制粒度更细。when:条件满足时执行一次就自动销毁。适合"等到某个状态再处理"的一次性逻辑。import { autorun, reaction, when } from 'mobx';// 每次变化都执行autorun(() => console.log('当前计数:', store.count));// 只有 count 变了才执行(name 变了不触发)reaction( () => store.count, (count) => console.log('计数变为:', count));// count 到 10 时执行一次when(() => store.count >= 10, () => console.log('达标了'));Observer:让 React 组件自动响应状态在 React 中,observer 是连接 MobX 状态和组件渲染的桥梁:import { observer } from 'mobx-react-lite';const Counter = observer(() => { return <div>{store.count}</div>;});observer 本质上是一个高阶组件,它在组件渲染时收集该组件对 observable 的依赖,当依赖变化时触发组件重渲染。与 useState + useEffect 手动管理的方式相比,你不需要写任何订阅或取消订阅的逻辑。依赖追踪的工作原理MobX 的核心机制是依赖图(dependency graph)。每当一个 derivation(computed、autorun、observer 组件等)运行时,MobX 会记录它访问了哪些 observable 属性,建立一条从 observable 到 derivation 的有向边。属性变化时,沿着这条边反向通知。这个图是动态维护的——如果 derivation 这次运行没有访问某个属性,对应的边就会被移除。所以不会出现"以前依赖过但现在已经不用了却还在更新"的问题。事务机制也很关键:在 action 内多次修改不同的 observable,MobX 只会在 action 结束后触发一次更新,中间状态不会泄露给 reaction。MobX 与 Redux 的关键差异| 维度 | MobX | Redux ||------|-------|-------|| 状态修改 | 直接赋值,action 内修改即可 | 必须通过 dispatch + reducer,返回新对象 || 样板代码 | 极少 | action type、action creator、reducer、dispatch || 更新粒度 | 细粒度,只有真正依赖变化数据的组件更新 | 组件需手动用 selector 或 reselect 控制重渲染 || 学习曲线 | 概念少,上手快 | 中间件、thunk/saga 等概念较多 || 可预测性 | action 默认可追踪,但自由度高 | 纯函数 reducer,状态变更可回放 || 适用场景 | 复杂对象关系、频繁局部更新 | 需要严格状态审计、团队协作规范 |选择建议:如果你的应用状态树结构复杂、嵌套深、局部更新频繁,MobX 写起来更自然。如果团队重视状态变更的可审计性和一致性规范,Redux 的约束反而是优势。常见使用模式一个典型的 MobX Store 结构:import { makeAutoObservable } from 'mobx';class TodoStore { todos = []; filter = 'all'; constructor() { makeAutoObservable(this); } get filteredTodos() { switch (this.filter) { case 'done': return this.todos.filter(t => t.done); case 'pending': return this.todos.filter(t => !t.done); default: return this.todos; } } addTodo(text) { this.todos.push({ text, done: false, id: Date.now() }); } toggle(id) { const todo = this.todos.find(t => t.id === id); if (todo) todo.done = !todo.done; }}makeAutoObservable 会自动把类的属性变成 observable、getter 变成 computed、方法变成 action,是最简洁的用法。
前端阅读 05月27日 18:11

MobX 6 有哪些主要变化和新特性?

MobX 6 最核心的变化有三个:强制 action 修改状态、引入 makeObservable/makeAutoObservable 替代装饰器、Proxy 成为底层实现。装饰器仍然支持但不再是推荐方式,配合 mobx-undecorate 工具可以一键迁移旧代码。追问makeObservable 和 makeAutoObservable 有什么区别?makeObservable 需要你手动标注每个成员的类型(observable、computed、action),适合需要精细控制的场景。makeAutoObservable 自动推断:getter 标记为 computed,方法标记为 action,其余字段标记为 observable。但 makeAutoObservable 不能用于子类,也不能标注被忽略的字段——这种时候用 makeObservable。class Store { count = 0; constructor() { // 二选一 makeObservable(this, { count: observable, doubled: computed, increment: action }); // 或者 makeAutoObservable(this); } get doubled() { return this.count * 2; } increment() { this.count++; }}为什么 MobX 6 强制要求在 action 中修改状态?MobX 5 可以在 action 外部直接修改 observable,这导致状态变更难以追踪,调试时无法定位是哪段代码改了数据。MobX 6 默认 enforceActions: "always",所有状态修改必须在 action 内进行,这样每次状态变更都有明确的调用栈,DevTools 也能清晰展示变更来源。如果迁移时不想立刻改,可以临时配置 enforceActions: "never" 回退到旧行为。装饰器为什么不再是推荐方式?TC39 装饰器提案经历了多次语法变更,Legacy Decorators(Babel experimentalDecorators)一直不是标准。MobX 6 选择拥抱标准:用 makeObservable 在 constructor 中声明式标注成员类型,这在任何 JS 环境下都能运行,不需要 Babel 插件或 TypeScript 实验性配置。如果你仍想用装饰器,MobX 6 也支持,但需要在 constructor 里补一句 makeObservable(this) 才能生效。MobX 5 升级到 6 的迁移步骤是什么?先升级到 MobX 5 的最新小版本,解决所有废弃警告安装 MobX 6,运行 npx mobx-undecorate 自动迁移代码TypeScript 项目设置 useDefineForClassFields: true;Babel 项目设置 ["@babel/plugin-proposal-class-properties", { "loose": false }]每个有 MobX 成员的类,在 constructor 中调用 makeObservable(this) 或 makeAutoObservable(this)用 configure({ enforceActions: "always" }) 启用严格模式替换已移除的 API:decorate() 用 makeObservable 替代,isObservableObject 用 isObservable 替代Proxy 在 MobX 6 中扮演什么角色?MobX 5 默认也用 Proxy,但可以降级到 getter/setter 实现。MobX 6 将 Proxy 作为唯一的响应式实现(IE 和旧版 React Native 除外,需配置 useProxies: "never")。Proxy 的好处是能拦截更多操作(如动态添加属性),Observable 对象的行为更接近普通对象,不需要额外的 API 来处理属性增删。写段代码import { makeAutoObservable } from "mobx";class TodoStore { todos = []; constructor() { makeAutoObservable(this); } get pending() { return this.todos.filter(t => !t.done); } addTodo(text) { this.todos.push({ text, done: false }); } toggle(id) { this.todos[id].done = !this.todos[id].done; }}
前端阅读 05月27日 18:11

MobX 和 Redux 有什么区别,应该如何选择?

MobX 和 Redux 是前端领域两种主流的状态管理方案,但它们的底层思路完全不同。Redux 走的是函数式、显式派发的路线,MobX 走的是响应式、自动追踪的路线。理解这个根本分歧,是做出技术选型的前提。核心设计理念的区别Redux 的哲学可以用三句话概括:单一数据源、状态只读、纯函数修改。整个应用的状态存在一棵对象树里,你永远不能直接改它,只能通过 dispatch 一个 action,由 reducer 这个纯函数算出下一个状态。这种设计让状态变化变得可追踪、可复现。MobX 的思路则截然相反——它允许你直接修改状态,但通过 observable 包装后,MobX 会自动追踪哪些组件依赖了哪些数据,当数据变化时自动触发对应组件的更新。你不需要写 reducer,不需要 dispatch action,改了数据,UI 就会跟着变。用代码对比会更直观。同样是管理一个计数器:Redux 写法:// 定义 action typeconst INCREMENT = 'counter/increment';// 定义 action creatorfunction increment() { return { type: INCREMENT };}// 定义 reducerfunction counterReducer(state = { value: 0 }, action) { switch (action.type) { case INCREMENT: return { ...state, value: state.value + 1 }; default: return state; }}// 组件中使用function Counter() { const count = useSelector(state => state.counter.value); const dispatch = useDispatch(); return <button onClick={() => dispatch(increment())}>{count}</button>;}MobX 写法:// 定义 storeclass CounterStore { @observable value = 0; @action increment() { this.value++; }}const counterStore = new CounterStore();// 组件中使用(observer 自动追踪依赖)const Counter = observer(() => ( <button onClick={() => counterStore.increment()}> {counterStore.value} </button>));可以看到,Redux 为了改一个数字,需要定义 action type、action creator、reducer 三层结构;而 MobX 直接在方法里改值就行。这就是两种方案最核心的体验差异。样板代码与开发效率Redux 最常被诟病的就是样板代码太多。一个简单的增删改查,你可能要写 action types、action creators、reducers、selectors,还要配置 store 和 middleware。对于小需求来说,这套流程显得很重。Redux Toolkit 的出现大幅缓解了这个问题。它用 createSlice 把 action 和 reducer 合并到一起,用 createAsyncThunk 处理异步,样板代码减少了很多:const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: state => { state.value += 1; }, },});注意上面 reducer 里直接写了 state.value += 1——Redux Toolkit 内部用了 Immer,所以你表面上在"直接修改",实际上 Immer 帮你生成了不可变的新状态。相比之下,MobX 从一开始就不需要这么多仪式感。定义 observable 数据、写 action 方法、用 observer 包组件,这三步就够了。对于快速迭代的项目,这种简洁性能省下不少时间。性能机制对比Redux 的更新机制比较"粗暴"——每次 dispatch action 后,reducer 返回新的 state 对象,所有通过 useSelector 订阅了这个 state 的组件都会被通知。虽然 useSelector 默认用浅比较来决定是否重渲染,但如果 selector 返回的引用每次都不同(比如返回了一个新数组或新对象),组件就会不必要地更新。这时候需要用 reselect 来做记忆化:const selectFilteredItems = createSelector( state => state.items, state => state.filter, (items, filter) => items.filter(item => item.type === filter));MobX 的机制更精细。它在运行时追踪每个 observer 组件实际访问了哪些 observable 属性,只有这些属性变化时才会重渲染组件。这个过程是自动的,不需要开发者手动优化。如果一个组件只用了 store.userName,那 store.age 变化不会触发它更新。在大多数中大型应用中,这种细粒度追踪比 Redux 的浅比较更高效,尤其是状态树很大但单个组件只用其中一小部分数据的场景。异步处理的差异Redux 处理异步的经典方案是中间件。早期用 redux-thunk,后来社区转向 redux-saga 和 Redux Toolkit 的 createAsyncThunk:const fetchUser = createAsyncThunk('user/fetch', async (userId) => { const res = await fetch(`/api/users/${userId}`); return res.json();});const userSlice = createSlice({ name: 'user', initialState: { data: null, status: 'idle' }, extraReducers: builder => { builder .addCase(fetchUser.pending, state => { state.status = 'loading'; }) .addCase(fetchUser.fulfilled, (state, action) => { state.status = 'succeeded'; state.data = action.payload; }) .addCase(fetchUser.rejected, state => { state.status = 'failed'; }); },});MobX 处理异步则简单得多——在 action 里直接写 async/await 就行,不需要额外的中间件:class UserStore { @observable data = null; @observable status = 'idle'; @action async fetchUser(userId) { this.status = 'loading'; try { const res = await fetch(`/api/users/${userId}`); this.data = await res.json(); this.status = 'succeeded'; } catch { this.status = 'failed'; } }}如果你用过 Redux 处理复杂异步流程(比如并行请求、取消请求、条件触发),就会感受到 Redux 在这方面的抽象能力更强,但也更啰嗦。MobX 胜在直觉和简洁。TypeScript 体验Redux 和 TypeScript 的结合曾经很痛苦——你需要为 state、action、dispatch 分别定义类型,类型体操写起来很繁琐。Redux Toolkit 改善了很多,createSlice 能自动推断出 action 的类型,但遇到 extraReducers 和异步 thunk 时,类型推导仍然需要额外的泛型标注。MobX 和 TypeScript 的配合更顺畅。observable 属性的类型就是你在 class 里声明的类型,action 的参数类型也是方法签名的类型,不需要额外的类型桥接。编译器能自然地推断出大多数类型,代码也更易读。调试与可维护性Redux 最大的优势之一是调试体验。因为每次状态变化都对应一个明确的 action,Redux DevTools 能完整记录状态变更历史,支持时间旅行——你可以回到任意一个历史状态查看应用当时的样子。这对排查复杂 bug 非常有用。MobX 的调试相对困难一些。虽然 MobX 也有 DevTools,但由于状态可以直接在 action 中修改,修改点分散在各处,不像 Redux 那样有一个统一的 action 日志。在大型项目里,如果不严格遵守"所有状态修改都在 action 中进行"的规范,调试起来会很头疼。可维护性方面,Redux 的约束实际上是一种保护——它强制团队按照统一的模式写代码,新人接手时只要理解了 Redux 的数据流模式,就能比较容易地看懂业务逻辑。MobX 的自由度更高,但如果团队没有建立好编码规范,代码风格可能千差万别。什么时候选 Redux团队规模较大,需要统一的状态管理模式来降低协作成本业务逻辑对状态变化的可追溯性要求高(如金融、交易系统)已有成熟的 Redux 技术栈和工具链,团队经验丰富需要频繁使用 DevTools 的时间旅行功能排查问题项目中大量使用中间件处理复杂副作用(如轮询、WebSocket)什么时候选 MobX团队规模中小型,追求开发速度和迭代效率状态模型比较复杂(深嵌套对象、频繁修改),不可变更新写起来很痛苦希望减少样板代码,把精力集中在业务逻辑上已有面向对象编程背景的团队,class + decorator 的写法更自然两者能否共存在同一个项目里混用 Redux 和 MobX 并不常见,但也不是不可行。比如你可以在全局状态层面使用 Redux(保证可追溯性),在局部复杂表单状态使用 MobX(减少不可变更新的负担)。不过这种方式会增加新人理解项目的成本,除非有非常充分的理由,否则不建议这么做。更值得考虑的替代方案是 Zustand、Jotai 或 Valtio 这些轻量级状态库。Zustand 的 API 比 Redux 简洁得多,同时保留了不可变状态的核心思想;Valtio 则融合了 MobX 的可变状态思路和 Proxy 追踪机制,体积更小。如果你的项目不需要 Redux 的完整工具链,这些轻量方案值得认真评估。
前端阅读 02月21日 15:45

MobX 的依赖追踪系统是如何工作的?

MobX 的依赖追踪系统是其核心机制,它通过细粒度的追踪实现了高效的响应式更新。以下是 MobX 依赖追踪的详细工作原理:依赖追踪的基本原理MobX 使用观察者模式和依赖图来实现依赖追踪。当 observable 被访问时,MobX 会建立依赖关系;当 observable 被修改时,MobX 会通知所有依赖它的观察者。核心组件1. Reaction(反应)Reaction 是依赖追踪的执行单元,包括:autorun:立即执行,并在依赖变化时自动重新执行reaction:提供更细粒度的控制,可以指定追踪函数和效果函数observer(React 组件):包装 React 组件,使其能够响应状态变化computed:计算属性,也是一种特殊的 reaction2. Derivation(派生)Derivation 表示依赖于 observable 的计算或副作用。每个 derivation 维护一个依赖列表。3. Atom(原子)Atom 是最小的可观察单元,每个 observable 对象、数组、Map 等都由多个 atom 组成。依赖追踪的执行流程1. 追踪阶段(Tracing)当 reaction 执行时:autorun(() => { console.log(store.count); // 访问 observable});执行步骤:MobX 将当前 reaction 标记为"正在追踪"当访问 store.count 时,MobX 记录下这个 reaction 依赖于 count 这个 atom继续执行,记录所有访问的 observable执行完成后,reaction 进入"稳定"状态2. 通知阶段(Notification)当 observable 被修改时:runInAction(() => { store.count++; // 修改 observable});执行步骤:MobX 检测到 count atom 被修改查找所有依赖于 count 的 reaction将这些 reaction 标记为"过时"(stale)在下一个事件循环中,重新执行这些 reaction依赖图的结构MobX 维护一个双向的依赖图:Atom → Derivation:每个 atom 知道哪些 derivation 依赖于它Derivation → Atom:每个 derivation 知道自己依赖于哪些 atom这种双向关系使得 MobX 能够高效地进行依赖更新和清理。细粒度更新MobX 的依赖追踪是细粒度的,这意味着:只更新真正需要更新的部分避免不必要的重新计算和重新渲染自动处理嵌套的依赖关系示例:class Store { @observable firstName = 'John'; @observable lastName = 'Doe'; @observable age = 30; @computed get fullName() { return `${this.firstName} ${this.lastName}`; }}const observerComponent = observer(() => { // 只依赖 fullName,不依赖 age return <div>{store.fullName}</div>;});当 age 变化时,组件不会重新渲染;只有当 firstName 或 lastName 变化时才会重新渲染。批量更新MobX 会自动批量更新,避免多次触发 reaction:runInAction(() => { store.firstName = 'Jane'; store.lastName = 'Smith'; store.age = 25;});即使修改了多个 observable,相关的 reaction 只会执行一次。依赖清理当 reaction 不再需要时,MobX 会自动清理依赖关系:组件卸载时,observer 会自动清理使用 dispose() 方法手动清理 reaction避免内存泄漏性能优化MobX 的依赖追踪系统提供了多种性能优化:懒计算:computed 只在需要时才计算缓存机制:computed 的结果会被缓存批量更新:多个状态变化合并为一次更新细粒度追踪:只追踪真正需要的依赖调试依赖追踪MobX 提供了调试工具来查看依赖关系:import { trace } from 'mobx';// 追踪 computed 的依赖trace(store.fullName);// 追踪 reaction 的依赖autorun(() => { console.log(store.count);}, { name: 'myReaction' });常见问题1. 循环依赖MobX 能够检测和避免循环依赖,但设计时应尽量避免。2. 过度追踪避免在循环或条件中访问 observable,这可能导致不必要的依赖。3. 内存泄漏确保在组件卸载时清理 reaction,避免内存泄漏。总结MobX 的依赖追踪系统通过观察者模式和依赖图实现了高效的响应式更新。理解这个系统的工作原理有助于编写更高效的 MobX 代码,并避免常见的性能问题。
前端阅读 02月21日 15:45

如何测试 MobX 应用?

MobX 的测试策略和工具对于构建可靠的应用至关重要。以下是 MobX 测试的完整指南:1. 测试 Store基本测试import { UserStore } from './UserStore';describe('UserStore', () => { let store; beforeEach(() => { store = new UserStore(); }); it('should initialize with default values', () => { expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); it('should login user', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).not.toBeNull(); expect(store.isAuthenticated).toBe(true); }); it('should logout user', () => { store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; store.logout(); expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); });});测试 computed 属性describe('ProductStore', () => { let store; beforeEach(() => { store = new ProductStore(); }); it('should compute featured products', () => { store.products = [ { id: 1, name: 'Product 1', featured: true }, { id: 2, name: 'Product 2', featured: false }, { id: 3, name: 'Product 3', featured: true } ]; expect(store.featuredProducts).toHaveLength(2); expect(store.featuredProducts[0].name).toBe('Product 1'); expect(store.featuredProducts[1].name).toBe('Product 3'); }); it('should update when products change', () => { store.products = [{ id: 1, name: 'Product 1', featured: true }]; expect(store.featuredProducts).toHaveLength(1); store.products.push({ id: 2, name: 'Product 2', featured: true }); expect(store.featuredProducts).toHaveLength(2); });});测试异步 actiondescribe('AsyncStore', () => { let store; let mockApi; beforeEach(() => { mockApi = { fetchData: jest.fn().mockResolvedValue({ data: 'test' }) }; store = new AsyncStore(mockApi); }); it('should fetch data successfully', async () => { await store.fetchData(); expect(store.data).toEqual({ data: 'test' }); expect(store.loading).toBe(false); expect(mockApi.fetchData).toHaveBeenCalled(); }); it('should handle errors', async () => { mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false); });});2. 测试 React 组件测试 observer 组件import { render, screen, fireEvent } from '@testing-library/react';import { observer } from 'mobx-react-lite';import { UserStore } from './UserStore';const TestComponent = observer(({ store }) => ( <div> {store.isAuthenticated ? ( <div>Welcome, {store.user?.name}</div> ) : ( <div>Please login</div> )} <button onClick={store.login}>Login</button> </div>));describe('TestComponent', () => { it('should show login message when not authenticated', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); }); it('should show welcome message when authenticated', () => { const store = new UserStore(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; render(<TestComponent store={store} />); expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); }); it('should update when state changes', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); });});测试表单组件describe('FormComponent', () => { it('should update form data', () => { const store = new FormStore(); render(<FormComponent store={store} />); const input = screen.getByLabelText('Name'); fireEvent.change(input, { target: { value: 'John' } }); expect(store.formData.name).toBe('John'); }); it('should submit form', async () => { const store = new FormStore(); store.submit = jest.fn(); render(<FormComponent store={store} />); const button = screen.getByText('Submit'); fireEvent.click(button); expect(store.submit).toHaveBeenCalled(); });});3. 使用 MobX 测试工具使用 spyimport { spy } from 'mobx';describe('Spy Usage', () => { it('should spy on observable changes', () => { const store = observable({ count: 0 }); const countSpy = jest.fn(); spy(store, 'count', (change) => { countSpy(change); }); store.count = 1; store.count = 2; expect(countSpy).toHaveBeenCalledTimes(2); });});使用 traceimport { trace } from 'mobx';describe('Trace Usage', () => { it('should trace computed dependencies', () => { const store = observable({ firstName: 'John', lastName: 'Doe' }); const fullName = computed(() => `${store.firstName} ${store.lastName}`); // 追踪依赖 trace(fullName); expect(fullName.get()).toBe('John Doe'); });});使用 isObservableimport { isObservable } from 'mobx';describe('IsObservable Usage', () => { it('should check if object is observable', () => { const observableObj = observable({ count: 0 }); const plainObj = { count: 0 }; expect(isObservable(observableObj)).toBe(true); expect(isObservable(plainObj)).toBe(false); });});4. Mock API 调用使用 Jest mockimport { UserStore } from './UserStore';describe('UserStore with API', () => { let store; let mockApi; beforeEach(() => { mockApi = { login: jest.fn(), logout: jest.fn(), fetchUser: jest.fn() }; store = new UserStore(mockApi); }); it('should call API on login', async () => { mockApi.login.mockResolvedValue({ id: 1, name: 'Test' }); await store.login({ username: 'test', password: 'test' }); expect(mockApi.login).toHaveBeenCalledWith({ username: 'test', password: 'test' }); }); it('should handle API errors', async () => { mockApi.login.mockRejectedValue(new Error('Invalid credentials')); await expect(store.login({ username: 'test', password: 'test' })) .rejects.toThrow('Invalid credentials'); expect(store.error).toBe('Invalid credentials'); });});使用 MSW (Mock Service Worker)import { setupServer, rest } from 'msw';import { UserStore } from './UserStore';const server = setupServer( rest.post('/api/login', (req, res, ctx) => { return res( ctx.status(200), ctx.json({ id: 1, name: 'Test' }) ); }));describe('UserStore with MSW', () => { let store; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); beforeEach(() => { store = new UserStore(); }); it('should login successfully', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).toEqual({ id: 1, name: 'Test' }); expect(store.isAuthenticated).toBe(true); });});5. 测试 reactiondescribe('Reaction Testing', () => { it('should trigger reaction when observable changes', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 1; expect(reactionSpy).toHaveBeenCalledWith(1); store.count = 2; expect(reactionSpy).toHaveBeenCalledWith(2); }); it('should not trigger when value is same', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 0; expect(reactionSpy).not.toHaveBeenCalled(); });});6. 测试中间件describe('Middleware Testing', () => { it('should call middleware before action', () => { const middlewareSpy = jest.fn(); const actionSpy = jest.fn(); const store = { @observable count: 0, @action increment() { this.count++; } }; const originalIncrement = store.increment; store.increment = function(...args) { middlewareSpy(...args); return originalIncrement.apply(this, args); }; store.increment(); expect(middlewareSpy).toHaveBeenCalled(); expect(actionSpy).toHaveBeenCalled(); });});7. 集成测试describe('Integration Tests', () => { it('should handle complete user flow', async () => { const store = new RootStore(); // 登录 await store.userStore.login({ username: 'test', password: 'test' }); expect(store.userStore.isAuthenticated).toBe(true); // 加载数据 await store.dataStore.loadData(); expect(store.dataStore.data).not.toBeNull(); // 添加到购物车 store.cartStore.addItem(store.dataStore.data[0]); expect(store.cartStore.items).toHaveLength(1); // 结账 await store.cartStore.checkout(); expect(store.cartStore.items).toHaveLength(0); });});8. 测试最佳实践1. 隔离测试// 每个测试都应该独立beforeEach(() => { store = new Store();});// 清理副作用afterEach(() => { if (store.dispose) { store.dispose(); }});2. 使用快照测试it('should match snapshot', () => { const store = new Store(); store.data = { id: 1, name: 'Test' }; expect(toJS(store.data)).toMatchSnapshot();});3. 测试边界情况it('should handle empty array', () => { const store = new Store(); store.items = []; expect(store.itemCount).toBe(0);});it('should handle null values', () => { const store = new Store(); store.user = null; expect(store.userName).toBe('Guest');});4. 测试错误处理it('should handle network errors gracefully', async () => { const store = new Store(); mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false);});总结MobX 测试的关键点:测试 Store:验证 observable、computed 和 action 的行为测试组件:验证 observer 组件的响应性使用测试工具:spy、trace、isObservableMock API:使用 Jest mock 或 MSW测试 reaction:验证副作用是否正确触发测试中间件:验证中间件是否正确执行集成测试:验证多个 store 之间的交互最佳实践:隔离测试、快照测试、边界情况、错误处理遵循这些测试策略,可以构建可靠、可维护的 MobX 应用。