redux-saga 的 fork model
在 redux-saga
的世界里,你可以使用 2 个 Effects 在后台动态地 fork task
fork
用来创建 attached forksspawn
用来创建 detached forks
Attached forks (using fork
)
Attached forks 通过以下规则继续附加在它们的 parent
结论
- Saga 只会在这之后终止:
- 它终止了自己的指令
- 所有附加的 forks 本身被终止
假如我们有以下情形:
import { delay } from 'redux-saga'
import { fork, call, put } from 'redux-saga/effects'
import api from './somewhere/api' // app specific
import { receiveData } from './somewhere/actions' // app specific
function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield call(delay, 1000)
}
function* fetchResource(resource) {
const {data} = yield call(api.fetch, resource)
yield put(receiveData(data))
}
function* main() {
yield call(fetchAll)
}
call(fetchAll)
将在这之后终止:
fetchAll
本身终止了, 意味着这 3 个 effect 都会被执行. 由于fork
effects 是非阻塞的, task 将被阻塞在call(delay, 1000)
这 2 个被 fork 的 task 终止, 意思是在 fetch 所需的资源之后放入对应的
receiveData
action
所以整个 task 将阻塞直到一个 1000 毫秒的 delay 被传送,并且 task1 和 task2 完成了他们的任务。
比方说,1000 毫秒的 delay 和这两个 task 还沒有完成,然后 fetchAll 在终止整个 task 之前,将一直等待直到所有被 fork 的 task 完成。
细心的读者可能会注意到 fetchAll
saga 可以使用平行 Effect 来重写。
function* fetchAll() {
yield all([
call(fetchResource, 'users'), // task1
call(fetchResource, 'comments'), // task2,
call(delay, 1000)
])
}
事实上,被附加的 fork 与平行 Effect 共享相同的语意:
- 在平行情況下我们执行 task
- 在所有被 launch 的 task 终止后,parent 将会终止
这也适用于其他语意(错误和取消传播)。你可以简单地把它考虑作为一个动态平行 Effect,来理解附加 fork 的行为。
Error 传播
按照同样的比喻,让我们来详细的检查在平行的 Effect 中是如何处理错误的
例如,假设我们有这么一个 Effect:
yield all([
call(fetchResource, 'users'),
call(fetchResource, 'comments'),
call(delay, 1000)
])
一旦其中一个子 Effect 失敗,上方的 Effect 就会失敗。
此外,未捕获的错误将会造成平行 Effect 取消所有其他 pending 中的 Effect。例如,如果 call(fetchResource, 'users')
发出了一个未捕获的错误,平行 Effect 将会取消其他两个 task(如果它们依然在 pending),然后从失败的调用中,以错误终止它本身。
类似于被 attach 的 forks,Saga 会在以下情况马上终止:
指令的 main body 拋出了一个错误
一个未捕获的错误通过其中一个被 attach 的 forks 抛出
所以在先前的例子中:
//... imports
function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield call(delay, 1000)
}
function* fetchResource(resource) {
const {data} = yield call(api.fetch, resource)
yield put(receiveData(data))
}
function* main() {
try {
yield call(fetchAll)
} catch (e) {
// handle fetchAll errors
}
}
If at a moment, for example, fetchAll
is blocked on the call(delay, 1000)
Effect, and say, task1
failed, then the whole
fetchAll
task will fail causing
Cancellation of all other pending tasks. This includes:
- The main task (the body of
fetchAll
): cancelling it means cancelling the current Effectcall(delay, 1000)
- The other forked tasks which are still pending. i.e.
task2
in our example.
- The main task (the body of
The
call(fetchAll)
will raise itself an error which will be caught in thecatch
body ofmain
Note we're able to catch the error from call(fetchAll)
inside main
only because we're using a blocking call. And that
we can't catch the error directly from fetchAll
. This is a rule of thumb, you can't catch errors from forked tasks. A failure
in an attached fork will cause the forking parent to abort (Just like there is no way to catch an error inside a parallel Effect, only from
outside by blocking on the parallel Effect).
如果在这样的情況下,例如 fetchAll
被阻塞在 call(delay, 1000)
Effect,假如 task1
失败了,然后整个 fetchAll
task 将会因此失敗
取消所有其他在 pending 的 task。包含:
- main task(
fetchAll
的本身):取消的意思是,取消目前的call(delay, 1000)
的 Effect - 其他被 fork 的 task 仍然在 pending。例如我们例子中的
task2
。
- main task(
在
main
的catch
内将会捕获由call(fetchAll)
抛出的错误
注意,因为我们使用了一个阻塞的 call,所以我们只能从 main
內部 catch call(fetchAll)
的错误,而且我们不能直接从 fetchAll
捕获错误。这是首要的原则,你不能从被 fork 的 task 捕获错误。在一个被附加的 fork 中的错误将会导致被 fork 的 parent 被终止(就像没有方法在一个平行 Effect 内捕捉错误一样,只能从外部通过被阻塞的平行 Effect)。
Cancellation
取消 Saga 导致:
main task 意思是当 Saga 被阻塞时,取消当前的 Effect
所有被附加的 fork 仍然继续执行
WIP
Detached forks (using spawn
)
Detached forks live in their own execution context. A parent doesn't wait for detached forks to terminate. Uncaught errors from spawned tasks are not bubbled up to the parent. And cancelling a parent doesn't automatically cancel detached forks (you need to cancel them explicitly).
In short, detached forks behave like root Sagas started directly using the middleware.run
API.
Detached forks(使用 spawn
)
被分离的 fork 存活在它们本身的执行上下文中。parent 不会等待被分离的 fork 终止。从被 spawn 的 task 抛出的未捕获的错误不会冒泡到 parent,而且取消一个 parent 不会自动取消被分离的 fork(你需要明确地取消它们)。
简单来说,被分离的 fork 的行为像是使用 middleware.run
API 直接启动 root Saga。
WIP