redux-sagaのフォークモデル
redux-saga
では、バックグラウンドで実行されるタスクを2つのエフェクトを使用して動的にフォークできます。
fork
はアタッチされたフォークを作成するために使用されます。spawn
はデタッチされたフォークを作成するために使用されます。
アタッチされたフォーク(fork
を使用)
アタッチされたフォークは、次の規則によって親にアタッチされたままになります。
完了
- Sagaは以下の場合にのみ終了します。
- 自身の命令本体を終了する場合
- すべてのアタッチされたフォーク自体が終了する場合
たとえば、次のような場合を考えてみましょう。
import { fork, call, put, delay } 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 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つのエフェクトすべてが実行されます。fork
エフェクトはノンブロッキングであるため、タスクはdelay(1000)
でブロックされます。2つのフォークされたタスクが終了する場合。つまり、必要なリソースをフェッチし、対応する
receiveData
アクションを実行した後。
したがって、タスク全体は、1000ミリ秒の遅延が経過し、かつ、task1
とtask2
の両方が処理を終了するまでブロックされます。
たとえば、1000ミリ秒の遅延が経過し、2つのタスクがまだ終了していない場合、fetchAll
はすべてのフォークされたタスクが完了するまで待ってから、タスク全体を終了します。
注意深い読者なら、fetchAll
sagaが並列エフェクトを使用して書き換えられることに気づいたかもしれません。
function* fetchAll() {
yield all([
call(fetchResource, 'users'), // task1
call(fetchResource, 'comments'), // task2,
delay(1000)
])
}
実際、アタッチされたフォークは並列エフェクトと同じセマンティクスを共有しています。
- タスクを並行して実行しています。
- 親は、起動されたすべてのタスクが終了した後に終了します。
これは、他のすべてのセマンティクス(エラーとキャンセルの伝播)にも適用されます。アタッチされたフォークがどのように動作するかを理解するには、それを動的な並列エフェクトと考えるとよいでしょう。
エラー伝播
同じアナロジーに従って、並列エフェクトでエラーがどのように処理されるかを詳しく見てみましょう。
たとえば、次のエフェクトがあるとしましょう。
yield all([
call(fetchResource, 'users'),
call(fetchResource, 'comments'),
delay(1000)
])
上記のエフェクトは、3つの子エフェクトのいずれかが失敗するとすぐに失敗します。さらに、キャッチされなかったエラーは、並列エフェクトが他の保留中のエフェクトをすべてキャンセルする原因になります。たとえば、call(fetchResource, 'users')
がキャッチされないエラーを発生させた場合、並列エフェクトは他の2つのタスクをキャンセルし(まだ保留中の場合)、失敗した呼び出しからの同じエラーで自身を中断します。
同様に、アタッチされたフォークの場合、Sagaは以下の場合にすぐに中断します。
命令の本体がエラーをスローする場合
キャッチされなかったエラーがアタッチされたフォークの1つによって発生した場合
したがって、前の例では、
//... imports
function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield 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
}
}
たとえば、ある時点で、fetchAll
がdelay(1000)
エフェクトでブロックされていて、たとえば、task1
が失敗した場合、fetchAll
タスク全体が失敗し、次のことが発生します。
他の保留中のタスクのすべてをキャンセルします。 これには以下が含まれます。
- メインタスク(
fetchAll
の本体):キャンセルするということは、現在のエフェクトdelay(1000)
をキャンセルすることを意味します。 - まだ保留中の他のフォークされたタスク。つまり、例では
task2
です。
- メインタスク(
call(fetchAll)
自体がエラーを発生させ、それがmain
のcatch
本体でキャッチされます。
main
内でcall(fetchAll)
からのエラーをキャッチできるのは、ブロッキング呼び出しを使用しているためであることに注意してください。また、fetchAll
から直接エラーをキャッチすることはできません。これは経験則です。フォークされたタスクからのエラーをキャッチすることはできません。アタッチされたフォークの失敗は、フォーク元の親を中断させます(並列エフェクト内部からエラーをキャッチする方法はなく、並列エフェクトをブロックすることによって外部からのみキャッチできるのと同様です)。
キャンセル
Sagaをキャンセルすると、次のキャンセルが発生します。
メインタスク これは、Sagaがブロックされている現在のエフェクトをキャンセルすることを意味します。
まだ実行中のすべてのアタッチされたフォーク
WIP
デタッチされたフォーク(spawn
を使用)
デタッチされたフォークは、独自の実行コンテキストで動作します。親は、デタッチされたフォークが終了するのを待ちません。spawnされたタスクからキャッチされないエラーは、親にバブルアップされません。また、親をキャンセルしても、デタッチされたフォークは自動的にキャンセルされません(明示的にキャンセルする必要があります)。
簡単に言うと、デタッチされたフォークは、middleware.run
APIを使用して直接開始されたルートSagaのように動作します。
WIP