摘要:通過可以實現很多有趣的簡潔的控制。這里默認使用到了的一個特性,如果某一個任務成功了過后,其他任務都會被。組合是的內關鍵字,使用的場景是一個。
3.5 compose redux sages書籍完整目錄
基于 redux-thunk 的實現特性,可以做到基于 promise 和遞歸的組合編排,而 redux-saga 提供了更容易的更高級的組合編排方式(當然這一切要歸功于 Generator 特性),這一節的主要內容為:
基于 take Effect 實現更自由的任務編排
fork 和 cancel 實現非阻塞任務
Parallel 和 Race 任務
saga 組合 yield* saga
channels
3.5.1 基于 take Effect 實現更自由的任務編排前面我們使用過 takeEvery helper, 其實底層是通過 take effect 來實現的。通過 take effect 可以實現很多有趣的簡潔的控制。
如果用 takeEvery 實現日志打印,我們可以用:
import { takeEvery } from "redux-saga" import { put, select } from "redux-saga/effects" function* watchAndLog() { yield* takeEvery("*", function* logger(action) { const state = yield select() console.log("action", action) console.log("state after", state) }) }
使用使用 take 過后可以改為:
import { take } from "redux-saga/effects" import { put, select } from "redux-saga/effects" function* watchAndLog() { while (true) { const action = yield take("*") const state = yield select() console.log("action", action) console.log("state after", state) } }
while(true) 的執行并非是死循環,而只是不斷的生成迭代項而已,take Effect 在沒有獲取到對象的 action 時,會停止執行,直到接收到 action 才會執行后面的代碼,然后重新等待
take 和 takeEvery 最大的區別在于 take 是主動獲取 action ,相當于 action = getNextAction() , 而 takeEvery 是消息推送。
基于主動獲取的,可以做到更自由的控制,如下面的兩個例子:
完成了三個任務后,提示恭喜
import { take, put } from "redux-saga/effects" function* watchFirstThreeTodosCreation() { for (let i = 0; i < 3; i++) { const action = yield take("TODO_CREATED") } yield put({type: "SHOW_CONGRATULATION"}) }
登錄和登出邏輯可以放在同一個函數內共享變量
function* loginFlow() { while (true) { yield take("LOGIN") // ... perform the login logic yield take("LOGOUT") // ... perform the logout logic } }
take 最不可思議的地方就是,將 異步的任務用同步的方式來編排 ,使用好 take 能極大的簡化交互邏輯處理
3.5.2 fork 和 cancel 實現非阻塞任務在提非阻塞之前肯定要先要說明什么叫阻塞的代碼。我們看一下下面的例子:
function* generatorFunction() { console.log("start") yield take("action1") console.log("take action1") yield call("api") console.log("call api") yield put({type: "SOME_ACTION"}) console.log("put blabla") }
因為 generator 的特性,必須要等到 take 完成才會輸出 take action1, 同理必須要等待 call api 完成才會輸出 call api, 這就是我們所說的阻塞。
那阻塞會造成什么問題呢?見下面的例子:
一個登錄的例子(這是一段有問題的代碼,可以先研究一下這段代碼問題出在哪兒)
import { take, call, put } from "redux-saga/effects" import Api from "..." function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: "LOGIN_SUCCESS", token}) return token } catch(error) { yield put({type: "LOGIN_ERROR", error}) } } function* loginFlow() { while (true) { const {user, password} = yield take("LOGIN_REQUEST") const token = yield call(authorize, user, password) if (token) { yield call(Api.storeItem, {token}) yield take("LOGOUT") yield call(Api.clearItem, "token") } } }
我們先來分析一下 loginFlow 的流程:
通過 take effect 監聽 login_request action
通過 call effect 來異步獲取 token (call 不僅可以用來調用返回 Promise 的函數,也可以用它來調用其他 Generator 函數,返回結果為調用 Generator return 值)
成功(有 token)
1 過后異步存儲 token
等待 logout action
logout 事件觸發后異步清除 token
然后回到第 0 步
失敗(token === undefined) 回到第 0 步
其中的問題:
一個隱藏的陷阱,在調用 authorize 的時候,如果用戶點擊了頁面中的 logout 按鈕將會沒有反應(此時還沒有執行 take("LOGOUT")) , 也就是被 authorize 阻塞了。
redux-sage 提供了一個叫 fork 的 Effect,可以實現非阻塞的方式,下面我們重新設計上面的登錄例子:
function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: "LOGIN_SUCCESS", token}) } catch(error) { yield put({type: "LOGIN_ERROR", error}) } } function* loginFlow() { while(true) { const {user, password} = yield take("LOGIN_REQUEST") yield fork(authorize, user, password) yield take(["LOGOUT", "LOGIN_ERROR"]) yield call(Api.clearItem("token")) } }
token 的獲取放在了 authorize saga 中,因為 fork 是非阻塞的,不會返回值
authorize 中的 call 和 loginFlow 中的 take 并行調用
這里 take 了兩個 action , take 可以監聽并發的 action ,不管哪個 action 觸發都會執行 call(Api.clearItem...) 并回到 while 開始
在用戶觸發 logout 之前, 如果 authorize 成功,那么 loginFlow 會等待 LOGOUT action
在用戶觸發 logout 之前, 如果 authorize 失敗,那么 loginFlow 會 take("LOGIN_ERROR")
如果在用戶觸發 logout 的時候,authorize 還沒有執行完成,那么會執行后面的語句并回到 while 開始
這個過程中的問題是如果用戶觸發 logout 了,沒法停止 call api.authorize , 并會觸發 LOGIN_SUCCESS 或者 LOGIN_ERROR action 。
redux-saga 提供了 cancel Effect,可以 cancel 一個 fork task
import { take, put, call, fork, cancel } from "redux-saga/effects" // ... function* loginFlow() { while (true) { const {user, password} = yield take("LOGIN_REQUEST") // fork return a Task object const task = yield fork(authorize, user, password) const action = yield take(["LOGOUT", "LOGIN_ERROR"]) if (action.type === "LOGOUT") yield cancel(task) yield call(Api.clearItem, "token") } }
cancel 的了某個 generator, generator 內部會 throw 一個錯誤方便捕獲,generator 內部 可以針對不同的錯誤做不同的處理
import { isCancelError } from "redux-saga" import { take, call, put } from "redux-saga/effects" import Api from "..." function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: "LOGIN_SUCCESS", token}) return token } catch(error) { if(!isCancelError(error)) yield put({type: "LOGIN_ERROR", error}) } }3.5.3 Parallel 和 Race 任務 Parallel
基于 generator 的特性,下面的代碼會按照順序執行
const users = yield call(fetch, "/users"), repos = yield call(fetch, "/repos")
為了優化效率,可以讓兩個任務并行執行
const [users, repos] = yield [ call(fetch, "/users"), call(fetch, "/repos") ]Race
某些情況下可能會對優先完成的任務進行處理,一個很常見的例子就是超時處理,當請求一個 API 超過多少時間過后執行特定的任務。
eg:
import { race, take, put } from "redux-saga/effects" import { delay } from "redux-saga" function* fetchPostsWithTimeout() { const {posts, timeout} = yield race({ posts: call(fetchApi, "/posts"), timeout: call(delay, 1000) }) if (posts) put({type: "POSTS_RECEIVED", posts}) else put({type: "TIMEOUT_ERROR"}) }
這里默認使用到了 race 的一個特性,如果某一個任務成功了過后,其他任務都會被 cancel 。
3.5.4 yield* 組合 sagayield* 是 generator 的內關鍵字,使用的場景是 yield 一個 generaor。
yield* someGenerator 相當于把 someGenerator 的代碼放在當前函數執行,利用這個特性,可以組合使用 saga
function* playLevelOne() { ... } function* playLevelTwo() { ... } function* playLevelThree() { ... } function* game() { const score1 = yield* playLevelOne() put(showScore(score1)) const score2 = yield* playLevelTwo() put(showScore(score2)) const score3 = yield* playLevelThree() put(showScore(score3)) }3.5.5 channels 通過 actionChannel 實現緩存區
先看如下的例子:
import { take, fork, ... } from "redux-saga/effects" function* watchRequests() { while (true) { const {payload} = yield take("REQUEST") yield fork(handleRequest, payload) } } function* handleRequest(payload) { ... }
這個例子是典型的 watch -> fork ,也就是每一個 REQEST 請求都會被并發的執行,現在如果有需求要求 REQUEST 一次只能執行一個,這種情況下可以使用到 actionChannel
通過 actionChannel 修改上例子
import { take, actionChannel, call, ... } from "redux-saga/effects" function* watchRequests() { // 為 REQUEST 創建一個 actionChannel 相當于一個緩沖區 const requestChan = yield actionChannel("REQUEST") while (true) { // 重 channel 中取一個 action const {payload} = yield take(requestChan) // 使用非阻塞的方式調用 request yield call(handleRequest, payload) } } function* handleRequest(payload) { ... }
channel 可以設置緩沖區的大小,如果只想處理最近的5個 action 可以如下設置
import { buffers } from "redux-saga" const requestChan = yield actionChannel("REQUEST", buffers.sliding(5))eventChannel 和外部事件連接起來
eventChannel 不同于 actionChannel,actionChannel 是一個 Effect ,而 eventChannel 是一個工廠函數,可以創建一個自定義的 channel
下面創建一個倒計時的 channel 工廠
import { eventChannel, END } from "redux-saga" function countdown(secs) { return eventChannel(emitter => { const iv = setInterval(() => { secs -= 1 if (secs > 0) { emitter(secs) } else { // 結束 channel emitter(END) clearInterval(iv) } }, 1000); // 返回一個 unsubscribe 方法 return () => { clearInterval(iv) } } ) }
通過 call 使用創建 channel
export function* saga() { const chan = yield call(countdown, value) try { while (true) { // take(END) 會導致直接跳轉到 finally let seconds = yield take(chan) console.log(`countdown: ${seconds}`) } } finally { // 支持外部 cancel saga if (yield cancelled()) { // 關閉 channel chan.close() console.log("countdown cancelled") } else { console.log("countdown terminated") } } }通過 channel 在 saga 之間通信
除了 eventChannel 和 actionChannel,channel 可以不用連接任何事件源,直接創建一個空的 channel,然后手動的 put 事件到 channel 中
以上面的 watch->fork 為基礎,需求改為 ,需要同時并發 3 個request 請求執行:
import { channel } from "redux-saga" import { take, fork, ... } from "redux-saga/effects" function* watchRequests() { // 創建一個空的 channel const chan = yield call(channel) // fork 3 個 worker saga for (var i = 0; i < 3; i++) { yield fork(handleRequest, chan) } while (true) { // 等待 request action const {payload} = yield take("REQUEST") // put payload 到 channel 中 yield put(chan, payload) } } function* handleRequest(chan) { while (true) { const payload = yield take(chan) // process the request } }參考鏈接
http://yelouafi.github.io/redux-saga/docs/advanced/index.html
http://gajus.com/blog/2/the-definitive-guide-to-the-javascript-generators#understanding-the-execution-flow
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/79753.html
摘要:舉例來說一個異步的請求場景,可以如下實現任何異步的邏輯都可以,如等等也可以使用的和。實際上在中,一個就是一個函數。 書籍完整目錄 3.4 redux 異步 showImg(https://segmentfault.com/img/bVyou8); 在大多數的前端業務場景中,需要和后端產生異步交互,在本節中,將詳細講解 redux 中的異步方案以及一些異步第三方組件,內容有: redu...
摘要:數組為新的數組,包含了方法將新的和結合起來,生成一個新的方法返回的新增了一個方法,這個新的方法是改裝過的,也就是封裝了中間件的執行。 書籍完整目錄 3.3 理解 Redux 中間件 showImg(https://segmentfault.com/img/bVymkt); 這一小節會講解 redux 中間件的原理,為下一節講解 redux 異步 action 做鋪墊,主要內容為: ...
摘要:另外一點是組件應該盡量保證獨立性,避免和外部的耦合,使用全局事件造成了和外部事件的耦合。明確的職責分配也增加了應用的確定性明確只有組件能夠知道狀態數據,且是對應部分的數據。 書籍完整目錄 4.2 react patterns 修改 Props Immutable data representation 確定性 在 getInitialState 中使用 props 私有狀態和...
摘要:單向數據流應用的核心設計模式,數據流向自頂向下我也是性子急的人,按照技術界的慣例,在學習一個技術前,首先得說一句。然而的單向數據流的設計讓前端定位變得簡單,頁面的和數據的對應是唯一的我們可以通過定位數據變化就可以定位頁面展現問題。 書籍完整目錄 1.1 React 介紹 showImg(https://segmentfault.com/img/bVvJgS); 1.1.1 React ...
閱讀 667·2021-11-24 09:39
閱讀 2332·2021-11-22 13:54
閱讀 2204·2021-09-23 11:46
閱讀 3250·2019-08-30 15:55
閱讀 2685·2019-08-30 15:54
閱讀 2410·2019-08-30 14:18
閱讀 1550·2019-08-29 14:15
閱讀 2738·2019-08-29 13:49