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

前端面试题手册

Lottie 动画开发中常见的问题和解决方案有哪些?

Lottie 动画开发中常见的问题和解决方案如下:1. 动画不显示问题原因:JSON 文件路径错误JSON 文件格式不正确容器元素没有设置宽高动画数据加载失败解决方案:// 检查 JSON 文件路径import animationData from './animation.json';// 设置容器宽高const container = document.getElementById('lottie-container');container.style.width = '300px';container.style.height = '300px';// 添加错误处理const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json', rendererSettings: { preserveAspectRatio: 'xMidYMid slice' }});animation.addEventListener('data_failed', (error) => { console.error('Animation data failed to load:', error); // 显示降级内容 container.innerHTML = '<img src="fallback.png" alt="Animation fallback">';});2. 动画卡顿或性能差问题原因:动画文件过大同时播放多个动画设备性能不足渲染器选择不当解决方案:// 使用 Canvas 渲染器(性能更好)const animation = lottie.loadAnimation({ container: container, renderer: 'canvas', // 使用 canvas 而不是 svg loop: true, autoplay: true, path: 'animation.json', rendererSettings: { preserveAspectRatio: 'xMidYMid slice', clearCanvas: false, progressiveLoad: true, hideOnTransparent: true }});// 降低帧率animation.setSpeed(0.5); // 降低播放速度// 懒加载动画const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const animation = lottie.loadAnimation({ container: entry.target, renderer: 'canvas', loop: true, autoplay: true, path: 'animation.json' }); observer.unobserve(entry.target); } });});// 在低端设备上禁用复杂动画if (navigator.hardwareConcurrency < 4) { // 使用简化版本或静态图片}3. 动画在不同平台表现不一致问题原因:不同平台的渲染引擎差异字体支持不一致缓动函数实现差异解决方案:// 使用标准的缓动函数const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json', rendererSettings: { preserveAspectRatio: 'xMidYMid meet' }});// 检测平台并调整const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);const isAndroid = /Android/.test(navigator.userAgent);if (isIOS) { // iOS 特定调整 animation.setSpeed(1.0);} else if (isAndroid) { // Android 特定调整 animation.setSpeed(0.9);}4. 内存泄漏问题原因:动画实例未正确销毁事件监听器未移除组件卸载时未清理资源解决方案:// React 中正确清理import { useEffect, useRef } from 'react';function MyComponent() { const animationRef = useRef(null); const containerRef = useRef(null); useEffect(() => { if (containerRef.current) { animationRef.current = lottie.loadAnimation({ container: containerRef.current, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json' }); } return () => { // 清理动画实例 if (animationRef.current) { animationRef.current.destroy(); animationRef.current = null; } }; }, []); return <div ref={containerRef}></div>;}// 移除所有事件监听器function cleanupAnimation(animation) { const events = ['complete', 'loopComplete', 'enterFrame', 'config_ready', 'data_ready', 'DOMLoaded', 'destroy']; events.forEach(event => { animation.removeEventListener(event); }); animation.destroy();}5. 动画加载慢问题原因:JSON 文件过大网络延迟未使用缓存解决方案:// 压缩 JSON 文件// 使用 LottieFiles 优化器:https://lottiefiles.com/tools/optimize// 使用 CDNconst animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'https://cdn.example.com/animation.json'});// 启用 Service Worker 缓存// 在 service-worker.js 中self.addEventListener('fetch', (event) => { if (event.request.url.includes('.json')) { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request).then((response) => { return caches.open('lottie-cache').then((cache) => { cache.put(event.request, response.clone()); return response; }); }); }) ); }});// 显示加载状态let isLoading = true;const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json'});animation.addEventListener('DOMLoaded', () => { isLoading = false; container.classList.remove('loading');});6. 动画颜色不正确问题原因:颜色格式不匹配动态颜色修改失败平台颜色渲染差异解决方案:// 使用正确的颜色格式(RGBA)animation.setColorFilter([ { keypath: 'layer1', color: 'rgba(255, 0, 0, 1)' }]);// 或者直接修改 JSON 数据function modifyAnimationColor(animationData, keypath, newColor) { const colorArray = hexToRgba(newColor); animationData.layers.forEach(layer => { if (layer.nm === keypath) { layer.shapes.forEach(shape => { if (shape.ty === 'fl') { shape.c.k = colorArray; } }); } }); return animationData;}function hexToRgba(hex) { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; return [r, g, b, 1];}7. 动画在列表中性能问题问题原因:同时渲染多个动画实例未使用虚拟化列表动画未正确卸载解决方案:// 使用虚拟化列表(React)import { FixedSizeList as List } from 'react-window';function Row({ index, style }) { const containerRef = useRef(null); const animationRef = useRef(null); useEffect(() => { if (containerRef.current) { animationRef.current = lottie.loadAnimation({ container: containerRef.current, renderer: 'canvas', loop: false, autoplay: false, path: animations[index].url }); } return () => { if (animationRef.current) { animationRef.current.destroy(); } }; }, [index]); return ( <div style={style}> <div ref={containerRef} style={{ width: 100, height: 100 }}></div> </div> );}<List height={600} itemCount={animations.length} itemSize={120} width={400}> {Row}</List>// 或者使用 Intersection Observerconst observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const animation = lottie.loadAnimation({ container: entry.target, renderer: 'canvas', loop: true, autoplay: true, path: entry.target.dataset.url }); } else { // 暂停不可见的动画 const animation = entry.target.lottieAnimation; if (animation) { animation.pause(); } } });}, { threshold: 0.1 });8. iOS 上动画不工作问题原因:未安装 CocoaPods 依赖iOS 版本过低内存限制解决方案:# 安装 CocoaPods 依赖cd ios && pod install# 检查 iOS 版本兼容性# Lottie iOS 支持的最低版本:iOS 9.0+# 在 Info.plist 中添加内存警告处理9. Android 上动画不工作问题原因:Gradle 配置问题权限问题内存限制解决方案:// 在 app/build.gradle 中添加android { defaultConfig { vectorDrawables.useSupportLibrary = true }}dependencies { implementation 'com.airbnb.android:lottie:6.0.0'}10. 动画循环问题问题原因:循环设置不正确动画结束事件未正确处理多个动画实例冲突解决方案:// 正确设置循环const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, // 启用循环 autoplay: true, path: 'animation.json'});// 或者手动控制循环animation.addEventListener('complete', () => { animation.goToAndPlay(0, true);});// 限制循环次数let loopCount = 0;const maxLoops = 3;animation.addEventListener('loopComplete', () => { loopCount++; if (loopCount >= maxLoops) { animation.loop = false; }});最佳实践总结:始终添加错误处理和降级方案在组件卸载时清理动画实例使用适当的渲染器(Canvas 用于性能,SVG 用于质量)实现懒加载和虚拟化列表压缩和优化动画文件使用 CDN 和缓存加速加载测试不同设备和平台的兼容性监控动画性能和内存使用
阅读 0·2月21日 15:52

Lottie 动画与其他动画技术(GIF、视频、CSS 动画等)相比有哪些区别和优势?

Lottie 动画与其他动画技术相比有明显的区别和优势,以下是详细的对比分析:1. Lottie vs GIF文件大小Lottie:JSON 文件通常只有几 KB 到几百 KB,压缩率极高GIF:文件体积较大,通常在几百 KB 到几 MB,且压缩率低渲染质量Lottie:矢量图形,任意缩放不失真,支持透明背景GIF:位图格式,缩放会失真,不支持真正的透明背景(只有 1 位透明)性能表现Lottie:使用原生渲染,支持硬件加速,流畅度高GIF:解码开销大,不支持硬件加速,容易造成卡顿交互控制Lottie:支持播放、暂停、进度控制、速度调节、反向播放等GIF:无法控制,只能循环播放动态修改Lottie:支持运行时修改颜色、文本、路径等属性GIF:无法修改适用场景Lottie:适合需要高质量、可交互、可缩放的动画场景GIF:适合简单的、不需要交互的动画场景2. Lottie vs PNG 序列帧文件大小Lottie:JSON 文件小,只存储动画数据PNG 序列帧:需要存储每一帧的完整图像,文件体积最大内存占用Lottie:内存占用低,只存储动画数据PNG 序列帧:需要加载所有图片到内存,内存占用最高加载速度Lottie:加载快,支持渐进式加载PNG 序列帧:加载最慢,需要下载所有帧性能表现Lottie:渲染流畅,性能优异PNG 序列帧:切换帧时有性能开销,容易卡顿响应式支持Lottie:矢量图形,任意缩放不失真PNG 序列帧:位图格式,缩放会失真适用场景Lottie:适合需要高性能、小体积的动画场景PNG 序列帧:适合需要精确控制每一帧的场景3. Lottie vs 视频(MP4/WebM)文件大小Lottie:JSON 文件小,压缩率高视频:体积中等,编码后仍有较大文件渲染性能Lottie:使用原生渲染,性能优异视频:解码开销中等,播放控制不灵活交互控制Lottie:支持播放、暂停、进度控制、速度调节等视频:基本控制,但交互性有限动态修改Lottie:支持运行时修改属性视频:无法修改加载速度Lottie:加载快,支持渐进式加载视频:支持流媒体加载,但初始加载仍需时间适用场景Lottie:适合需要交互控制、动态修改的动画场景视频:适合复杂的、需要高保真的视频内容4. Lottie vs CSS 动画开发复杂度Lottie:设计师在 After Effects 中创建,开发者直接使用 JSON 文件CSS 动画:需要开发者编写 CSS 代码,复杂动画实现困难动画能力Lottie:支持复杂的形状动画、路径变形、3D 变换等CSS 动画:支持基础变换,复杂动画实现困难跨平台一致性Lottie:跨平台渲染一致CSS 动画:不同浏览器可能有差异性能表现Lottie:使用原生渲染,性能优异CSS 动画:使用浏览器渲染引擎,性能良好动态修改Lottie:支持运行时修改颜色、文本等CSS 动画:可以通过 CSS 变量动态修改适用场景Lottie:适合复杂的、需要设计师参与的动画场景CSS 动画:适合简单的、由开发者实现的动画场景5. Lottie vs Canvas 动画开发效率Lottie:设计师创建,开发者直接使用,开发效率高Canvas 动画:需要开发者编写 JavaScript 代码,开发效率低动画质量Lottie:矢量图形,高质量,任意缩放Canvas 动画:位图渲染,缩放会失真性能表现Lottie:使用原生渲染,性能优异Canvas 动画:使用 Canvas API,性能良好但需要优化可维护性Lottie:JSON 文件易于管理和更新Canvas 动画:代码复杂,维护困难适用场景Lottie:适合需要高质量、易于维护的动画场景Canvas 动画:适合需要高度自定义、复杂交互的动画场景6. Lottie vs SVG 动画开发复杂度Lottie:设计师在 After Effects 中创建,自动导出SVG 动画:需要手动编写 SVG 代码或使用工具生成动画能力Lottie:支持复杂的形状动画、路径变形、3D 变换等SVG 动画:支持基础的形状和路径动画文件大小Lottie:JSON 文件小,压缩率高SVG 动画:SVG 文件相对较大跨平台支持Lottie:支持 iOS、Android、Web 等多个平台SVG 动画:主要支持 Web 平台适用场景Lottie:适合跨平台、复杂的动画场景SVG 动画:适合 Web 平台的简单动画场景7. Lottie vs 原生动画(iOS Core Animation / Android Animator)开发效率Lottie:设计师创建,开发者直接使用,开发效率高原生动画:需要开发者编写原生代码,开发效率低跨平台一致性Lottie:跨平台渲染一致原生动画:不同平台需要分别实现动画能力Lottie:支持复杂的形状动画、路径变形等原生动画:支持基础变换,复杂动画实现困难性能表现Lottie:使用原生渲染,性能优异原生动画:直接使用平台 API,性能最佳适用场景Lottie:适合需要跨平台、快速开发的动画场景原生动画:适合需要最佳性能、高度定制的动画场景8. Lottie vs FLIP 动画开发复杂度Lottie:设计师创建,开发者直接使用FLIP 动画:需要开发者计算元素位置和状态,复杂度高动画类型Lottie:适合预定义的、独立的动画FLIP 动画:适合布局变化、元素移动等过渡动画性能表现Lottie:使用原生渲染,性能优异FLIP 动画:使用 transform 和 opacity,性能良好适用场景Lottie:适合独立的、预定义的动画场景FLIP 动画:适合布局变化、元素移动等过渡场景9. Lottie vs Three.js / WebGL 动画动画类型Lottie:适合 2D 矢量动画Three.js / WebGL:适合 3D 动画和复杂视觉效果性能表现Lottie:使用原生渲染,性能优异Three.js / WebGL:使用 GPU 加速,性能强大但需要优化开发复杂度Lottie:设计师创建,开发者直接使用Three.js / WebGL:需要开发者编写复杂的 3D 代码适用场景Lottie:适合 2D 矢量动画场景Three.js / WebGL:适合 3D 动画和复杂视觉效果场景10. 选择建议选择 Lottie 的场景:需要跨平台一致的动画需要高质量的矢量动画需要交互控制和动态修改需要小文件体积需要快速开发和迭代选择其他技术的场景:GIF:简单的、不需要交互的动画PNG 序列帧:需要精确控制每一帧的场景视频:复杂的、需要高保真的视频内容CSS 动画:简单的、由开发者实现的动画Canvas 动画:需要高度自定义、复杂交互的动画SVG 动画:Web 平台的简单动画原生动画:需要最佳性能、高度定制的动画FLIP 动画:布局变化、元素移动等过渡动画Three.js / WebGL:3D 动画和复杂视觉效果总结:Lottie 动画在文件大小、渲染质量、性能表现、交互控制等方面具有明显优势,特别适合需要跨平台、高质量、可交互的动画场景。但在某些特定场景下,其他动画技术可能更适合。选择时需要根据具体需求、性能要求、开发资源等因素综合考虑。
阅读 0·2月21日 15:52

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

MobX 6 是 MobX 的一个重大版本更新,引入了许多重要的变化和新特性。以下是 MobX 6 的主要变化:1. 强制使用 ActionMobX 5 及之前class Store { @observable count = 0; increment() { this.count++; // 可以直接修改 }}MobX 6class Store { @observable count = 0; @action // 必须使用 action increment() { this.count++; }}在 MobX 6 中,所有状态修改都必须在 action 中进行。这是为了提供更好的可预测性和调试体验。2. 装饰器 API 的变化MobX 5import { observable, computed, action } from 'mobx';class Store { @observable count = 0; @computed get doubled() { return this.count * 2; } @action increment() { this.count++; }}MobX 6import { makeObservable, observable, computed, action } from 'mobx';class Store { count = 0; constructor() { makeObservable(this); } get doubled() { return this.count * 2; } increment() { this.count++; }}MobX 6 推荐使用 makeObservable 而不是装饰器,但装饰器仍然支持。3. makeAutoObservable 的引入MobX 6 引入了 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++; }}makeAutoObservable 会自动:将 getter 标记为 computed将方法标记为 action将字段标记为 observable4. 配置 API 的简化MobX 5import { configure } from 'mobx';configure({ enforceActions: 'always', useProxies: 'always', computedRequiresReaction: true});MobX 6import { configure } from 'mobx';configure({ enforceActions: 'always', // 默认值 useProxies: 'ifavailable', // 默认值 computedRequiresReaction: false // 默认值});MobX 6 的默认配置更加严格和合理。5. Proxy 的强制使用MobX 6 强制使用 Proxy(在支持的浏览器中),这提供了更好的性能和更简单的 API。Proxy 的优势更好的性能更简单的 API更好的 TypeScript 支持更少的限制不支持 Proxy 的环境在旧浏览器或 Node.js 环境中,MobX 6 会自动降级到兼容模式。6. 移除的 API以下 API 在 MobX 6 中被移除:isObservableObject → 使用 isObservableintercept → 使用 observe 的拦截功能extras API → 大部分功能已集成到主 APItoJS → 使用 toJSON 或手动转换7. TypeScript 支持的改进MobX 6 对 TypeScript 的支持更加完善:import { 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++; }}类型推断更加准确,类型定义更加简洁。8. 性能优化MobX 6 引入了多项性能优化:更快的依赖追踪更高效的 computed 缓存更好的批量更新机制减少的内存占用9. 调试体验的改进MobX 6 提供了更好的调试工具:更清晰的错误消息更好的堆栈跟踪改进的 DevTools 支持10. 迁移指南从 MobX 5 迁移到 MobX 6更新依赖npm install mobx@6 mobx-react@6添加 action 装饰器// 之前increment() { this.count++;}// 之后@actionincrement() { this.count++;}使用 makeObservable 或 makeAutoObservableconstructor() { makeAutoObservable(this);}更新配置configure({ enforceActions: 'always'});移除已废弃的 API替换 toJS 为 toJSON更新 isObservableObject 为 isObservable总结MobX 6 是一个重要的版本更新,主要改进包括:强制使用 action 提高可预测性简化的 API 和更好的 TypeScript 支持强制使用 Proxy 提高性能更好的调试体验移除已废弃的 API迁移到 MobX 6 需要一些工作,但带来的改进是值得的。建议新项目直接使用 MobX 6,现有项目逐步迁移。
阅读 0·2月21日 15:51

MobX 常见问题和解决方案有哪些?

MobX 是一个功能强大的状态管理库,但在使用过程中可能会遇到一些常见问题。了解这些问题及其解决方案可以帮助开发者更好地使用 MobX。1. 组件不更新问题描述组件使用 observer 包装,但状态变化时组件不更新。常见原因和解决方案原因 1:访问的是普通对象而不是 observable// 错误const store = { count: 0};@observerclass Counter extends React.Component { render() { return <div>{store.count}</div>; // 不会更新 }}// 正确import { observable } from 'mobx';const store = observable({ count: 0});@observerclass Counter extends React.Component { render() { return <div>{store.count}</div>; // 会更新 }}原因 2:在 action 外修改状态(MobX 6)// 错误(MobX 6)class Store { @observable count = 0; increment() { this.count++; // 不在 action 中,会报错 }}// 正确class Store { @observable count = 0; @action increment() { this.count++; // 在 action 中 }}原因 3:在 render 中创建新对象// 错误@observerclass Component extends React.Component { render() { const style = { color: 'red' }; // 每次渲染都创建新对象 return <div style={style}>{store.count}</div>; }}// 正确const style = { color: 'red' }; // 在组件外部定义@observerclass Component extends React.Component { render() { return <div style={style}>{store.count}</div>; }}2. 性能问题问题描述应用性能下降,组件频繁重新渲染。常见原因和解决方案原因 1:过度追踪// 错误:在循环中访问 observable@observerclass List extends React.Component { render() { return ( <div> {store.items.map(item => ( <div key={item.id}> {item.name} - {item.value} - {item.description} </div> ))} </div> ); }}// 正确:使用 computed 预处理数据class Store { @observable items = []; @computed get itemDisplayData() { return this.items.map(item => ({ id: item.id, display: `${item.name} - ${item.value} - ${item.description}` })); }}@observerclass List extends React.Component { render() { return ( <div> {store.itemDisplayData.map(item => ( <div key={item.id}>{item.display}</div> ))} </div> ); }}原因 2:组件依赖太多状态// 错误:组件依赖太多状态@observerclass Dashboard extends React.Component { render() { return ( <div> <UserInfo /> <Settings /> <DataCount /> </div> ); }}// 正确:拆分为多个组件@observerclass UserInfo extends React.Component { render() { return ( <div> <div>{store.user.name}</div> <div>{store.user.email}</div> </div> ); }}3. 内存泄漏问题描述组件卸载后,reaction 仍然在运行,导致内存泄漏。解决方案// 错误:忘记清理 reactionuseEffect(() => { autorun(() => { console.log(store.count); });}, []);// 正确:清理 reactionuseEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => { dispose(); // 清理 reaction };}, []);4. 异步操作问题问题描述异步操作中的状态修改不生效。解决方案// 错误:异步操作中的状态修改@actionasync fetchData() { this.loading = true; const data = await fetch('/api/data').then(r => r.json()); this.data = data; // 不在 action 中 this.loading = false; // 不在 action 中}// 正确:使用 runInAction@actionasync fetchData() { this.loading = true; try { const data = await fetch('/api/data').then(r => r.json()); runInAction(() => { this.data = data; }); } finally { runInAction(() => { this.loading = false; }); }}// 或者使用 flowfetchData = flow(function* () { this.loading = true; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } finally { this.loading = false; }});5. computed 不更新问题描述computed 属性没有按预期更新。常见原因和解决方案原因 1:在 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:依赖项没有正确追踪// 错误:依赖项没有正确追踪@computed get badComputed() { const data = this.data; // 没有在返回值中使用 return this.items.length;}// 正确:依赖项正确追踪@computed get goodComputed() { return this.data.length + this.items.length;}6. 循环依赖问题描述多个 store 之间存在循环依赖,导致无限循环或性能问题。解决方案// 错误:循环依赖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; }}7. 装饰器不工作问题描述使用装饰器时出现错误或不生效。解决方案确保配置正确// package.json{ "babel": { "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose": true }] ] }}或者使用 makeObservable// 不使用装饰器class Store { count = 0; constructor() { makeObservable(this, { count: observable }); }}8. TypeScript 类型错误问题描述使用 TypeScript 时出现类型错误。解决方案// 错误:没有类型参数class Store { count = 0; constructor() { makeObservable(this, { count: observable }); }}// 正确:使用类型参数class Store { count: number = 0; constructor() { makeObservable<Store>(this, { count: observable }); }}9. 数组操作问题问题描述数组操作不触发更新。解决方案// 错误:重新赋值整个数组@actionbadAddItem(item) { this.items = [...this.items, item];}// 正确:使用数组方法@actiongoodAddItem(item) { this.items.push(item);}// 或者使用 replace@actionreplaceItems(newItems) { this.items.replace(newItems);}10. 调试困难问题描述难以追踪状态变化和依赖关系。解决方案使用 traceimport { trace } from 'mobx';// 追踪 computedtrace(store.fullName);// 追踪组件渲染@observerclass MyComponent extends React.Component { render() { trace(true); // 追踪组件渲染 return <div>{store.count}</div>; }}使用 MobX DevToolsimport { configure } from 'mobx';configure({ // 启用调试模式 useProxies: 'ifavailable', isolateGlobalState: true});最佳实践总结始终在 action 中修改状态(MobX 6)使用 observer 包装需要响应状态变化的组件避免在 render 中创建新对象使用 computed 优化计算逻辑及时清理 reaction 和副作用正确处理异步操作避免循环依赖使用 trace 和 DevTools 调试合理拆分组件以减少依赖遵循 MobX 的最佳实践遵循这些最佳实践,可以避免大多数常见的 MobX 问题,构建稳定、高效的应用。
阅读 0·2月21日 15:50

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