完全指南:用 `all` Effect 組合器并發(fā)執(zhí)行多個 Saga 任務(wù))
前端【免費下載鏈接】redux-sagaAn alternative side effect model for Redux apps項目地址https://gitcode.com/gh_mirrors/re/redux-saga點擊查看免費下載yield語句雖然能以線性、同步的書寫風(fēng)格描述異步控制流但它天然是逐條阻塞的——連續(xù)兩次yield call(...)只會串行執(zhí)行。本文聚焦 redux-saga 的 Effect 組合器all講解如何將多個 effect 并行運行、如何收集全部結(jié)果、如何處理快速失敗與 END 終止并結(jié)合本倉庫packages/core的源碼實現(xiàn)effectRunnerMap.js、io.js、utils.js與官方測試用例剖析其底層工作原理幫助你寫出真正可并發(fā)的 Saga。為什么連續(xù)yield只能串行執(zhí)行在 Saga 中yield會暫停生成器直到被 yield 的 effect 被中間件解析完畢next()才會繼續(xù)推進。因此下面這段代碼是錯誤的寫法——第二個 effect 必須等待第一個 effect 完成后才會開始執(zhí)行// 錯誤兩個 effect 會按順序串行執(zhí)行 const users yield call(fetch, /users) const repos yield call(fetch, /repos)fetch(/repos)要等到fetch(/users)返回結(jié)果之后才會發(fā)起請求兩個網(wǎng)絡(luò)請求的總耗時是兩次請求耗時之和。如果這兩次請求彼此獨立、沒有數(shù)據(jù)依賴這種寫法就白白浪費了并發(fā)能力。使用all([...])并行執(zhí)行多個 Effect正確的做法是把多個 effect 放進一個數(shù)組中交給all組合器import { all, call } from redux-saga/effects // 正確多個 effect 會并行執(zhí)行 const [users, repos] yield all([ call(fetch, /users), call(fetch, /repos), ])all接收一個 effect 數(shù)組中間件會同時啟動其中的每一個 effect而不是按順序等待生成器會被阻塞直到所有effect 都成功 resolve此時yield表達式返回一個數(shù)組元素順序與傳入數(shù)組一一對應(yīng)users對應(yīng)第一個結(jié)果repos對應(yīng)第二個如果其中任意一個effect 被 reject生成器會立即恢復(fù)并拋出該錯誤——即快速失敗。從行為語義上看yield all([...])與標準Promise.all([...])完全一致官方文檔 API.md 中也明確指出它是Promise#all的對應(yīng) API。本倉庫的官方測試 packages/core/tests/interpreter/all.js 驗證了這一點將一個 Promise、一個cps回調(diào)式調(diào)用和一個take放入同一數(shù)組并行執(zhí)行三者全部完成后結(jié)果按序返回[1, 2, {type: action}]。并行的不只是call任何 Effect 都可以組合all數(shù)組中的元素可以是任意 effectcall、cps、take、put、select甚至嵌套的all與fork。例如測試用例中就把三種不同類型的 effect 放進了同一個數(shù)組function* genFn() { actual yield all([ def.promise, // 直接 yield 一個 Promise cps(cps, 2), // Node 風(fēng)格回調(diào)函數(shù) take(action), // 等待某個 action 被 dispatch ]) }只有當(dāng) Promise 被 resolve、CPS 回調(diào)被調(diào)用、且action被 dispatch 之后這個yield all才會返回[1, 2, {type: action}]。對象形式all({...})為結(jié)果命名all除了接收數(shù)組還支持傳入一個標簽 → effect的字典對象返回結(jié)果同樣是一個對象鍵與傳入的標簽一一對應(yīng)無需依賴數(shù)組下標import { all, call } from redux-saga/effects function* mySaga() { const { customers, products } yield all({ customers: call(fetchCustomers), products: call(fetchProducts), }) }這與race(effects)的命名方式保持一致參見 API.md。官方測試 packages/core/tests/interpreter/all.js 專門覆蓋了這種named effects場景{ ac: take(action), prom: promise }全部完成后返回{ ac: action, prom: 1 }。對象形式特別適合結(jié)果語義清晰、后續(xù)還要按名字取用的場景也讓代碼可讀性更強。源碼級原理all在中間件內(nèi)部是如何工作的要真正理解all需要沿三條代碼路徑追溯其實現(xiàn)。1. 創(chuàng)建 Effect 描述對象all在 packages/core/src/internal/io.js 中只是一個描述符工廠它并不執(zhí)行任何邏輯只返回一個帶有[IO]標志的純對象export function all(effects) { const eff makeEffect(effectTypes.ALL, effects) eff.combinator true return eff }combinator true標記它是一個組合器 effect這與race一致見同文件race實現(xiàn)目的是讓類型系統(tǒng)與中間件能夠把它和普通 effect 區(qū)分開。這正是 redux-saga 聲明式 effect 思想的體現(xiàn)Saga 只 yield 描述由中間件負責(zé)執(zhí)行參見 docs/basics/DeclarativeEffects.md 與 docs/basics/Effect.md。2. 并行啟動runAllEffect中間件內(nèi)部維護著一張effectRunnerMap見 packages/core/src/internal/effectRunnerMap.js其中ALL類型對應(yīng)runAllEffectfunction runAllEffect(env, effects, cb, { digestEffect }) { const effectId currentEffectId const keys Object.keys(effects) if (keys.length 0) { cb(is.array(effects) ? [] : {}) return } const childCallbacks createAllStyleChildCallbacks(effects, cb) keys.forEach((key) { digestEffect(effects[key], effectId, childCallbacks[key], key) }) }關(guān)鍵點對空數(shù)組/空對象直接以[]或{}完成不產(chǎn)生任何等待官方測試 packages/core/tests/interpreter/all.js 驗證空數(shù)組返回[]通過createAllStyleChildCallbacks為每個子 effect 創(chuàng)建一個獨立回調(diào)同步遍歷所有鍵并逐個digestEffect中間件逐個啟動它們——這正是并行發(fā)生的時刻所有子 effect 在同一輪中被派發(fā)執(zhí)行而不是等待前一個完成后再啟動下一個。digestEffect定義在 packages/core/src/internal/proc.js它負責(zé)把 effect 分發(fā)給對應(yīng)的 runner是組合器驅(qū)動子任務(wù)的入口。3. 收集結(jié)果與快速失敗createAllStyleChildCallbacks結(jié)果收集邏輯在 packages/core/src/internal/utils.js 的createAllStyleChildCallbacks中這是理解all行為的關(guān)鍵function checkEnd() { if (completedCount totalCount) { completed true parentCallback(results) } } keys.forEach((key) { const chCbAtKey (res, isErr) { if (completed) return if (isErr || shouldComplete(res)) { parentCallback.cancel() // 快速失敗取消其余子任務(wù) parentCallback(res, isErr) } else { results[key] res completedCount checkEnd() } } ... })從中可以得出三條確鑿的行為規(guī)則全部完成才返回內(nèi)部用completedCount計數(shù)只有子 effect 全部成功時才會把結(jié)果數(shù)組/對象回傳給生成器任一失敗立即終止只要有子 effect 以錯誤結(jié)束isErr為真立即觸發(fā)父回調(diào)并取消其余仍在運行的子任務(wù)——對應(yīng)文檔所述as soon as one is rejectedEND 也是一種完成信號shouldComplete(res)會檢查TERMINATE/TASK_CANCEL定義在同文件 utils.js。也就是說如果all中的某個take在 channel 關(guān)閉時收到了END信號整個all會被視為終止而不是繼續(xù)等待。官方測試 packages/core/tests/interpreter/all.js 驗證了這一點all([promise, take(action)])在 dispatchEND后生成器進入finally塊結(jié)束。4. 與race的本質(zhì)區(qū)別對比 effectRunnerMap.js 中的runRaceEffectrace在第一個子 effect 完成時立即返回該結(jié)果并自動取消其余落敗的 effect即誰先完成聽誰的而all必須等待全部子 effect 完成全部完成才算完成。二者分別對應(yīng)Promise.race與Promise.all的語義具體用法可參考 docs/advanced/RacingEffects.md。實戰(zhàn)用all并行啟動多個后臺任務(wù)一個非常典型的用法是在根 Saga 中并行 fork 多個獨立監(jiān)聽器。倉庫示例 examples/shopping-cart/src/sagas/index.js 就是這么做的export default function* rootSaga() { yield all([ fork(getAllProducts), // 加載商品數(shù)據(jù) fork(watchGetProducts), // 監(jiān)聽商品請求 fork(watchCheckout), // 監(jiān)聽結(jié)算請求 ]) }這里all([fork(...), fork(...), fork(...)])一次性并行啟動三個相互獨立的后臺任務(wù)fork 是非阻塞的參見 docs/advanced/NonBlockingCalls.md根 Saga 會一直持有它們直到三者全部結(jié)束。這種并行啟動 統(tǒng)一收口的模式在真實項目中極為常見。另一處參考是 examples/real-world/sagas/index.js同樣用yield all([...])組合多個fork。錯誤處理與注意事項快速失敗的代價由于all具備Promise.all式的快速失敗語義任何一個子 effect 拋錯都會中斷整個組合其余仍在進行的請求會被取消。如果希望某個失敗不影響其他任務(wù)要么用try/catch包住單個 effect要么改用spawn/fork獨立任務(wù)或配合race與cancelled()實現(xiàn)更細粒度的容錯參考 docs/basics/ErrorHandling.md 與 docs/advanced/TaskCancellation.md??諗?shù)組是合法的yield all([])立即返回[]不會阻塞生成器測試 packages/core/tests/interpreter/all.js 已覆蓋可以安全地在動態(tài)拼接 effect 列表的場景中使用。結(jié)果順序與輸入順序一致數(shù)組形式下無論各請求完成先后返回數(shù)組的下標始終對應(yīng)傳入時的下標不會出現(xiàn)結(jié)果錯位。不要與嵌套all混淆all內(nèi)部還可以嵌套all組合器支持遞歸但要注意嵌套會引入階段化等待——內(nèi)層all全部完成后外層才繼續(xù)收集這通常用于先并行 A、B再并行 C、D的分階段場景。并行并不等于線程Saga 的并行是異步并發(fā)所有 effect 仍在同一個 JavaScript 線程中通過事件循環(huán)調(diào)度不會帶來多線程加速它的價值在于同時發(fā)起多個 I/O 操作以縮短總等待時間。小結(jié)連續(xù)yield call(...)是串行執(zhí)行獨立任務(wù)請改用all組合器yield all([e1, e2, ...])并行執(zhí)行并返回按序結(jié)果數(shù)組行為等價于Promise.allyield all({label: effect})返回帶標簽的結(jié)果對象語義更清晰中間件通過runAllEffecteffectRunnerMap.js同步派發(fā)所有子 effect并用createAllStyleChildCallbacksutils.js實現(xiàn)全部完成才返回、任一失敗即取消的收口邏輯根 Saga 中常用yield all([fork(a), fork(b), fork(c)])并行啟動多個后臺任務(wù)。相關(guān)文檔可繼續(xù)閱讀 docs/API.md、docs/advanced/ComposingSagas.md、docs/advanced/ForkModel.md并通過官方測試 packages/core/tests/interpreter/all.js 驗證上述所有行為。贊分享前端【免費下載鏈接】redux-sagaAn alternative side effect model for Redux apps項目地址https://gitcode.com/gh_mirrors/re/redux-saga點擊查看免費下載相關(guān)推薦Redux-Saga并發(fā)模式并行任務(wù)執(zhí)行與結(jié)果聚合策略Redux Saga并發(fā)模式并行任務(wù)執(zhí)行與結(jié)果聚合策略 你是否在處理復(fù)雜異步邏輯時遇到過以下困境多個API請求串行執(zhí)行導(dǎo)致頁面加載緩慢重復(fù)點擊按鈕觸發(fā)冗余前端Redux-Saga進階指南Saga的組合與并行控制Redux Saga進階指南Saga的組合與并行控制 引言為什么需要Saga的組合與并行控制 在現(xiàn)代前端應(yīng)用中復(fù)雜的異步邏輯處理已成為常態(tài)。你是否遇到過前端Redux-Saga高級模式任務(wù)管理與并發(fā)控制Redux Saga高級模式任務(wù)管理與并發(fā)控制 本文深入探討Redux Saga的高級并發(fā)控制模式重點分析fork與spawn的任務(wù)創(chuàng)建與生命周期管理、任務(wù)前端上一篇ai-engineering-from-scratch 如何使用 Batch API 降低非交互式 LLM 負載的推理成本下一篇模型微調(diào)實戰(zhàn)如何用自有數(shù)據(jù)優(yōu)化Hy-MT1.5-1.8B-2bit翻譯質(zhì)量創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考