乐闻世界logo
搜索文章和话题

前端面试题手册

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

MobX 提供了多种工具来处理状态,包括 toJS、toJSON 和 observable.shallow。理解它们的区别和使用场景对于正确使用 MobX 至关重要。1. toJS基本用法toJS 将 observable 对象深度转换为普通 JavaScript 对象。import { observable, toJS } from 'mobx';const store = observable({ user: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }, items: [1, 2, 3]});// 转换为普通对象const plainObject = toJS(store);console.log(plainObject);// {// user: {// name: 'John',// age: 30,// address: { city: 'New York', country: 'USA' }// },// items: [1, 2, 3]// }// plainObject 不再是 observableconsole.log(isObservable(plainObject)); // falseconsole.log(isObservable(plainObject.user)); // falseconsole.log(isObservable(plainObject.items)); // false使用场景将 observable 对象发送到 API将 observable 对象存储到 localStorage将 observable 对象传递给不兼容 observable 的库调试时查看状态示例:发送到 API@actionasync saveData() { const plainData = toJS(this.data); await api.saveData(plainData);}示例:存储到 localStorage@actionsaveToLocalStorage() { const plainState = toJS(this.state); localStorage.setItem('appState', JSON.stringify(plainState));}2. toJSON基本用法toJSON 将 observable 对象转换为 JSON 可序列化的对象。import { observable, toJSON } from 'mobx';const store = observable({ user: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }, items: [1, 2, 3]});// 转换为 JSON 对象const jsonObject = toJSON(store);console.log(jsonObject);// {// user: {// name: 'John',// age: 30,// address: { city: 'New York', country: 'USA' }// },// items: [1, 2, 3]// }// 可以直接序列化为 JSONconst jsonString = JSON.stringify(store);console.log(jsonString);// {"user":{"name":"John","age":30,"address":{"city":"New York","country":"USA"}},"items":[1,2,3]}自定义 toJSONclass User { @observable name = 'John'; @observable password = 'secret'; @observable email = 'john@example.com'; toJSON() { return { name: this.name, email: this.email // 不包含 password }; }}const user = new User();const json = JSON.stringify(user);console.log(json);// {"name":"John","email":"john@example.com"}使用场景序列化 observable 对象为 JSON发送数据到服务器存储数据到数据库创建 API 响应3. observable.shallow基本用法observable.shallow 创建浅层可观察对象,只有顶层属性是可观察的。import { observable } from 'mobx';// 深度可观察(默认)const deepStore = observable({ user: { name: 'John', age: 30 }, items: [1, 2, 3]});// 浅层可观察const shallowStore = observable.shallow({ user: { name: 'John', age: 30 }, items: [1, 2, 3]});// deepStore 的嵌套对象也是可观察的deepStore.user.name = 'Jane'; // 会触发更新deepStore.items.push(4); // 会触发更新// shallowStore 的嵌套对象不是可观察的shallowStore.user.name = 'Jane'; // 不会触发更新shallowStore.items.push(4); // 不会触发更新// 但顶层属性的变化会触发更新shallowStore.user = { name: 'Jane', age: 30 }; // 会触发更新shallowStore.items = [1, 2, 3, 4]; // 会触发更新使用场景性能优化:减少需要追踪的依赖避免深度追踪带来的性能问题只需要追踪顶层变化处理大型数据结构示例:大型数组class Store { @observable.shallow items = []; constructor() { makeAutoObservable(this); } @action loadItems = async () => { const data = await fetch('/api/items').then(r => r.json()); this.items = data; // 只追踪整个数组的替换 };}4. observable.deep基本用法observable.deep 创建深度可观察对象,所有嵌套的属性都是可观察的(这是默认行为)。import { observable } from 'mobx';const deepStore = observable.deep({ user: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }, items: [1, 2, 3]});// 所有嵌套属性都是可观察的deepStore.user.name = 'Jane'; // 会触发更新deepStore.user.address.city = 'Boston'; // 会触发更新deepStore.items.push(4); // 会触发更新5. 对比总结| 特性 | toJS | toJSON | observable.shallow ||------|------|--------|-------------------|| 用途 | 转换为普通 JS 对象 | 转换为 JSON 对象 | 创建浅层可观察对象 || 深度 | 深度转换 | 深度转换 | 仅顶层可观察 || 返回值 | 普通 JS 对象 | JSON 可序列化对象 | 可观察对象 || 可观察性 | 不可观察 | 不可观察 | 可观察 || 使用场景 | API 调用、存储 | 序列化、API 响应 | 性能优化 |6. 性能考虑使用 shallow 优化性能// 不好的做法:深度可观察大型数组class BadStore { @observable items = []; // 可能有数千个元素}// 好的做法:浅层可观察class GoodStore { @observable.shallow items = []; @action loadItems = async () => { const data = await fetch('/api/items').then(r => r.json()); this.items = data; // 只追踪数组替换 };}避免频繁调用 toJS// 不好的做法:频繁调用 toJS@observerclass BadComponent extends React.Component { render() { const plainData = toJS(store.data); // 每次渲染都调用 return <div>{plainData.length}</div>; }}// 好的做法:缓存结果或直接使用 observable@observerclass GoodComponent extends React.Component { render() { return <div>{store.data.length}</div>; // 直接使用 observable }}7. 常见陷阱陷阱 1:在 computed 中调用 toJS// 不好的做法@computed get badComputed() { const plainData = toJS(this.data); return plainData.filter(item => item.active);}// 好的做法@computed get goodComputed() { return this.data.filter(item => item.active);}陷阱 2:忘记 shallow 的限制const shallowStore = observable.shallow({ items: []});// 不会触发更新shallowStore.items.push(1);// 会触发更新shallowStore.items = [1];陷阱 3:混淆 toJS 和 toJSONconst store = observable({ user: { name: 'John' }});// toJS 返回普通对象const plain = toJS(store);console.log(plain instanceof Object); // true// toJSON 返回 JSON 可序列化对象const json = toJSON(store);console.log(JSON.stringify(json)); // {"user":{"name":"John"}}8. 最佳实践1. 根据需求选择可观察深度// 小型数据结构:使用深度可观察const smallStore = observable({ config: { theme: 'dark', language: 'en' }});// 大型数据结构:使用浅层可观察const largeStore = observable.shallow({ items: [] // 可能有数千个元素});2. 在需要时才使用 toJS// 只在发送到 API 时使用@actionasync sendData() { const plainData = toJS(this.data); await api.sendData(plainData);}// 在组件中直接使用 observable@observerconst Component = () => { return <div>{store.data.length}</div>;};3. 自定义 toJSON 控制序列化class User { @observable id = 1; @observable name = 'John'; @observable password = 'secret'; toJSON() { return { id: this.id, name: this.name // 不包含敏感信息 }; }}总结理解 toJS、toJSON 和 observable.shallow 的区别和使用场景:toJS:将 observable 转换为普通 JS 对象,用于 API 调用和存储toJSON:将 observable 转换为 JSON 对象,用于序列化observable.shallow:创建浅层可观察对象,用于性能优化正确使用这些工具,可以构建更高效、更可维护的 MobX 应用。
阅读 0·2月21日 15:50

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

MobX 提供了多种工具来创建和管理可观察状态,包括 makeObservable、makeAutoObservable 和装饰器。理解它们的区别和使用场景对于正确使用 MobX 至关重要。1. makeObservable基本用法import { makeObservable, observable, computed, action } from 'mobx';class Store { count = 0; firstName = 'John'; lastName = 'Doe'; constructor() { makeObservable(this, { count: observable, firstName: observable, lastName: observable, fullName: computed, increment: action, decrement: action.bound }); } get fullName() { return `${this.firstName} ${this.lastName}`; } increment() { this.count++; } decrement = () => { this.count--; };}特点显式声明:需要显式声明每个属性的类型灵活性高:可以精确控制每个属性的行为类型安全:与 TypeScript 集成良好需要配置:需要在构造函数中调用适用场景需要精确控制每个属性的行为使用 TypeScript需要自定义配置高级用法class Store { data = []; loading = false; error = null; constructor() { makeObservable(this, { data: observable, loading: observable, error: observable, itemCount: computed, fetchData: action, clearData: action }, { autoBind: true }); // 自动绑定 this } get itemCount() { return this.data.length; } async fetchData() { this.loading = true; try { const response = await fetch('/api/data'); this.data = await response.json(); } catch (error) { this.error = error.message; } finally { this.loading = false; } } clearData() { this.data = []; this.error = null; }}2. makeAutoObservable基本用法import { makeAutoObservable } from 'mobx';class Store { count = 0; firstName = 'John'; lastName = 'Doe'; constructor() { makeAutoObservable(this); } get fullName() { return `${this.firstName} ${this.lastName}`; } increment() { this.count++; } decrement = () => { this.count--; };}特点自动推断:自动推断属性的类型简洁:代码更简洁,减少样板代码智能推断:getter → computed方法 → action字段 → observable可覆盖:可以覆盖默认推断适用场景快速开发不需要精确控制代码简洁性优先高级用法class Store { data = []; loading = false; error = null; _internalState = {}; // 以下划线开头的属性不会被自动推断 constructor() { makeAutoObservable(this, { // 覆盖默认推断 data: observable.deep, fetchData: flow, _internalState: false // 不使其可观察 }); } get itemCount() { return this.data.length; } fetchData = flow(function* () { this.loading = true; try { const response = yield fetch('/api/data'); this.data = yield response.json(); } catch (error) { this.error = error.message; } finally { this.loading = false; } });}3. 装饰器基本用法import { observable, computed, action } from 'mobx';class Store { @observable count = 0; @observable firstName = 'John'; @observable lastName = 'Doe'; @computed get fullName() { return `${this.firstName} ${this.lastName}`; } @action increment() { this.count++; } @action.bound decrement = () => { this.count--; };}特点声明式:使用装饰器语法简洁:代码更易读需要配置:需要 Babel 或 TypeScript 支持MobX 6 中可选:装饰器不再是必需的适用场景项目已配置装饰器支持喜欢装饰器语法需要与 MobX 4/5 兼容高级用法import { observable, computed, action, flow } from 'mobx';class Store { @observable data = []; @observable loading = false; @observable error = null; @computed get itemCount() { return this.data.length; } @action async fetchData() { this.loading = true; try { const response = await fetch('/api/data'); this.data = await response.json(); } catch (error) { this.error = error.message; } finally { this.loading = false; } } @action.bound clearData() { this.data = []; this.error = null; }}三者的对比| 特性 | makeObservable | makeAutoObservable | 装饰器 ||------|----------------|-------------------|--------|| 声明方式 | 显式配置 | 自动推断 | 装饰器 || 代码量 | 较多 | 少 | 少 || 灵活性 | 高 | 中 | 高 || TypeScript 支持 | 好 | 好 | 好 || 配置要求 | 需要 | 不需要 | 需要 Babel/TS || MobX 6 推荐 | 是 | 是 | 可选 |选择指南使用 makeObservable 当:需要精确控制每个属性的行为使用 TypeScript需要自定义配置需要覆盖默认行为class Store { data = []; constructor() { makeObservable(this, { data: observable.shallow, // 浅层可观察 itemCount: computed, fetchData: action }); }}使用 makeAutoObservable 当:快速开发不需要精确控制代码简洁性优先使用 MobX 6class Store { data = []; constructor() { makeAutoObservable(this); }}使用装饰器当:项目已配置装饰器支持喜欢装饰器语法需要与 MobX 4/5 兼容class Store { @observable data = [];}与 TypeScript 的集成makeObservable + TypeScriptimport { makeObservable, observable, computed, action } from 'mobx';class Store { count: number = 0; firstName: string = 'John'; lastName: string = 'Doe'; constructor() { makeObservable<Store>(this, { count: observable, firstName: observable, lastName: observable, fullName: computed, increment: action }); } get fullName(): string { return `${this.firstName} ${this.lastName}`; } increment(): void { this.count++; }}makeAutoObservable + TypeScriptimport { makeAutoObservable } from 'mobx';class Store { count: number = 0; firstName: string = 'John'; lastName: string = 'Doe'; constructor() { makeAutoObservable(this); } get fullName(): string { return `${this.firstName} ${this.lastName}`; } increment(): void { this.count++; }}装饰器 + TypeScriptimport { observable, computed, action } from 'mobx';class Store { @observable count: number = 0; @observable firstName: string = 'John'; @observable lastName: string = 'Doe'; @computed get fullName(): string { return `${this.firstName} ${this.lastName}`; } @action increment(): void { this.count++; }}最佳实践1. MobX 6 推荐使用 makeAutoObservable// 推荐class Store { count = 0; constructor() { makeAutoObservable(this); }}// 也可以使用 makeObservableclass Store { count = 0; constructor() { makeObservable(this, { count: observable }); }}2. 使用 makeObservable 覆盖默认行为class Store { data = []; constructor() { makeAutoObservable(this, { data: observable.shallow // 覆盖默认的深度可观察 }); }}3. 使用 action.bound 或 autoBindclass Store { count = 0; constructor() { makeAutoObservable(this, {}, { autoBind: true }); } increment() { this.count++; // this 自动绑定 }}4. 私有属性处理class Store { data = []; _privateData = []; // 以下划线开头,不会被自动推断 constructor() { makeAutoObservable(this, { _privateData: false // 明确不使其可观察 }); }}常见问题1. 装饰器不工作确保:配置了 Babel 或 TypeScript 装饰器支持使用了正确的装饰器语法MobX 版本支持装饰器2. makeAutoObservable 推断错误// 如果推断错误,使用 makeObservable 显式声明class Store { data = []; constructor() { makeAutoObservable(this, { data: observable.shallow // 显式声明 }); }}3. TypeScript 类型错误// 使用泛型参数class Store { count = 0; constructor() { makeObservable<Store>(this, { count: observable }); }}总结在 MobX 6 中,推荐使用 makeAutoObservable 进行快速开发,使用 makeObservable 进行精确控制。装饰器仍然可用,但不再是必需的。选择哪种方式取决于项目需求和个人偏好。
阅读 0·2月21日 15:50

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

MobX 提供了三种主要的 reaction 类型:autorun、reaction 和 when。它们各有不同的使用场景和特点,理解它们的区别对于正确使用 MobX 至关重要。1. autorun基本用法import { autorun } from 'mobx';const store = observable({ count: 0});autorun(() => { console.log(`Count is: ${store.count}`);});store.count++; // 输出: Count is: 1store.count++; // 输出: Count is: 2特点立即执行:autorun 会在创建时立即执行一次自动追踪:自动追踪函数中访问的所有 observable自动重新执行:当依赖的 observable 变化时自动重新执行无返回值:不能返回值,主要用于副作用适用场景日志记录数据持久化同步状态到 localStorage发送分析数据示例:日志记录autorun(() => { console.log('State changed:', toJS(store));});示例:持久化到 localStorageautorun(() => { localStorage.setItem('appState', JSON.stringify(toJS(store)));});2. reaction基本用法import { reaction } from 'mobx';const store = observable({ count: 0, name: 'John'});reaction( () => store.count, // 追踪函数 (count, reaction) => { // 效果函数 console.log(`Count changed to: ${count}`); }, { fireImmediately: false } // 配置选项);store.count++; // 输出: Count changed to: 1store.name = 'Jane'; // 不会触发,因为只追踪 count特点延迟执行:默认情况下不会立即执行精确控制:可以精确指定要追踪的 observable比较变化:可以比较新旧值可配置:提供多种配置选项配置选项reaction( () => store.count, (count, prevCount, reaction) => { console.log(`Count changed from ${prevCount} to ${count}`); }, { fireImmediately: true, // 立即执行 delay: 100, // 延迟执行 equals: (a, b) => a === b, // 自定义比较函数 name: 'myReaction' // 调试名称 });适用场景需要精确控制追踪范围需要比较新旧值需要延迟执行复杂的副作用逻辑示例:搜索防抖reaction( () => store.searchQuery, (query) => { // 延迟 300ms 执行搜索 debounce(() => { performSearch(query); }, 300); }, { delay: 300 });示例:比较新旧值reaction( () => store.items, (items, prevItems) => { const added = items.filter(item => !prevItems.includes(item)); const removed = prevItems.filter(item => !items.includes(item)); console.log('Added:', added); console.log('Removed:', removed); }, { equals: comparer.structural } // 深度比较);3. when基本用法import { when } from 'mobx';const store = observable({ loaded: false, data: null});when( () => store.loaded, // 条件函数 () => { // 效果函数 console.log('Data loaded:', store.data); });store.loaded = true;store.data = { name: 'John' }; // 输出: Data loaded: { name: 'John' }特点一次性执行:条件满足后只执行一次自动清理:执行后自动清理可取消:可以手动取消返回 disposer:返回一个清理函数适用场景等待某个条件满足后执行操作初始化逻辑一次性副作用示例:等待数据加载when( () => store.isLoaded, () => { initializeApp(); });示例:可取消的 whenconst dispose = when( () => store.userLoggedIn, () => { showWelcomeMessage(); });// 如果需要取消dispose();示例:超时处理const dispose = when( () => store.dataLoaded, () => { console.log('Data loaded successfully'); });// 5秒后取消setTimeout(() => { dispose(); console.log('Loading timeout');}, 5000);三者的对比| 特性 | autorun | reaction | when ||------|---------|----------|------|| 执行时机 | 立即执行 | 延迟执行(默认) | 条件满足时执行 || 执行次数 | 多次 | 多次 | 一次 || 追踪范围 | 自动追踪所有依赖 | 精确指定追踪范围 | 只追踪条件 || 返回值 | 无 | disposer | disposer || 适用场景 | 日志、持久化 | 复杂副作用、比较新旧值 | 初始化、一次性操作 |选择指南使用 autorun 当:需要立即执行需要追踪所有依赖用于简单的副作用不需要比较新旧值autorun(() => { document.title = store.pageTitle;});使用 reaction 当:需要精确控制追踪范围需要比较新旧值需要延迟执行需要复杂的副作用逻辑reaction( () => store.userId, (userId, prevUserId) => { if (userId !== prevUserId) { loadUserData(userId); } });使用 when 当:需要等待某个条件只需要执行一次用于初始化逻辑需要可取消的操作when( () => store.initialized, () => { startApp(); });性能考虑1. 避免过度追踪// 不好的做法:autorun 追踪太多autorun(() => { console.log(store.user.name, store.user.email, store.user.age);});// 好的做法:reaction 精确追踪reaction( () => store.user.name, (name) => { console.log(name); });2. 及时清理// 在组件卸载时清理useEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => { dispose(); // 清理 reaction };}, []);3. 使用 comparer 优化比较import { comparer } from 'mobx';reaction( () => store.items, (items) => { console.log('Items changed'); }, { equals: comparer.structural } // 深度比较,避免不必要的更新);常见陷阱1. 在 reaction 中产生副作用// 不好的做法:在追踪函数中产生副作用reaction( () => { console.log('Side effect!'); // 不应该在追踪函数中 return store.count; }, (count) => { console.log(count); });// 好的做法:追踪函数应该是纯函数reaction( () => store.count, (count) => { console.log('Side effect:', count); // 副作用在效果函数中 });2. 忘记清理 reaction// 不好的做法:忘记清理useEffect(() => { autorun(() => { console.log(store.count); }); // 没有清理函数}, []);// 好的做法:清理 reactionuseEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => dispose();}, []);3. 滥用 autorun// 不好的做法:使用 autorun 处理一次性操作autorun(() => { if (store.initialized) { initializeApp(); // 会多次执行 }});// 好的做法:使用 when 处理一次性操作when( () => store.initialized, () => { initializeApp(); // 只执行一次 });总结理解 autorun、reaction 和 when 的区别和使用场景是掌握 MobX 的关键:autorun:用于简单的、需要立即执行的副作用reaction:用于需要精确控制、比较新旧值的复杂副作用when:用于等待条件满足后执行的一次性操作正确选择和使用这些 reaction 类型,可以构建更高效、更可维护的 MobX 应用。
阅读 0·2月21日 15:50

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

MobX 和 Redux 是两种流行的状态管理库,它们在设计理念和使用方式上有显著差异:架构设计Redux:采用单向数据流架构遵循严格的不可变性原则使用纯函数(reducers)来处理状态更新状态是只读的,只能通过 dispatch action 来修改需要手动选择需要的状态(通过 useSelector)MobX:采用响应式编程架构允许可变状态,但通过 observable 进行追踪可以直接修改状态(在 action 中)自动追踪依赖关系,自动更新相关组件无需手动选择状态,组件自动订阅所需数据代码量和复杂度Redux:需要编写大量的样板代码(actions、action creators、reducers)需要配置 store、middleware、reducers代码结构相对复杂,学习曲线陡峭MobX:代码量少,简洁直观最小化配置,开箱即用学习曲线平缓,容易上手性能Redux:通过 shallowEqual 进行浅比较来决定是否重新渲染需要开发者手动优化性能(如使用 reselect)对于大型应用,可能需要额外的优化策略MobX:细粒度的依赖追踪,只更新真正需要更新的组件自动缓存计算属性,避免不必要的计算性能优化是自动的,开发者无需过多关注TypeScript 支持Redux:需要为 actions、reducers、state 等定义类型类型定义相对复杂,但类型安全性高需要使用类型断言或类型守卫MobX:类型推断更自然,类型定义更简单与 TypeScript 集成更流畅可以充分利用 TypeScript 的类型推断能力调试和可预测性Redux:状态变化完全可预测,易于调试Redux DevTools 提供强大的时间旅行调试功能所有的状态变化都通过 action 记录MobX:调试相对复杂,因为状态可以在多处修改MobX DevTools 提供了调试支持,但不如 Redux 强大需要遵循最佳实践(如使用 action)来提高可预测性适用场景选择 Redux:需要严格的状态管理规范团队规模大,需要明确的代码结构需要时间旅行调试状态变化逻辑复杂,需要中间件支持选择 MobX:追求开发效率和代码简洁性项目规模中小型需要快速原型开发团队对函数式响应式编程更熟悉总结Redux 更适合需要严格架构和可预测性的大型项目,而 MobX 更适合追求开发效率和简洁性的项目。选择哪种库应该根据项目需求、团队经验和长期维护考虑来决定。
阅读 0·2月21日 15:50

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

MobX 是一个基于函数式响应式编程(FRP)的状态管理库,它通过透明地应用响应式编程范式,使状态管理变得简单和可扩展。MobX 的核心理念是"任何源自状态的内容都应该自动派生",这意味着当状态发生变化时,所有依赖于该状态的派生值(如计算属性、反应等)会自动更新。MobX 的核心概念包括:Observable(可观察对象):使用 observable、observable.object、observable.array 等方法创建可观察的状态。当这些状态发生变化时,MobX 会自动追踪并通知相关的观察者。Computed(计算属性):使用 computed 创建派生值,这些值会根据其依赖的可观察状态自动计算和缓存。只有当依赖项发生变化时才会重新计算,具有高效的缓存机制。Actions(动作):使用 action 或 action.bound 来修改状态。在 MobX 6 中,所有状态修改都必须在 action 中进行,这有助于追踪状态变化并确保可预测性。Reactions(反应):包括 autorun、reaction 和 when,用于在状态变化时自动执行副作用。autorun 会立即执行并在依赖变化时重新运行;reaction 提供了更细粒度的控制,可以指定追踪函数和效果函数;when 会在条件满足时执行一次。Observer(观察者):在 React 组件中使用 observer 高阶组件或 useObserver hook,使组件能够响应状态变化并自动重新渲染。MobX 的工作原理基于依赖追踪系统。当可观察对象被访问时,MobX 会建立依赖关系;当可观察对象被修改时,MobX 会通知所有依赖它的派生值和反应,触发相应的更新。这种机制使得 MobX 能够高效地管理状态,避免了手动触发更新的繁琐过程。与 Redux 等其他状态管理库相比,MobX 的优势在于:更少的样板代码更直观的状态管理方式自动化的依赖追踪更好的性能(通过细粒度的更新)更容易与 TypeScript 集成MobX 适用于各种规模的应用,特别是那些需要复杂状态管理和响应式更新的场景。
阅读 0·2月21日 15:49

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

MobX 本身已经是一个高性能的状态管理库,但在实际应用中,仍然有一些优化技巧可以进一步提升性能。以下是 MobX 性能优化的最佳实践:1. 合理使用 computedcomputed 的缓存机制computed 属性会自动缓存结果,只在依赖项变化时重新计算:class Store { @observable firstName = 'John'; @observable lastName = 'Doe'; @observable age = 30; @computed get fullName() { console.log('Computing fullName'); return `${this.firstName} ${this.lastName}`; } @computed get info() { console.log('Computing info'); return `${this.fullName}, ${this.age} years old`; }}// 第一次访问会计算console.log(store.info); // Computing fullName, Computing info// 再次访问,使用缓存console.log(store.info); // 无输出// 修改 age,只重新计算 infostore.age = 31;console.log(store.info); // Computing info避免在 computed 中产生副作用// 错误:在 computed 中产生副作用@computed get badComputed() { console.log('Side effect!'); // 不应该在 computed 中 fetch('/api/data'); // 不应该在 computed 中 return this.data;}// 正确:computed 应该是纯函数@computed get goodComputed() { return this.data.filter(item => item.active);}2. 优化 observable 的使用只对需要追踪的状态使用 observable// 不好的做法:所有状态都是 observableclass Store { @observable config = { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 };}// 好的做法:只对会变化的状态使用 observableclass Store { config = { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 }; @observable data = []; @observable loading = false;}使用 shallow 或 deep 控制可观察深度import { observable, deep, shallow } from 'mobx';// 深度可观察(默认)const deepStore = observable({ user: { profile: { name: 'John' } }});// 浅层可观察const shallowStore = observable.shallow({ users: [ { name: 'John' }, { name: 'Jane' } ]});// 只有数组本身是可观察的,数组中的对象不是3. 批量更新状态使用 runInAction 批量更新// 不好的做法:多次触发更新@actionbadUpdate() { this.count++; this.name = 'New Name'; this.age++;}// 好的做法:批量更新@actiongoodUpdate() { runInAction(() => { this.count++; this.name = 'New Name'; this.age++; });}使用 transaction(MobX 4/5)import { transaction } from 'mobx';transaction(() => { store.count++; store.name = 'New Name'; store.age++;});4. 优化组件渲染使用 observer 只在需要的地方// 不好的做法:所有组件都用 observer@observerconst Header = () => <h1>My App</h1>;@observerconst Footer = () => <footer>© 2024</footer>;// 好的做法:只在需要响应状态变化的组件上使用 observerconst Header = () => <h1>My App</h1>;const Footer = () => <footer>© 2024</footer>;@observerconst Counter = () => <div>{store.count}</div>;拆分组件以减少依赖// 不好的做法:组件依赖太多状态@observerconst BadComponent = () => { return ( <div> <div>{store.user.name}</div> <div>{store.user.email}</div> <div>{store.settings.theme}</div> <div>{store.settings.language}</div> <div>{store.data.length}</div> </div> );};// 好的做法:拆分为多个组件@observerconst UserInfo = () => { return ( <div> <div>{store.user.name}</div> <div>{store.user.email}</div> </div> );};@observerconst Settings = () => { return ( <div> <div>{store.settings.theme}</div> <div>{store.settings.language}</div> </div> );};@observerconst DataCount = () => { return <div>{store.data.length}</div>;};使用 React.memo 配合 observerconst PureComponent = React.memo(observer(() => { return <div>{store.count}</div>;}));5. 避免在 render 中创建新对象// 不好的做法:每次渲染都创建新对象@observerconst BadComponent = () => { const style = { color: 'red' }; const handleClick = () => console.log('clicked'); return <div style={style} onClick={handleClick}>{store.count}</div>;};// 好的做法:在组件外部定义const style = { color: 'red' };const handleClick = () => console.log('clicked');@observerconst GoodComponent = () => { return <div style={style} onClick={handleClick}>{store.count}</div>;};6. 使用 trace 调试性能问题import { trace } from 'mobx';// 追踪 computed 的依赖trace(store.fullName);// 追踪 reaction 的依赖autorun(() => { console.log(store.count);}, { name: 'myReaction' });// 追踪组件的渲染@observerclass MyComponent extends React.Component { render() { trace(true); // 追踪组件渲染 return <div>{store.count}</div>; }}7. 使用 configure 优化配置import { configure } from 'mobx';configure({ // 强制所有状态修改都在 action 中 enforceActions: 'always', // 使用 Proxy(如果可用) useProxies: 'ifavailable', // computed 需要 reaction 才能计算 computedRequiresReaction: false, // 禁用不需要的警告 isolateGlobalState: true});8. 优化数组操作使用 splice 而不是重新赋值// 不好的做法:重新赋值整个数组@actionbadAddItem(item) { this.items = [...this.items, item];}// 好的做法:使用 splice@actiongoodAddItem(item) { this.items.push(item);}使用 replace 批量替换@actionreplaceItems(newItems) { this.items.replace(newItems);}9. 使用 reaction 替代 autorun// 不好的做法:autorun 会立即执行autorun(() => { console.log(store.count);});// 好的做法:reaction 提供更细粒度的控制reaction( () => store.count, (count) => { console.log(count); }, { fireImmediately: false });10. 使用 when 处理一次性条件// 不好的做法:使用 autorun 处理一次性条件autorun(() => { if (store.data.length > 0) { processData(store.data); }});// 好的做法:使用 whenwhen( () => store.data.length > 0, () => processData(store.data));11. 避免循环依赖// 不好的做法:循环依赖class StoreA { @observable value = 0; @computed get doubled() { return storeB.value * 2; }}class StoreB { @observable value = 0; @computed get doubled() { return storeA.value * 2; }}// 好的做法:避免循环依赖class Store { @observable valueA = 0; @observable valueB = 0; @computed get doubledA() { return this.valueA * 2; } @computed get doubledB() { return this.valueB * 2; }}12. 清理不需要的 reaction// 在组件卸载时清理 reactionuseEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => { dispose(); // 清理 reaction };}, []);13. 使用 MobX DevTools 分析性能MobX DevTools 提供了强大的性能分析功能:查看依赖关系图监控状态变化分析渲染性能调试 computed 和 reaction14. 避免过度追踪// 不好的做法:在循环中访问 observable@observerconst BadComponent = () => { return ( <div> {store.items.map(item => ( <div key={item.id}> {item.name} - {item.value} </div> ))} </div> );};// 好的做法:使用 computed 预处理数据class Store { @observable items = []; @computed get itemDisplayData() { return this.items.map(item => ({ id: item.id, display: `${item.name} - ${item.value}` })); }}@observerconst GoodComponent = () => { return ( <div> {store.itemDisplayData.map(item => ( <div key={item.id}>{item.display}</div> ))} </div> );};15. 使用 makeAutoObservable 简化代码// MobX 6 推荐使用 makeAutoObservableclass Store { count = 0; firstName = 'John'; lastName = 'Doe'; constructor() { makeAutoObservable(this); } get fullName() { return `${this.firstName} ${this.lastName}`; } increment() { this.count++; }}总结MobX 性能优化的关键点:合理使用 computed 的缓存机制只对需要追踪的状态使用 observable批量更新状态减少触发次数优化组件渲染,减少不必要的重新渲染避免在 render 中创建新对象使用 trace 调试性能问题清理不需要的 reaction避免循环依赖和过度追踪遵循这些最佳实践,可以构建高性能的 MobX 应用。
阅读 0·2月21日 15:49

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 代码,并避免常见的性能问题。
阅读 0·2月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 应用。
阅读 0·2月21日 15:45

OffscreenCanvas 如何在 Web Worker 中进行渲染?

OffscreenCanvas 是 HTML5 提供的一个功能,允许在 Web Worker 中进行 Canvas 渲染,从而将复杂的图形计算从主线程移到后台线程。OffscreenCanvas 的核心概念特点可以在 Worker 中进行 Canvas 绘图操作支持大部分 Canvas 2D API 和 WebGL API通过 transferControlToOffscreen() 方法将 Canvas 控制权转移适用于复杂的图形渲染和动画基本使用主线程设置// 获取 Canvas 元素const canvas = document.getElementById('myCanvas');// 将 Canvas 控制权转移到 OffscreenCanvasconst offscreen = canvas.transferControlToOffscreen();// 创建 Workerconst worker = new Worker('canvas-worker.js');// 将 OffscreenCanvas 发送给 Workerworker.postMessage({ canvas: offscreen }, [offscreen]);Worker 中渲染// canvas-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); // 在 Worker 中进行绘图 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制图形 ctx.fillStyle = 'blue'; ctx.fillRect(50, 50, 100, 100); // 继续动画 requestAnimationFrame(render); } render();};实际应用场景1. 复杂动画渲染// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('animation-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// animation-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); let particles = []; function initParticles() { for (let i = 0; i < 1000; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, size: Math.random() * 3 + 1 }); } } function updateParticles() { particles.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > canvas.width) p.vx *= -1; if (p.y < 0 || p.y > canvas.height) p.vy *= -1; }); } function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(100, 150, 255, 0.7)`; ctx.fill(); }); updateParticles(); requestAnimationFrame(render); } initParticles(); render();};2. 图像处理// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('image-worker.js');// 加载图像const img = new Image();img.onload = function() { worker.postMessage({ canvas: offscreen, image: img }, [offscreen]);};img.src = 'image.jpg';// image-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); const img = e.data.image; // 设置 Canvas 大小 canvas.width = img.width; canvas.height = img.height; // 绘制原始图像 ctx.drawImage(img, 0, 0); // 获取图像数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // 图像处理:灰度化 for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = avg; // R data[i + 1] = avg; // G data[i + 2] = avg; // B } // 放回处理后的图像 ctx.putImageData(imageData, 0, 0);};3. WebGL 渲染// 主线程const canvas = document.getElementById('glCanvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('webgl-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// webgl-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const gl = canvas.getContext('webgl'); // WebGL 初始化代码 const vertexShaderSource = ` attribute vec4 aVertexPosition; void main() { gl_Position = aVertexPosition; } `; const fragmentShaderSource = ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } `; // 编译着色器 const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建程序 const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // 渲染 function render() { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(shaderProgram); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(render); } render();};function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader;}与主线程交互动态调整 Canvas 大小// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 监听窗口大小变化window.addEventListener('resize', function() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; worker.postMessage({ type: 'resize', width: canvas.width, height: canvas.height });});// canvas-worker.jsself.onmessage = function(e) { if (e.data.type === 'resize') { canvas.width = e.data.width; canvas.height = e.data.height; }};接收用户输入// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 发送鼠标位置canvas.addEventListener('mousemove', function(e) { const rect = canvas.getBoundingClientRect(); worker.postMessage({ type: 'mousemove', x: e.clientX - rect.left, y: e.clientY - rect.top });});// canvas-worker.jslet mouseX = 0, mouseY = 0;self.onmessage = function(e) { if (e.data.type === 'mousemove') { mouseX = e.data.x; mouseY = e.data.y; }};注意事项1. Canvas 控制权只能转移一次// ❌ 错误:多次转移const offscreen1 = canvas.transferControlToOffscreen();const offscreen2 = canvas.transferControlToOffscreen(); // 报错// ✅ 正确:只转移一次const offscreen = canvas.transferControlToOffscreen();2. OffscreenCanvas 不支持所有 Canvas API// ❌ 不支持canvas.toDataURL(); // 在 Worker 中不可用canvas.toBlob(); // 在 Worker 中不可用// ✅ 使用 ImageBitmap 代替const bitmap = await createImageBitmap(canvas);3. 浏览器兼容性// 检查浏览器支持if ('transferControlToOffscreen' in HTMLCanvasElement.prototype) { // 支持 OffscreenCanvas} else { // 不支持,使用回退方案}性能优化1. 批量绘制// ❌ 频繁调用绘制方法for (let i = 0; i < 1000; i++) { ctx.beginPath(); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2); ctx.fill();}// ✅ 批量绘制ctx.beginPath();for (let i = 0; i < 1000; i++) { ctx.moveTo(particles[i].x, particles[i].y); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2);}ctx.fill();2. 使用 ImageBitmap// 加载图像为 ImageBitmapconst bitmap = await createImageBitmap(image);// 在 Worker 中绘制ctx.drawImage(bitmap, 0, 0);3. 降低渲染频率let lastRenderTime = 0;const targetFPS = 30;const frameInterval = 1000 / targetFPS;function render(timestamp) { if (timestamp - lastRenderTime >= frameInterval) { // 执行渲染 ctx.clearRect(0, 0, canvas.width, canvas.height); // ... 绘制代码 lastRenderTime = timestamp; } requestAnimationFrame(render);}最佳实践复杂渲染使用 OffscreenCanvas:将计算密集型图形渲染移到 Worker合理控制渲染频率:避免不必要的重绘批量处理:减少绘制调用次数使用 ImageBitmap:提高图像加载和渲染性能检查浏览器兼容性:提供回退方案及时释放资源:使用完毕后清理资源
阅读 0·2月21日 15:44

如何优化Expo应用的性能?有哪些常见的性能问题?

Expo应用的性能优化是确保良好用户体验的关键。Expo基于React Native,因此许多React Native的性能优化技巧同样适用,同时Expo也提供了一些特定的优化工具和策略。性能优化策略:组件渲染优化使用React.memo、useMemo和useCallback减少不必要的渲染:// 使用React.memo避免不必要的重新渲染const MyComponent = React.memo(({ data }) => { return <Text>{data.value}</Text>;});// 使用useMemo缓存计算结果function ExpensiveComponent({ items }) { const sortedItems = useMemo(() => { return items.sort((a, b) => a.id - b.id); }, [items]); return <FlatList data={sortedItems} />;}// 使用useCallback缓存函数function ParentComponent() { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return <ChildComponent onClick={handleClick} />;}列表优化使用FlatList而不是ScrollView处理长列表:<FlatList data={items} renderItem={({ item }) => <Item item={item} />} keyExtractor={(item) => item.id} removeClippedSubviews={true} maxToRenderPerBatch={10} windowSize={10} initialNumToRender={10} getItemLayout={(data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, })}/>关键属性说明:removeClippedSubviews:移除屏幕外的视图maxToRenderPerBatch:每批渲染的项目数windowSize:渲染窗口大小initialNumToRender:初始渲染项目数getItemLayout:提供项目布局信息图片优化使用expo-image和图片缓存策略:import { Image } from 'expo-image';<Image source={{ uri: 'https://example.com/image.jpg' }} style={{ width: 200, height: 200 }} cachePolicy="memory-disk" contentFit="cover" transition={200}/>优化技巧:使用适当的图片尺寸启用缓存策略使用WebP格式懒加载图片网络请求优化使用缓存和请求去重:import { useQuery } from '@tanstack/react-query';function UserProfile({ userId }) { const { data, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000, // 10分钟 }); if (isLoading) return <Loading />; return <Text>{data.name}</Text>;}动画优化使用react-native-reanimated进行高性能动画:import Animated, { useSharedValue, useAnimatedStyle, withTiming,} from 'react-native-reanimated';function AnimatedBox() { const opacity = useSharedValue(0); const animatedStyle = useAnimatedStyle(() => { return { opacity: withTiming(opacity.value, { duration: 500 }), }; }); useEffect(() => { opacity.value = 1; }, []); return <Animated.View style={animatedStyle} />;}内存管理及时释放资源:// 取消网络请求useEffect(() => { const controller = new AbortController(); fetchData(controller.signal); return () => controller.abort();}, []);// 清理定时器useEffect(() => { const timer = setInterval(() => { console.log('Tick'); }, 1000); return () => clearInterval(timer);}, []);// 清理订阅useEffect(() => { const subscription = someEvent.subscribe(); return () => subscription.unsubscribe();}, []);Bundle优化减少应用包大小:// 代码分割const LazyComponent = React.lazy(() => import('./LazyComponent'));// 动态导入const loadModule = async () => { const module = await import('./heavyModule'); module.doSomething();};// 移除未使用的依赖npm prune使用Expo的性能工具Expo提供了一些性能监控工具:import { Performance } from 'react-native';// 记录性能标记Performance.mark('component-start');// 组件渲染完成后Performance.mark('component-end');// 测量性能Performance.measure('component-render', 'component-start', 'component-end');性能分析工具:React DevTools Profiler分析组件渲染性能识别性能瓶颈优化渲染次数Flipper网络请求监控布局检查内存分析Expo DevTools实时性能监控Bundle大小分析加载时间追踪常见性能问题及解决方案:列表滚动卡顿使用FlatList而不是ScrollView启用removeClippedSubviews提供正确的getItemLayout图片加载慢使用expo-image启用缓存使用适当的图片尺寸应用启动慢优化初始化代码延迟加载非关键模块使用启动画面内存泄漏及时清理订阅和定时器使用WeakMap和WeakSet定期检查内存使用最佳实践:性能监控:定期使用性能工具分析应用渐进优化:先优化关键路径,再优化次要功能测试覆盖:在不同设备上测试性能持续优化:将性能优化作为持续过程用户体验:平衡性能和功能,优先保证核心体验通过系统性的性能优化,可以显著提升Expo应用的用户体验和竞争力。
阅读 0·2月21日 15:43