在 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);
}
}
})
);
}