跳至主要內容

任務取消

我們已經在 非阻擋呼叫 區段看到取消的範例。在本區段,我們將更詳細地檢視取消。

一旦一個任務分叉,你可以使用 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 讓 Generator 直接跳到 finally 區塊。你可以使用 yield cancelled() 來檢查 Generator 是否已經被取消。

取消正在執行的任務也會取消當前 Effect,而任務會在取消的時刻停留在該處。

例如,假設在應用程式的生命週期中某一點,我們有這個待處理的呼叫鏈

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 效果測試 Generator

fork 被呼叫時,他會在背景中啟動任務,並且還會傳回任務物件,就像我們先前學過的。在測試時,我們必須使用工具函式 createMockTask。此函式回傳的物件應該傳遞給 fork 測試後的下一個 next 呼叫。例如,模擬任務可以傳遞給 cancel。以下是此頁面頂端的 main Generator 的測試。

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

你可以使用模擬任務的 setResultsetErrorcancel 方法來控制其狀態。例如,mockTask.setResult(42) 會將其內部狀態設定為已完成,而任何給定任務的 join 效果都會傳回 42

在模擬任務上呼叫 setResultsetErrorcancel,然後才呼叫其中一個(嘗試第二次更改其狀態),會擲回錯誤。

註解

請務必記得,yield cancel(task) 並不會等待被取消的工作完成(即執行其最終區塊)。取消效果的行為很像 fork。它會在取消發起後立即回傳。一旦被取消,工作通常會在其完成清理邏輯後立即回傳。

自動取消

除了手動取消外,也有一些會自動觸發取消的情況

  1. race 效果中。除了獲勝者外,所有 race 參與競爭者都會自動被取消。

  2. 在平行效果中 (yield all([...]))。只要其中一個子效果會被拒絕 (如同 Promise.all 所暗示),平行效果就會被拒絕。在這種情況下,所有其他子效果都會自動被取消。