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

MobX 中 computed 的作用和使用场景有哪些?

2月22日 14:05

在 MobX 中,computed 是基于 observable 状态自动更新的派生值,类似于 Vue 的 computed 属性。computed 值会缓存计算结果,只有当依赖的 observable 状态发生变化时才会重新计算。

Computed 的特性

  1. 自动缓存:计算结果会被缓存,避免重复计算
  2. 懒计算:只有在被访问时才会计算
  3. 自动追踪依赖:自动追踪依赖的 observable 状态
  4. 自动更新:依赖的状态变化时自动重新计算
  5. 纯函数:computed 应该是纯函数,不应该有副作用

Computed 的使用方式

1. 使用装饰器

javascript
import { observable, computed } from 'mobx'; class TodoStore { @observable todos = []; @observable filter = 'all'; @computed get completedTodos() { return this.todos.filter(todo => todo.completed); } @computed get activeTodos() { return this.todos.filter(todo => !todo.completed); } @computed get filteredTodos() { switch (this.filter) { case 'completed': return this.completedTodos; case 'active': return this.activeTodos; default: return this.todos; } } }

2. 使用 makeObservable

javascript
import { makeObservable, observable, computed } from 'mobx'; class TodoStore { todos = []; filter = 'all'; constructor() { makeObservable(this, { todos: observable, filter: observable, completedTodos: computed, activeTodos: computed, filteredTodos: computed }); } get completedTodos() { return this.todos.filter(todo => todo.completed); } get activeTodos() { return this.todos.filter(todo => !todo.completed); } get filteredTodos() { switch (this.filter) { case 'completed': return this.completedTodos; case 'active': return this.activeTodos; default: return this.todos; } } }

3. 使用 makeAutoObservable(推荐)

javascript
import { makeAutoObservable } from 'mobx'; class TodoStore { todos = []; filter = 'all'; constructor() { makeAutoObservable(this); } get completedTodos() { return this.todos.filter(todo => todo.completed); } get activeTodos() { return this.todos.filter(todo => !todo.completed); } get filteredTodos() { switch (this.filter) { case 'completed': return this.completedTodos; case 'active': return this.activeTodos; default: return this.todos; } } }

4. 使用 computed 函数

javascript
import { observable, computed } from 'mobx'; const store = observable({ todos: [], filter: 'all' }); const completedTodos = computed(() => store.todos.filter(todo => todo.completed) ); const activeTodos = computed(() => store.todos.filter(todo => !todo.completed) ); console.log(completedTodos.get()); // 获取计算值

Computed 的配置选项

1. name

为 computed 设置名称,便于调试。

javascript
@computed({ name: 'completedTodos' }) get completedTodos() { return this.todos.filter(todo => todo.completed); }

2. equals

自定义比较函数,决定是否需要重新计算。

javascript
@computed({ equals: (a, b) => a.length === b.length && a.every(item => b.includes(item)) }) get filteredTodos() { return this.todos.filter(todo => todo.completed); }

3. keepAlive

保持计算值活跃,即使没有被访问。

javascript
@computed({ keepAlive: true }) get expensiveComputation() { return this.largeArray.reduce((sum, item) => sum + item.value, 0); }

Computed 的最佳实践

1. 使用 computed 缓存复杂计算

javascript
class Store { @observable items = []; @computed get totalValue() { return this.items.reduce((sum, item) => sum + item.value, 0); } @computed get averageValue() { return this.items.length > 0 ? this.totalValue / this.items.length : 0; } }

2. 避免在 computed 中产生副作用

javascript
// ❌ 错误:computed 中有副作用 @computed get filteredItems() { console.log('Filtering items'); // 副作用 return this.items.filter(item => item.active); } // ✅ 正确:使用 reaction 处理副作用 @computed get filteredItems() { return this.items.filter(item => item.active); } constructor() { reaction( () => this.filteredItems, (items) => console.log('Filtered items:', items) ); }

3. 合理使用 computed 链

javascript
class Store { @observable products = []; @observable category = 'all'; @observable minPrice = 0; @observable maxPrice = Infinity; @computed get filteredByCategory() { return this.category === 'all' ? this.products : this.products.filter(p => p.category === this.category); } @computed get filteredByPrice() { return this.filteredByCategory.filter( p => p.price >= this.minPrice && p.price <= this.maxPrice ); } @computed get sortedProducts() { return [...this.filteredByPrice].sort((a, b) => a.price - b.price); } }

4. 使用 computed 处理表单验证

javascript
class FormStore { @observable username = ''; @observable email = ''; @observable password = ''; @computed get usernameError() { if (this.username.length < 3) { return 'Username must be at least 3 characters'; } return ''; } @computed get emailError() { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(this.email)) { return 'Invalid email format'; } return ''; } @computed get passwordError() { if (this.password.length < 8) { return 'Password must be at least 8 characters'; } return ''; } @computed get isValid() { return !this.usernameError && !this.emailError && !this.passwordError; } }

Computed vs Reaction

特性ComputedReaction
用途计算派生值执行副作用
返回值返回计算结果不返回值
缓存自动缓存不缓存
触发时机被访问时依赖变化时立即执行
适用场景数据转换、过滤、聚合日志、API 调用、DOM 操作

性能优化

  1. 合理使用 computed:避免过度使用,只在需要缓存时使用
  2. 避免复杂计算:如果计算非常耗时,考虑使用 memoization
  3. 使用 computed 链:将复杂计算拆分为多个小的 computed
  4. 避免在 computed 中创建新对象:可能导致不必要的重新计算

常见错误

1. 在 computed 中修改状态

javascript
// ❌ 错误:computed 中修改状态 @computed get filteredItems() { this.lastFilterTime = Date.now(); // 修改状态 return this.items.filter(item => item.active); } // ✅ 正确:使用 reaction 处理副作用 @computed get filteredItems() { return this.items.filter(item => item.active); } constructor() { reaction( () => this.filteredItems, () => { this.lastFilterTime = Date.now(); } ); }

2. 在 computed 中使用异步操作

javascript
// ❌ 错误:computed 中使用异步操作 @computed async get userData() { const response = await fetch('/api/user'); return response.json(); } // ✅ 正确:使用 reaction 处理异步操作 constructor() { reaction( () => this.userId, async (id) => { const response = await fetch(`/api/user/${id}`); runInAction(() => { this.userData = await response.json(); }); } ); }

总结

  • computed 是基于 observable 状态自动更新的派生值
  • computed 会自动缓存计算结果,提高性能
  • computed 应该是纯函数,不应该有副作用
  • 合理使用 computed 链处理复杂计算
  • 避免在 computed 中修改状态或使用异步操作
  • 使用 reaction 处理副作用和异步操作
标签:Mobx