跳至主內容

Root Saga 模式

Root Saga 會將多個 Sagas 彙整到單一進入點,供 sagaMiddleware 執行。

入門教學 中,已示範你的 root saga 應該類似以下內容

export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
// code after all-effect
}

這是實作 root 的方法之一。在此,all 效果用於陣列,並且你的 saga 會並行執行。其他實作可能有助於你更好地處理錯誤和更複雜的資料流程。

非同步 fork 效果

貢獻者 @slorber 在 議題 760 中提到其他常見的 root 實作。首先,有一個廣為人知的實作其行為類似於教學課程 root saga 的行為

export default function* rootSaga() {
yield fork(saga1)
yield fork(saga2)
yield fork(saga3)
// code after fork-effect
}

使用三個獨立的 yield fork 會產生一個工作描述符三次。應用程式的產生行為是,所有次級 saga 都以相同的順序啟動並執行。由於 fork 非阻斷,rootSaga 可以執行完成,同時子 saga 仍繼續執行並被其內部程序阻斷。

一個龐大的 all 目標與幾個 fork 目標的不同處在於,all 目標是阻斷的,因此在所有子 saga 完成時會執行all 目標後方的程式碼(請參閱上面程式碼中的註解),而 fork 目標是非阻斷的,因此會在 yield fork 目標後立即執行fork 目標後方的程式碼。另一個不同處是,在使用 fork 目標時你可以取得工作描述符,因此在後續程式碼中,你可以透過工作描述符來取消/加入分岔工作。

將 fork 目標嵌套在 all 目標中

const [task1, task2, task3] = yield all([ fork(saga1), fork(saga2), fork(saga3) ])

在設計 root saga 時,另一個廣為人知的範例是將 fork 目標嵌套在 all 目標中。藉此,你可以取得工作描述符陣列,而且 all 目標後方的程式碼會立即執行,因為每個 fork 目標都是非阻斷的,而且同步傳回工作描述符。

請注意,儘管 fork 目標嵌套在 all 目標中,它們始終透過基礎 forkQueue 連接到父工作中。已分岔工作產生的未捕捉錯誤會浮現至父工作中,並因此中止父工作(及其所有子工作) - 它們無法被父工作捕捉。

避免在 race 目標中嵌套 fork 目標

// DO NOT DO THIS. The fork effect always wins the race immediately.
yield race([
fork(someSaga),
take('SOME-ACTION'),
somePromise,
])

另一方面,在 race 目標中的 fork 目標很可能是錯誤。在上面的程式碼中,由於 fork 目標是非阻斷的,它們將永遠立即達成競程。

讓 root 保持運作狀態

實際上,這些實作並非非常實用,因為你的 rootSaga 會在任何個別子目標或 saga 中的第一個錯誤終止,並導致整個應用程式崩潰!特別是 Ajax 要求會讓你的應用程式聽命於應用程式提出要求的所有端點的狀態。

spawn 是能將子佐賀與其父佐賀斷開連線的效果,讓它能在不讓父佐賀崩潰的情況下失敗。顯然,這並不能減輕我們身為開發人員應有的責任,我們仍必須處理產生的錯誤。事實上,這可能會讓開發人員看不見某些故障,並在下個流程中造成問題。

spawn 效果可以說是類似 React 中的 spawn 效果的 錯誤界限,因為它可以在佐賀樹系結構的某些層級中用作額外的安全措施,並停止有故障的功能,而不讓整個應用程式崩潰。不同之處在於沒有像 React 錯誤界限所使用的特殊 componentDidCatch 語法。您仍必須撰寫自己的錯誤處理和復原程式碼。

export default function* rootSaga() {
yield spawn(saga1)
yield spawn(saga2)
yield spawn(saga3)
}

在這個實作中,即使其中一個佐賀發生故障,仍不會中止 rootSaga 和其他佐賀。不過,這也可能造成問題,因為有故障的佐賀在應用程式生命週期中將無法使用。

讓所有事物保持運作

在某些情況下,您可能會希望自己的佐賀在發生故障時能夠重新啟動。它的好處是,您的應用程式和佐賀可以在故障後繼續運作,例如 yield takeEvery(myActionType) 的佐賀。但我們不建議將它作為讓所有佐賀保持運作的通解。讓佐賀在合理可預測的情況下發生故障,並處理/記錄錯誤很有可能更合乎邏輯。

例如,@ajwhite 提供了以下場景作為讓佐賀保持運作會比解決問題造成更多問題的情況

function* sagaThatMayCrash () {
// wait for something that happens _during app startup_
yield take('APP_INITIALIZED')

// assume it dies here
yield call(doSomethingThatMayCrash)
}

如果 sagaThatMayCrash 重新啟動,它就會重新啟動並等待一個只會在應用程式啟動時發生一次的動作。在此場景中,它重新啟動了,但它從未復原。

但對於會受益於重新啟動的特定情況,使用者 @granmoe 在 Issue#570 中提出了類似以下內容的實作

function* rootSaga () {
const sagas = [
saga1,
saga2,
saga3,
];

yield all(sagas.map(saga =>
spawn(function* () {
while (true) {
try {
yield call(saga)
break
} catch (e) {
console.log(e)
}
}
}))
);
}

此策略將我們的子佐賀對應到衍生的產生器 (解除它們與父代根目錄的連接),並在 try 區塊中將我們的佐賀作為子任務來啟動。我們的佐賀會執行到終止後,然後自動重新啟動。catch 區塊會無害地處理任何我們的佐賀拋出的錯誤或終止該佐賀的錯誤。