本文へ移動

タスクのキャンセル

キャンセルの一例は、ノンブロッキングコール セクションで既に見てきました。このセクションでは、キャンセルについてより詳細に説明します。

タスクがフォークされた後、`yield cancel(task)` を使用して実行を中止できます。

その動作を確認するために、基本的な例を考えてみましょう。UIコマンドによって開始/停止できるバックグラウンド同期です。`START_BACKGROUND_SYNC`アクションを受け取ると、リモートサーバーから定期的にデータを同期するバックグラウンドタスクをフォークします。

`STOP_BACKGROUND_SYNC`アクションがトリガーされるまで、タスクは継続的に実行されます。その後、バックグラウンドタスクをキャンセルし、次の`START_BACKGROUND_SYNC`アクションを待ちます。

import { take, put, call, fork, cancel, cancelled, delay } from 'redux-saga/effects'
import { someApi, actions } from 'somewhere'

function* bgSync() {
try {
while (true) {
yield put(actions.requestStart())
const result = yield call(someApi)
yield put(actions.requestSuccess(result))
yield delay(5000)
}
} finally {
if (yield cancelled())
yield put(actions.requestFailure('Sync cancelled!'))
}
}

function* main() {
while ( yield take('START_BACKGROUND_SYNC') ) {
// starts the task in the background
const bgSyncTask = yield fork(bgSync)

// wait for the user stop action
yield take('STOP_BACKGROUND_SYNC')
// user clicked stop. cancel the background task
// this will cause the forked bgSync task to jump into its finally block
yield cancel(bgSyncTask)
}
}

上記の例では、`bgSyncTask` のキャンセルはGenerator.prototype.return を使用してジェネレータをfinallyブロックに直接ジャンプさせます。ここでは、`yield cancelled()` を使用して、ジェネレータがキャンセルされたかどうかを確認できます。

実行中のタスクをキャンセルすると、キャンセル時にタスクがブロックされている現在のエフェクトもキャンセルされます。

たとえば、アプリケーションのライフサイクルのある時点で、次の保留中の呼び出しチェーンがあるとします。

function* main() {
const task = yield fork(subtask)
...
// later
yield cancel(task)
}

function* subtask() {
...
yield call(subtask2) // currently blocked on this call
...
}

function* subtask2() {
...
yield call(someApi) // currently blocked on this call
...
}

`yield cancel(task)` は `subtask` のキャンセルをトリガーし、それは今度は `subtask2` のキャンセルをトリガーします。

キャンセルは下向きに伝播します(返された値とキャッチされないエラーは上向きに伝播するのと対照的です)。これは、呼び出し元(非同期操作を呼び出すもの)と呼び出し先(呼び出された操作)の間の *契約* と見なすことができます。呼び出し先は操作の実行を担当します。操作が完了した場合(成功またはエラーのいずれか)、その結果は呼び出し元に、最終的には呼び出し元の呼び出し元に伝播します。つまり、呼び出し先は *フローの完了* を担当します。

呼び出し先がまだ保留中で、呼び出し元が操作のキャンセルを決定した場合、呼び出し先(およびおそらく呼び出し先自体によって呼び出されるあらゆる深い操作)に伝播する一種のシグナルをトリガーします。すべての深く保留中の操作はキャンセルされます。

キャンセルが伝播する別の方向もあります。タスクのジョイナ(`yield join(task)` でブロックされているもの)も、結合されたタスクがキャンセルされた場合はキャンセルされます。同様に、これらのジョイナの潜在的な呼び出し元もキャンセルされます(それらは外部からキャンセルされた操作でブロックされているためです)。

fork エフェクトを使用したジェネレータのテスト

`fork` が呼び出されると、バックグラウンドでタスクが開始され、前に学習したようにタスクオブジェクトも返されます。これをテストする際には、ユーティリティ関数 `createMockTask` を使用する必要があります。この関数から返されるオブジェクトは、fork テストの後、次の `next` 呼び出しに渡す必要があります。モックタスクは、たとえば `cancel` に渡すことができます。これは、このページの一番上の `main` ジェネレータのテストです。

import { createMockTask } from '@redux-saga/testing-utils';

describe('main', () => {
const generator = main();

it('waits for start action', () => {
const expectedYield = take('START_BACKGROUND_SYNC');
expect(generator.next().value).to.deep.equal(expectedYield);
});

it('forks the service', () => {
const expectedYield = fork(bgSync);
const mockedAction = { type: 'START_BACKGROUND_SYNC' };
expect(generator.next(mockedAction).value).to.deep.equal(expectedYield);
});

it('waits for stop action and then cancels the service', () => {
const mockTask = createMockTask();

const expectedTakeYield = take('STOP_BACKGROUND_SYNC');
expect(generator.next(mockTask).value).to.deep.equal(expectedTakeYield);

const expectedCancelYield = cancel(mockTask);
expect(generator.next().value).to.deep.equal(expectedCancelYield);
});
});

モックタスクの `setResult`、`setError`、`cancel` メソッドを使用して、その状態を制御できます。たとえば、`mockTask.setResult(42)` はその内部状態を完了に設定し、そのタスクが与えられた `join` エフェクトは `42` を返します。

既にいずれかのメソッドを呼び出した後に、モックタスクに対して `setResult`、`setError`、または `cancel` を呼び出して、2 回目に状態を変更しようとすると、エラーがスローされます。

注記

`yield cancel(task)` は、キャンセルされたタスクが終了するのを待ちません(つまり、finally ブロックを実行するのを待ちません)。cancel エフェクトは fork と同様に動作します。キャンセルが開始されるとすぐに返されます。キャンセルされると、タスクは通常、クリーンアップロジックが終了するとすぐに返される必要があります。

自動キャンセル

手動によるキャンセル以外にも、自動的にキャンセルがトリガーされる場合があります。

  1. `race` エフェクト内では、勝者を除くすべての競合者は自動的にキャンセルされます。

  2. 並列エフェクト(`yield all([...])`)では、サブエフェクトのいずれかが拒否されるとすぐに(`Promise.all` で暗示されているように)、並列エフェクトは拒否されます。この場合、他のすべてのサブエフェクトは自動的にキャンセルされます。