Akita状态管理实战:高效构建可维护的前端应用
项目初期的技术选型
去年底接手一个中后台管理系统重构,前端技术栈要从老旧的 jQuery + 自研状态管理迁移到现代框架。团队里有人推 NgRx,有人想用 Redux Toolkit,但考虑到项目规模不大(也就 10 个左右的核心模块),加上我对 Angular 比较熟,最后选了 Akita —— 它轻量、API 简洁,文档也够直白,特别适合不想折腾复杂状态流的小团队。
说实话,一开始没想太多,就觉得它比 NgRx 少写一堆 boilerplate,store、query、service 三件套搭起来快得很。但真上手后才发现,有些坑藏得挺深。
最大的坑:性能问题
项目做到一半,测试反馈说列表页滚动卡顿,数据量大一点就明显掉帧。我一开始以为是 Angular 的变更检测问题,结果排查半天发现是 Akita 的 store 更新触发了太多不必要的重渲染。
具体来说,我们有个 ProductStore,里面存了上千条商品数据。每次用户筛选或排序,都会调用 update() 方法更新整个数组。虽然 Akita 内部用了 immutable 更新,但 Angular 组件订阅了 select() 后,只要 store 变了,整个列表就重新渲染 —— 即使只有排序顺序变了,数据本身没变。
折腾了半天,发现关键在于 如何让组件只响应真正需要变化的部分。Akita 的 select 默认是引用比较,但如果你每次 update 都传入新数组(哪怕内容一样),引用就变了,订阅就会触发。
后来我改成了这样:在 service 里做缓存和 diff,只在必要时才更新 store:
// product.service.ts
import { Injectable } from '@angular/core';
import { ProductStore } from './product.store';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ProductService {
private lastSortedData: any[] = [];
constructor(private productStore: ProductStore) {}
sortProducts(sortKey: string) {
const currentData = this.productStore._value(); // 注意:这是内部 API,慎用
const sorted = [...currentData].sort((a, b) => a[sortKey] > b[sortKey] ? 1 : -1);
// 关键:只在排序结果真的变化时才更新 store
if (JSON.stringify(sorted) !== JSON.stringify(this.lastSortedData)) {
this.productStore.update({ products: sorted });
this.lastSortedData = sorted;
}
}
}
这里用了 JSON.stringify 做浅比较,虽然性能不完美,但对我们的数据量(<5000 条)够用。当然,更优雅的做法是用 Lodash 的 isEqual,或者自己写个 diff 算法,但时间紧,先这么凑合了。
嵌套状态更新的恶心事
另一个头疼的问题是嵌套对象更新。比如用户资料里有个 settings 对象,里面又有 theme、lang 等字段。Akita 的 update 默认是 shallow merge,直接写:
this.userStore.update({ settings: { theme: 'dark' } });
会把整个 settings 覆盖掉,其他字段就没了。这谁顶得住?
官方文档其实提到了用 upsert 或者传函数,但一开始没注意。后来改成这样:
this.userStore.update(state => ({
...state,
settings: {
...state.settings,
theme: 'dark'
}
}));
虽然啰嗦点,但安全。为了少写这种样板代码,我还封装了个工具函数:
// utils.ts
export function deepUpdate<T extends Record<string, any>>(
state: T,
path: string,
value: any
): T {
const keys = path.split('.');
let result = { ...state };
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
current[key] = { ...current[key] };
current = current[key];
}
current[keys[keys.length - 1]] = value;
return result;
}
用的时候:
this.userStore.update(state => deepUpdate(state, 'settings.theme', 'dark'));
虽然不是最高效的,但至少避免了手写三层展开。不过要注意,这个函数只处理简单嵌套,循环引用或复杂结构会炸,好在我们业务里没遇到。
最终的解决方案
综合下来,我们定了几条规则:
- 所有 store 更新必须通过 service,禁止组件直接调用 store
- 大数组更新前做 diff,避免无意义的引用变更
- 嵌套对象更新统一用
deepUpdate工具函数 - 组件里用
select时尽量 select 具体字段,别整个 state 都 subscribe
比如列表组件,不再这样写:
// ❌ 别这么干
this.products$ = this.productQuery.select();
而是:
// ✅ 只取需要的
this.products$ = this.productQuery.select('products');
this.loading$ = this.productQuery.select('loading');
这样即使其他字段(比如 filters)变了,列表也不会重渲染。
回顾与反思
用 Akita 整体体验还是不错的,比 NgRx 轻快太多,学习成本低,适合中小项目。但它的“简单”也意味着你要自己处理很多边界情况,比如上面说的性能和嵌套更新。
做得好的地方:
- store 结构清晰,每个实体一个 store,职责分明
- 配合 Angular 的 async pipe,数据流很直观
- devtools 插件够用,调试状态变化不费劲
还能优化的点:
- 那个
JSON.stringify的 diff 方案其实有隐患,极端情况下可能漏更新(比如对象里有函数或 undefined),但目前没出问题,先放着 - 没有内置的 undo/redo 机制,如果业务需要,得自己实现,有点麻烦
- store 之间通信不太方便,我们最后用了一个全局 event bus 来解耦,感觉有点倒退
总的来说,Akita 不是银弹,但它在“够用”和“简洁”之间找到了一个不错的平衡点。如果你项目不大,又不想被 NgRx 的模板代码搞疯,Akita 值得试试。但记住,别以为它能自动解决所有性能问题,该做的优化一样不能少。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的 Akita 实践方案,欢迎评论区交流!

暂无评论