在 saga 中,无论是请求失败,还是代码异常,均可以通过 try catch 来捕获。
Call vs Fork
call
是阻塞任务,fork
是非阻塞任务。
function* errorSaga() {
throw new Error('模拟异常');
}
function* saga() {
try {
yield call(errorSaga);
} catch(e) {
console.err(e);
}
}
// 调用saga
用try catch
包裹一个call
调用的fn
,那么这个fn
里抛出错误时,是会被捕获到的
function* errorSaga() {
throw new Error('模拟异常');
}
function* saga() {
try {
yield fork(errorSaga);
} catch(e) {
console.err(e);
}
}
// 调用saga
用try catch
包裹一个fork
调用的fn
,那么这个fn
里抛出错误时,是不会被捕获到的。
原因在于,fork 出的 childtask
,与执行 fork 的 parentTask
之间虽然是父子关系,但 childtask
是异步的,所以没办法捕获到。如果想捕获 fork 的错误,需要从 parentTask
入手
function* errorSaga() {
throw new Error('模拟异常');
}
function* saga() {
try {
yield fork(errorSaga);
} catch(e) { --> 这里不会捕获到,其效果与直接调用 yield fork(errorSaga); 一致
console.err(e); -->
}
}
function* root() {
try {
yield call(saga)
} catch(e) { --> 这里会捕获到 errorSaga 的错误
console.err(e);
}
}
// 调用root
也就是说,阻塞调用 parentTask
时,是能够捕获到 非阻塞的 childtask
错误的。那如果非阻塞调用 parentTask
呢?
function* errorSaga() {
throw new Error('模拟异常');
}
function* saga() {
try {
yield fork(errorSaga);
} catch(e) { --> 这里不会捕获到,其效果与直接调用 yield fork(errorSaga); 一致
console.err(e); -->
}
}
function* root() {
try {
yield fork(saga)
} catch(e) { --> 这里不会捕获到 errorSaga 的错误!!!
console.err(e);
}
}
// 调用root
Fork vs Spawn
Spawn
类似 fork
,是非阻塞的,但Spawn
出的 task1
是一个独立的任务,与执行Spawn
的 task2
之间不是父子关系,task1
属于顶层任务
function* errorSaga() {
throw new Error('模拟异常');
}
function* saga() {
yield spawn(errorSaga);
}
function* root() {
try {
yield call(saga)
} catch(e) { --> 这里不会捕获到 errorSaga 的错误
console.err(e);
}
}
// 调用root
未捕获 Error 场景
function* saga1 () { /* ... */ }
function* saga2 () { throw new Error('模拟异常'); }
function* saga3 () { /* ... */ }
function* rootSaga() {
yield fork(saga1);
yield fork(saga2);
yield fork(saga3);
}
// 启动 saga
sagaMiddleware.run(rootSaga);
假设 saga2 出现代码异常了,且没有进行异常捕获,这样的异常会导致整个 Web App 崩溃么?答案是:肯定的!
解释如下:
redux-saga 中执行 sagaMiddleware.run(rootsaga)
或 fork(saga)
时,均会返回一个 task 对象(上文中说到),嵌套的 task 之间会存在 父子关系, 就比如上述代码:
- rootSaga 生成了 rootTask。
- saga1,saga2 和 saga3,在 rootSaga 内部执行,生成的 task,均被认为是 rootTask 的 childTask。
现在某一个 childTask 异常了(比如这里的: saga2),那么它的 parentTask(如:rootTask)收到通知先会执行自身的 cancel 操作,再通知其他 childTask(如:saga1,saga3) 同样执行 cancel 操作。(这其实正是 Saga Pattern 的思想)
但这就意味着,用户可能会因为一个按钮点击引发的异常,而导致整个 Web 应用的功能均无法使用!!
那么,面对这样的问题,如何优化呢?隔离 childTask 是首先想到的一种方案。
export default function* root() {
yield spawn(saga1);
yield spawn(saga2);
yield spawn(saga3);
}
使用 spawn 替换 fork,它们的区别在于 spawn 返回 isolate task,是一个顶层 task,不存在 父子关系,也就是说,即使 saga2 挂了,rootSaga 也不受影响,saga1 和 saga3 自然更不会受影响,依然可以正常工作。
Error 捕获方案
通过在最上层为每一个 childSaga 添加异常捕获,并通过 while(true) {}
循环自动创建新的 childTask 取代 异常 childTask,以保证功能依然可用(这就类似于 Egg 中某一个 woker 进程 挂了,自动重启一个新的 woker 进程一样)
function* rootSaga () {
const sagas = [ saga1, saga2, saga3 ];
yield sagas.map(saga =>
spawn(function* () {
while (true) {
try {
yield call(saga);
} catch (e) {
console.log(e);
}
}
})
);
}