Epic引擎中C++与蓝图协同开发的实战技巧与性能优化
为什么我又在折腾 Epic?
最近项目里要搞一个带复杂状态流的交互模块,比如用户从首页滑动进入详情页,中间可能触发多个异步操作(加载数据、埋点、动画),还得支持中途取消或回退。这类需求用 Redux 的话,光靠 reducer 肯定不够,得上 middleware。Epic(基于 RxJS)和 Thunk 是最常见的两种方案,但其实还有更轻量的替代品,比如直接用 async/await 配合自定义 hook。我之前在几个项目里都试过,这次干脆拉出来对比下,省得下次又纠结。
谁更灵活?谁更省事?
先说结论:如果项目已经重度依赖 RxJS,或者需要处理复杂的异步流(比如 debounce、retry、合并多个事件源),那 Epic 无可替代;但如果只是简单地发个请求、更新状态,Thunk 或原生 async 函数更省事。我比较喜欢用 Thunk,因为团队新人上手快,调试也直观——毕竟 RxJS 的 operator 链一长,出问题真得看半天 marble 图。
来看个具体场景:点击按钮后,先调接口获取用户信息,再根据返回结果决定是否跳转页面。用 Epic 写大概是这样:
import { ofType } from 'redux-observable';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
const fetchUserEpic = (action$) =>
action$.pipe(
ofType('FETCH_USER'),
switchMap((action) =>
ajax.getJSON(https://jztheme.com/api/user/${action.payload.id}).pipe(
map((user) => ({ type: 'FETCH_USER_SUCCESS', payload: user })),
catchError((error) => of({ type: 'FETCH_USER_FAILURE', error }))
)
)
);
代码看起来挺优雅,但注意几点坑:
- switchMap 会自动取消上一个未完成的请求,这在搜索框场景很实用,但如果你需要保留所有请求(比如日志上报),就得换成 mergeMap,容易搞混。
- 错误处理必须显式写
catchError,否则整个流就挂了,页面直接卡死。我之前就因为漏了这个,在测试环境狂点按钮,结果主线程被 unhandled error 拦截,白屏了半小时。 - 调试时得装 Redux DevTools 的 RxJS 插件,不然看不到内部流的状态,纯靠 console.log 猜,效率低。
同样的逻辑用 Thunk 写:
const fetchUser = (id) => async (dispatch) => {
try {
dispatch({ type: 'FETCH_USER_REQUEST' });
const response = await fetch(https://jztheme.com/api/user/${id});
const user = await response.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_FAILURE', error });
}
};
是不是一眼就懂?async/await 的线性结构对大多数开发者更友好,错误处理也自然。而且不用额外学 RxJS 的概念,团队协作成本低。不过缺点是:如果后续要加防抖、节流,或者监听多个 action 触发同一个逻辑,就得自己封装工具函数,不像 Epic 天然支持组合流。
性能对比:差距比我想象的大
很多人以为 Epic 因为基于 Observable,性能肯定比 Thunk 好。其实不然。在简单场景下(比如单次请求),Thunk 的开销更小,因为没有创建 Observable、订阅、operator 链这些额外步骤。我用 Chrome Performance 面板测过,同样触发 100 次请求,Thunk 的 CPU 占用平均低 15% 左右。
但 Epic 的优势在复杂流。比如实现“用户连续输入时,300ms 后才发请求,且只发最后一次”:
const searchEpic = (action$) =>
action$.pipe(
ofType('SEARCH_INPUT_CHANGE'),
debounceTime(300),
switchMap((action) =>
ajax.getJSON(https://jztheme.com/api/search?q=${action.payload.query})
),
map((result) => ({ type: 'SEARCH_SUCCESS', payload: result }))
);
Thunks 要做到这点,得手动管理 timer 和 abort controller,代码又臭又长:
let searchTimeout;
let searchAbortController;
const searchDebounced = (query) => (dispatch) => {
if (searchAbortController) searchAbortController.abort();
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
searchAbortController = new AbortController();
try {
const res = await fetch(https://jztheme.com/api/search?q=${query}, {
signal: searchAbortController.signal
});
const data = await res.json();
dispatch({ type: 'SEARCH_SUCCESS', payload: data });
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
}, 300);
};
这里注意我踩过好几次坑:abort controller 必须每次重置,否则新请求会被旧的 signal 拦截;clearTimeout 也得配对,不然内存泄漏。相比之下,Epic 的 debounceTime + switchMap 一行搞定,还自带取消逻辑,亲测有效。
我的选型逻辑
我现在基本按这个规则选:
- 项目已用 RxJS(比如 Angular 项目):直接上 Epic,别折腾,生态配套好。
- React 项目,且异步逻辑简单(90% 场景就是发请求):用 Thunk 或 RTK Query(Redux Toolkit 内置的),省心省力。
- 需要复杂流控制(防抖、重试、多事件联动):哪怕项目没用 RxJS,我也愿意为关键模块引入 Epic,因为维护成本长期看更低。但会严格限制使用范围,避免团队其他人看不懂。
另外,最近我在新项目里尝试完全不用 Redux,直接用 React Query + 自定义 hook 处理异步状态。比如上面的用户信息加载:
// hooks/useUser.js
import { useQuery } from 'react-query';
export const useUser = (id) => {
return useQuery(['user', id], () =>
fetch(https://jztheme.com/api/user/${id}).then(res => res.json())
);
};
// 组件中
const UserProfile = ({ userId }) => {
const { data, isLoading, error } = useUser(userId);
// 直接渲染,无需 dispatch action
};
这种方式连 middleware 都省了,状态自动缓存、自动重试,连 loading 状态都内置了。除非有跨组件的复杂状态同步需求,否则我越来越倾向这种方案。Redux 的样板代码实在写吐了。
踩坑提醒:这三点一定注意
如果你坚持用 Epic,这几个坑务必绕开:
- 别忘了 unsubscribe:在组件卸载时,如果 epic 还在跑(比如 pending 的请求),必须手动取消,否则可能 setState on unmounted component。通常用 takeUntil 配合组件卸载 action:
const fetchDataEpic = (action$, state$, { COMPONENT_UNMOUNT }) =>
action$.pipe(
ofType('FETCH_DATA'),
switchMap(() =>
ajax.getJSON('https://jztheme.com/api/data').pipe(
takeUntil(action$.pipe(ofType(COMPONENT_UNMOUNT)))
)
)
);
- 避免在 epic 里直接修改 state:所有状态变更必须通过 dispatch action,否则 Redux DevTools 无法追踪,时间旅行调试就废了。
- 测试时 mock ajax 要小心:RxJS 的测试 scheduler 和 Jest 的 mock 机制容易冲突,建议用
jasmine-marbles或直接 mock 整个 epic 的输入输出流,别 mock 内部的 ajax 调用。
折腾了半天发现,其实大部分业务场景根本不需要 Epic 的强大能力。过度设计反而增加维护负担。现在我除非遇到明确的流控制需求,否则一律用最简单的方案——能用 useEffect + async/await 解决的,绝不碰 middleware。
以上是我个人对 Epic、Thunk 及现代数据获取方案的对比总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

暂无评论