メインコンテンツにスキップ

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ミリ秒の遅延が経過し、かつtask1task2の両方が処理を終了するまでブロックされます。

たとえば、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
}
}

たとえば、ある時点で、fetchAlldelay(1000)エフェクトでブロックされていて、たとえば、task1が失敗した場合、fetchAllタスク全体が失敗し、次のことが発生します。

  • 他の保留中のタスクのすべてをキャンセルします。 これには以下が含まれます。

    • メインタスクfetchAllの本体):キャンセルするということは、現在のエフェクトdelay(1000)をキャンセルすることを意味します。
    • まだ保留中の他のフォークされたタスク。つまり、例ではtask2です。
  • call(fetchAll)自体がエラーを発生させ、それがmaincatch本体でキャッチされます。

main内でcall(fetchAll)からのエラーをキャッチできるのは、ブロッキング呼び出しを使用しているためであることに注意してください。また、fetchAllから直接エラーをキャッチすることはできません。これは経験則です。フォークされたタスクからのエラーをキャッチすることはできません。アタッチされたフォークの失敗は、フォーク元の親を中断させます(並列エフェクト内部からエラーをキャッチする方法はなく、並列エフェクトをブロックすることによって外部からのみキャッチできるのと同様です)。

キャンセル

Sagaをキャンセルすると、次のキャンセルが発生します。

  • メインタスク これは、Sagaがブロックされている現在のエフェクトをキャンセルすることを意味します。

  • まだ実行中のすべてのアタッチされたフォーク

WIP

デタッチされたフォーク(spawnを使用)

デタッチされたフォークは、独自の実行コンテキストで動作します。親は、デタッチされたフォークが終了するのを待ちません。spawnされたタスクからキャッチされないエラーは、親にバブルアップされません。また、親をキャンセルしても、デタッチされたフォークは自動的にキャンセルされません(明示的にキャンセルする必要があります)。

簡単に言うと、デタッチされたフォークは、middleware.run APIを使用して直接開始されたルートSagaのように動作します。

WIP