摘要:在函數(shù)式編程中,異步操作修改全局變量等與函數(shù)外部環(huán)境發(fā)生的交互叫做副作用通常認(rèn)為這些操作是邪惡骯臟的,并且也是導(dǎo)致的源頭。
注:這篇是17年1月的文章,搬運(yùn)自本人 blog...
https://github.com/BuptStEve/...
零、前言在上一篇中介紹了 Redux 的各項(xiàng)基礎(chǔ) api。接著一步一步地介紹如何與 React 進(jìn)行結(jié)合,并從引入過程中遇到的各個痛點(diǎn)引出 react-redux 的作用和原理。
不過目前為止還都是紙上談兵,在日常的開發(fā)中最常見異步操作(如通過 ajax、jsonp 等方法 獲取數(shù)據(jù)),在學(xué)習(xí)完上一篇后你可能依然沒有頭緒。因此本文將深入淺出地對于 redux 的進(jìn)階用法進(jìn)行介紹。
一、中間件(MiddleWare)It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. ———— by Dan Abramov
這是 redux 作者對 middleware 的描述,middleware 提供了一個分類處理 action 的機(jī)會,在 middleware 中你可以檢閱每一個流過的 action,挑選出特定類型的 action 進(jìn)行相應(yīng)操作,給你一次改變 action 的機(jī)會。
說得好像很吊...不過有啥用咧...?
1. 日志應(yīng)用場景[[2]]因?yàn)楦淖?store 的唯一方法就是 dispatch 一個 action,所以有時需要將每次 dispatch 操作都打印出來作為操作日志,這樣一來就可以很容易地看出是哪一次 dispatch 導(dǎo)致了異常。
1.1. 第一次嘗試:強(qiáng)行懟...const action = addTodo("Use Redux"); console.log("dispatching", action); store.dispatch(action); console.log("next state", store.getState());
顯然這種在每一個 dispatch 操作的前后都手動加代碼的方法,簡直讓人不忍直視...
1.2. 第二次嘗試:封裝 dispatch聰明的你一定馬上想到了,不如將上述代碼封裝成一個函數(shù),然后直接調(diào)用該方法。
function dispatchAndLog(store, action) { console.log("dispatching", action); store.dispatch(action); console.log("next state", store.getState()); } dispatchAndLog(store, addTodo("Use Redux"));
矮油,看起來不錯喲。
不過每次使用都需要導(dǎo)入這個額外的方法,一旦不想使用又要全部替換回去,好麻煩啊...
1.3. 第三次嘗試:猴子補(bǔ)丁(Monkey Patch)在此暫不探究為啥叫猴子補(bǔ)丁而不是什么其他補(bǔ)丁。
簡單來說猴子補(bǔ)丁指的就是:以替換原函數(shù)的方式為其添加新特性或修復(fù) bug。
let next = store.dispatch; // 暫存原方法 store.dispatch = function dispatchAndLog(action) { console.log("dispatching", action); let result = next(action); // 應(yīng)用原方法 console.log("next state", store.getState()); return result; };
這樣一來我們就“偷梁換柱”般的為原 dispatch 添加了輸出日志的功能。
1.4. 第四次嘗試:隱藏猴子補(bǔ)丁目前看起來很不錯,然鵝假設(shè)我們又要添加別的一個中間件,那么代碼中將會有重復(fù)的 let next = store.dispatch; 代碼。
對于這個問題我們可以通過參數(shù)傳遞,返回新的 dispatch 來解決。
function logger(store) { const next = store.dispatch; return function dispatchAndLog(action) { console.log("dispatching", action); const result = next(action); // 應(yīng)用原方法 console.log("next state", store.getState()); return result; } } store.dispatch = logger(store); store.dispatch = anotherMiddleWare(store);
注意到最后應(yīng)用中間件的代碼其實(shí)就是一個鏈?zhǔn)降倪^程,所以還可以更進(jìn)一步優(yōu)化綁定中間件的過程。
function applyMiddlewareByMonkeypatching(store, middlewares) { // 因?yàn)閭魅氲氖窃瓕ο笠玫闹担瑂lice 方法會生成一份拷貝, // 所以之后調(diào)用的 reverse 方法不會改變原數(shù)組 middlewares = middlewares.slice(); // 我們希望按照數(shù)組原本的先后順序觸發(fā)各個中間件, // 所以最后的中間件應(yīng)當(dāng)最接近原本的 dispatch, // 就像洋蔥一樣一層一層地包裹原 dispatch middlewares.reverse(); // 在每一個 middleware 中變換 store.dispatch 方法。 middlewares.forEach((middleware) => store.dispatch = middleware(store); ); } // 先觸發(fā) logger,再觸發(fā) anotherMiddleWare 中間件(類似于 koa 的中間件機(jī)制) applyMiddlewareByMonkeypatching(store, [ logger, anotherMiddleWare ]);
so far so good~! 現(xiàn)在不僅隱藏了顯式地緩存原 dispatch 的代碼,而且調(diào)用起來也很優(yōu)雅~,然鵝這樣就夠了么?
1.5. 第五次嘗試:移除猴子補(bǔ)丁注意到,以上寫法仍然是通過 store.dispatch = middleware(store); 改寫原方法,并在中間件內(nèi)部通過 const next = store.dispatch; 讀取當(dāng)前最新的方法。
本質(zhì)上其實(shí)還是 monkey patch,只不過將其封裝在了內(nèi)部,不過若是將 dispatch 方法通過參數(shù)傳遞進(jìn)來,這樣在 applyMiddleware 函數(shù)中就可以暫存 store.dispatch(而不是一次又一次的改寫),豈不美哉?
// 通過參數(shù)傳遞 function logger(store, next) { return function dispatchAndLog(action) { // ... } } function applyMiddleware(store, middlewares) { // ... // 暫存原方法 let dispatch = store.dispatch; // middleware 中通過閉包獲取 dispatch,并且更新 dispatch middlewares.forEach((middleware) => dispatch = middleware(store, dispatch); ); }
接著應(yīng)用函數(shù)式編程的 curry 化(一種使用匿名單參數(shù)函數(shù)來實(shí)現(xiàn)多參數(shù)函數(shù)的方法。),還可以再進(jìn)一步優(yōu)化。(其實(shí)是為了使用 compose 將中間件函數(shù)先組合再綁定)
function logger(store) { return function(next) { return function(action) { console.log("dispatching", action); const result = next(action); // 應(yīng)用原方法 console.log("next state", store.getState()); return result; } } } // -- 使用 es6 的箭頭函數(shù)可以讓代碼更加優(yōu)雅更函數(shù)式... -- const logger = (store) => (next) => (action) => { console.log("dispatching", action); const result = next(action); // 應(yīng)用原方法 console.log("next state", store.getState()); return result; }; function applyMiddleware(store, middlewares) { // ... let dispatch = store.dispatch; middlewares.forEach((middleware) => dispatch = middleware(store)(dispatch); // 注意調(diào)用了兩次 ); // ... }
以上方法離 Redux 中最終的 applyMiddleware 實(shí)現(xiàn)已經(jīng)很接近了,
1.6. 第六次嘗試:組合(compose,函數(shù)式方法)在 Redux 的最終實(shí)現(xiàn)中,并沒有采用我們之前的 slice + reverse 的方法來倒著綁定中間件。而是采用了 map + compose + reduce 的方法。
先來說這個 compose 函數(shù),在數(shù)學(xué)中以下等式十分的自然。
f(g(x)) = (f o g)(x)
f(g(h(x))) = (f o g o h)(x)
用代碼來表示這一過程就是這樣。
// 傳入?yún)?shù)為函數(shù)數(shù)組 function compose(...funcs) { // 返回一個閉包, // 將右邊的函數(shù)作為內(nèi)層函數(shù)執(zhí)行,并將執(zhí)行結(jié)果作為外層函數(shù)再次執(zhí)行 return funcs.reduce((a, b) => (...args) => a(b(...args))); }
不了解 reduce 函數(shù)的人可能對于以上代碼會感到有些費(fèi)解,舉個栗子來說,有函數(shù)數(shù)組 [f, g, h]傳入 compose 函數(shù)執(zhí)行。
首次 reduce 執(zhí)行的結(jié)果是返回一個函數(shù) (...args) => f(g(...args))
接著該函數(shù)作為下一次 reduce 函數(shù)執(zhí)行時的參數(shù) a,而參數(shù) b 是 h
再次執(zhí)行時 h(...args) 作為參數(shù)傳入 a,即最后返回的還是一個函數(shù) (...args) => f(g(h(...args)))
因此最終版 applyMiddleware 實(shí)現(xiàn)中并非依次執(zhí)行綁定,而是采用函數(shù)式的思維,將作用于 dispatch 的函數(shù)首先進(jìn)行組合,再進(jìn)行綁定。(所以要中間件要 curry 化)
// 傳入中間件函數(shù)的數(shù)組 function applyMiddleware(...middlewares) { // 返回一個函數(shù)的原因在 createStore 部分再進(jìn)行介紹 return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] // 保存綁定了 middlewareAPI 后的函數(shù)數(shù)組 const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) // 使用 compose 函數(shù)按照從右向左的順序綁定(執(zhí)行順序是從左往右) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } // store -> { getState } 從傳遞整個 store 改為傳遞部分 api const logger = ({ getState }) => (next) => (action) => { console.log("dispatching", action); const result = next(action); // 應(yīng)用原方法 console.log("next state", getState()); return result; };
綜上如下圖所示整個中間件的執(zhí)行順序是類似于洋蔥一樣首先按照從外到內(nèi)的順序執(zhí)行 dispatch 之前的中間件代碼,在 dispatch(洋蔥的心)執(zhí)行后又反過來,按照從內(nèi)到左外的順序執(zhí)行 dispatch 之后的中間件代碼。
橋都麻袋!
你真的都理解了么?
在之前的實(shí)現(xiàn)中直接傳遞 store,為啥在最終實(shí)現(xiàn)中傳遞的是 middlewareAPI?
middlewareAPI 里的 dispatch 是為啥一個匿名函數(shù)而不直接傳遞 dispatch?
如下列代碼所示,如果在中間件里不用 next 而是調(diào)用 store.dispatch 會怎樣呢?
const logger = (store) => (next) => (action) => { console.log("dispatching", action); // 調(diào)用原始 dispatch,而不是上一個中間件傳進(jìn)來的 const result = store.dispatch(action); // <- 這里 console.log("next state", store.getState()); return result; };1.7. middleware 中調(diào)用 store.dispatch[[6]]
正常情況下,如圖左,當(dāng)我們 dispatch 一個 action 時,middleware 通過 next(action) 一層一層處理和傳遞 action 直到 redux 原生的 dispatch。如果某個 middleware 使用 store.dispatch(action) 來分發(fā) action,就發(fā)生了右圖的情況,相當(dāng)于從外層重新來一遍,假如這個 middleware 一直簡單粗暴地調(diào)用 store.dispatch(action),就會形成無限循環(huán)了。(其實(shí)就相當(dāng)于猴子補(bǔ)丁沒補(bǔ)上,不停地調(diào)用原來的函數(shù))
因此最終版里不是直接傳遞 store,而是傳遞 getState 和 dispatch,傳遞 getState 的原因是可以通過 getState 獲取當(dāng)前狀態(tài)。并且還將 dispatch 用一個匿名函數(shù)包裹 dispatch: (action) => dispatch(action),這樣不但可以防止 dispatch 被中間件修改,而且只要 dispatch 更新了,middlewareAPI 中的 dispatch 也會隨之發(fā)生變化。
1.8. createStore 進(jìn)階在上一篇中我們使用 createStore 方法只用到了它前兩個參數(shù),即 reducer 和 preloadedState,然鵝其實(shí)它還擁有第三個參數(shù) enhancer。
enhancer 參數(shù)可以實(shí)現(xiàn)中間件、時間旅行、持久化等功能,Redux 僅提供了 applyMiddleware 用于應(yīng)用中間件(就是 1.6. 中的那個)。
在日常使用中,要應(yīng)用中間件可以這么寫。
import { createStore, combineReducers, applyMiddleware, } from "redux"; // 組合 reducer const rootReducer = combineReducers({ todos: todosReducer, filter: filterReducer, }); // 中間件數(shù)組 const middlewares = [logger, anotherMiddleWare]; const store = createStore( rootReducer, initialState, applyMiddleware(...middlewares), ); // 如果不需要 initialState 的話也可以忽略 const store = createStore( rootReducer, applyMiddleware(...middlewares), );
在上文 applyMiddleware 的實(shí)現(xiàn)中留了個懸念,就是為什么返回的是一個函數(shù),因?yàn)?enhancer 被定義為一個高階函數(shù),接收 createStore 函數(shù)作為參數(shù)。
/** * 創(chuàng)建一個 redux store 用于保存狀態(tài)樹, * 唯一改變 store 中數(shù)據(jù)的方法就是對其調(diào)用 dispatch * * 在你的應(yīng)用中應(yīng)該只有一個 store,想要針對不同的部分狀態(tài)響應(yīng) action, * 你應(yīng)該使用 combineReducers 將多個 reducer 合并。 * * @param {函數(shù)} reducer 不多解釋了 * @param {對象} preloadedState 主要用于前后端同構(gòu)時的數(shù)據(jù)同步 * @param {函數(shù)} enhancer 很牛逼,可以實(shí)現(xiàn)中間件、時間旅行,持久化等 * ※ Redux 僅提供 applyMiddleware 這個 Store Enhancer ※ * @return {Store} */ export default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function.") } // enhancer 是一個高階函數(shù),接收 createStore 函數(shù)作為參數(shù) return enhancer(createStore)(reducer, preloadedState) } // ... // 后續(xù)內(nèi)容推薦看看參考資料部分的【Redux 莞式教程】 }
總的來說 Redux 有五個 API,分別是:
createStore(reducer, [initialState], enhancer)
combineReducers(reducers)
applyMiddleware(...middlewares)
bindActionCreators(actionCreators, dispatch)
compose(...functions)
createStore 生成的 store 有四個 API,分別是:
getState()
dispatch(action)
subscribe(listener)
replaceReducer(nextReducer)
以上 API 我們還沒介紹的應(yīng)該就剩 bindActionCreators 了。這個 API 其實(shí)就是個語法糖起了方便地給 action creator 綁定 dispatch 的作用。
// 一般寫法 function mapDispatchToProps(dispatch) { return { onPlusClick: () => dispatch(increment()), onMinusClick: () => dispatch(decrement()), }; } // 使用 bindActionCreators import { bindActionCreators } from "redux"; function mapDispatchToProps(dispatch) { return bindActionCreators({ onPlusClick: increment, onMinusClick: decrement, // 還可以綁定更多函數(shù)... }, dispatch); } // 甚至如果定義的函數(shù)輸入都相同的話還能更加簡潔 export default connect( mapStateToProps, // 直接傳一個對象,connect 自動幫你綁定 dispatch { onPlusClick: increment, onMinusClick: decrement }, )(App);二、異步操作
下面讓我們告別干凈的同步世界,進(jìn)入“骯臟”的異步世界~。
在函數(shù)式編程中,異步操作、修改全局變量等與函數(shù)外部環(huán)境發(fā)生的交互叫做副作用(Side Effect)2.1. 通知應(yīng)用場景[[3]]
通常認(rèn)為這些操作是邪惡(evil)骯臟(dirty)的,并且也是導(dǎo)致 bug 的源頭。
因?yàn)榕c之相對的是純函數(shù)(pure function),即對于同樣的輸入總是返回同樣的輸出的函數(shù),使用這樣的函數(shù)很容易做組合、測試等操作,很容易驗(yàn)證和保證其正確性。(它們就像數(shù)學(xué)公式一般準(zhǔn)確)
現(xiàn)在有這么一個顯示通知的應(yīng)用場景,在通知顯示后5秒鐘隱藏該通知。
首先當(dāng)然是編寫 action
顯示:SHOW_NOTIFICATION
隱藏:HIDE_NOTIFICATION
2.1.1. 最直觀的寫法最直觀的寫法就是首先顯示通知,然后使用 setTimeout 在5秒后隱藏通知。
store.dispatch({ type: "SHOW_NOTIFICATION", text: "You logged in." }); setTimeout(() => { store.dispatch({ type: "HIDE_NOTIFICATION" }); }, 5000);
然鵝,一般在組件中尤其是展示組件中沒法也沒必要獲取 store,因此一般將其包裝成 action creator。
// actions.js export function showNotification(text) { return { type: "SHOW_NOTIFICATION", text }; } export function hideNotification() { return { type: "HIDE_NOTIFICATION" }; } // component.js import { showNotification, hideNotification } from "../actions"; this.props.dispatch(showNotification("You just logged in.")); setTimeout(() => { this.props.dispatch(hideNotification()); }, 5000);
或者更進(jìn)一步地先使用 connect 方法包裝。
this.props.showNotification("You just logged in."); setTimeout(() => { this.props.hideNotification(); }, 5000);
到目前為止,我們沒有用任何 middleware 或者別的概念。
2.1.2. 異步 action creator上一種直觀寫法有一些問題
每當(dāng)我們需要顯示一個通知就需要手動先顯示,然后再手動地讓其消失。其實(shí)我們更希望通知到時間后自動地消失。
通知目前沒有自己的 id,所以有些場景下存在競爭條件(race condition),即假如在第一個通知結(jié)束前觸發(fā)第二個通知,當(dāng)?shù)谝粋€通知結(jié)束時,第二個通知也會被提前關(guān)閉。
所以為了解決以上問題,我們可以為通知加上 id,并將顯示和消失的代碼包起來。
// actions.js const showNotification = (text, id) => ({ type: "SHOW_NOTIFICATION", id, text, }); const hideNotification = (id) => ({ type: "HIDE_NOTIFICATION", id, }); let nextNotificationId = 0; export function showNotificationWithTimeout(dispatch, text) { const id = nextNotificationId++; dispatch(showNotification(id, text)); setTimeout(() => { dispatch(hideNotification(id)); }, 5000); } // component.js showNotificationWithTimeout(this.props.dispatch, "You just logged in."); // otherComponent.js showNotificationWithTimeout(this.props.dispatch, "You just logged out.");
為啥 showNotificationWithTimeout 函數(shù)要接收 dispatch 作為第一個參數(shù)呢?
雖然通常一個組件都擁有觸發(fā) dispatch 的權(quán)限,但是現(xiàn)在我們想讓一個外部函數(shù)(showNotificationWithTimeout)來觸發(fā) dispatch,所以需要將 dispatch 作為參數(shù)傳入。
可能你會說如果有一個從其他模塊中導(dǎo)出的單例 store,那么是不是同樣也可以不傳遞 dispatch 以上代碼也可以這樣寫。
// store.js export default createStore(reducer); // actions.js import store from "./store"; // ... let nextNotificationId = 0; export function showNotificationWithTimeout(text) { const id = nextNotificationId++; store.dispatch(showNotification(id, text)); setTimeout(() => { store.dispatch(hideNotification(id)); }, 5000); } // component.js showNotificationWithTimeout("You just logged in."); // otherComponent.js showNotificationWithTimeout("You just logged out.");
這樣看起來似乎更簡單一些,不過墻裂不推薦這樣的寫法。主要的原因是這樣的寫法強(qiáng)制讓 store 成為一個單例。這樣一來要實(shí)現(xiàn)服務(wù)器端渲染(Server Rendering)將十分困難。因?yàn)樵诜?wù)端,為了讓不同的用戶得到不同的預(yù)先獲取的數(shù)據(jù),你需要讓每一個請求都有自己的 store。
并且單例 store 也將讓測試變得困難。當(dāng)測試 action creator 時你將無法自己模擬一個 store,因?yàn)樗鼈兌家昧藦耐獠繉?dǎo)入的那個特定的 store,所以你甚至無法從外部重置狀態(tài)。
2.1.4. redux-thunk 中間件首先聲明 redux-thunk 這種方案對于小型的應(yīng)用來說足夠日常使用,然鵝對于大型應(yīng)用來說,你可能會發(fā)現(xiàn)一些不方便的地方。(例如對于 action 需要組合、取消、競爭等復(fù)雜操作的場景)
首先來明確什么是 thunk...
A thunk is a function that wraps an expression to delay its evaluation.
簡單來說 thunk 就是封裝了表達(dá)式的函數(shù),目的是延遲執(zhí)行該表達(dá)式。不過有啥應(yīng)用場景呢?
目前為止,在上文中的 2.1.2. 異步 action creator 部分,最后得出的方案有以下明顯的缺點(diǎn)
我們必須將 dispatch 作為參數(shù)傳入。
這樣一來任何使用了異步操作的組件都必須用 props 傳遞 dispatch(不管有多深...)。我們也沒法像之前各種同步操作一樣使用 connect 函數(shù)來綁定回調(diào)函數(shù),因?yàn)?showNotificationWithTimeout 函數(shù)返回的不是一個 action。
此外,在日常使用時,我們還需要區(qū)分哪些函數(shù)是同步的 action creator,那些是異步的 action creator。(異步的需要傳 dispatch...)
同步的情況: store.dispatch(actionCreator(payload))
異步的情況: asyncActionCreator(store.dispatch, payload)
計將安出?
其實(shí)問題的本質(zhì)在于 Redux “有眼不識 function”,目前為止 dispatch 函數(shù)接收的參數(shù)只能是 action creator 返回的普通的 action。所以如果我們讓 dispatch 對于 function 網(wǎng)開一面,走走后門潛規(guī)則一下不就行啦~
實(shí)現(xiàn)方式很簡單,想想第一節(jié)介紹的為 dispatch 添加日志功能的過程。
// redux-thunk 源碼 function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === "function") { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
以上就是 redux-thunk 的源碼,就是這么簡單,判斷下如果傳入的 action 是函數(shù)的話,就執(zhí)行這個函數(shù)...(withExtraArgument 是為了添加額外的參數(shù),詳情見 redux-thunk 的 README.md)
這樣一來如果我們 dispatch 了一個函數(shù),redux-thunk 會傳給它一個 dispatch 參數(shù),我們就利用 thunk 解決了組件中不方便獲取 dispatch 的問題。
并且由于 redux-thunk 攔截了函數(shù),也可以防止 reducer 接收到函數(shù)而出現(xiàn)異常。
添加了 redux-thunk 中間件后代碼可以這么寫。
// actions.js // ... let nextNotificationId = 0; export function showNotificationWithTimeout(text) { // 返回一個函數(shù) return function(dispatch) { const id = nextNotificationId++; dispatch(showNotification(id, text)); setTimeout(() => { dispatch(hideNotification(id)); }, 5000); }; } // component.js 像同步函數(shù)一樣的寫法 this.props.dispatch(showNotificationWithTimeout("You just logged in.")); // 或者 connect 后直接調(diào)用 this.props.showNotificationWithTimeout("You just logged in.");2.2. 接口應(yīng)用場景
目前我們對于簡單的延時異步操作的處理已經(jīng)了然于胸了,現(xiàn)在讓我們來考慮一下通過 ajax 或 jsonp 等接口來獲取數(shù)據(jù)的異步場景。
很自然的,我們會發(fā)起一個請求,然后等待請求的響應(yīng)(請求可能成功或是失敗)。
即有基本的三種狀態(tài)和與之對應(yīng)的 action:
請求開始的 action:isFetching 為真,UI 顯示加載界面
{ type: "FETCH_POSTS_REQUEST" }
請求成功的 action:isFetching 為假,隱藏加載界面并顯示接收到的數(shù)據(jù)
{ type: "FETCH_POSTS_SUCCESS", response: { ... } }
請求失敗的 action:isFetching 為假,隱藏加載界面,可能保存失敗信息并在 UI 中顯示出來
{ type: "FETCH_POSTS_FAILURE", error: "Oops" }
按照這個思路,舉一個簡單的栗子。
// Constants const FETCH_POSTS_REQUEST = "FETCH_POSTS_REQUEST"; const FETCH_POSTS_SUCCESS = "FETCH_POSTS_SUCCESS"; const FETCH_POSTS_FAILURE = "FETCH_POSTS_FAILURE"; // Actions const requestPosts = (id) => ({ type: FETCH_POSTS_REQUEST, payload: id, }); const receivePosts = (res) => ({ type: FETCH_POSTS_SUCCESS, payload: res, }); const catchPosts = (err) => ({ type: FETCH_POSTS_FAILURE, payload: err, }); const fetchPosts = (id) => (dispatch, getState) => { dispatch(requestPosts(id)); return api.getData(id) .then(res => dispatch(receivePosts(res))) .catch(error => dispatch(catchPosts(error))); }; // reducer const reducer = (oldState, action) => { switch (action.type) { case FETCH_POSTS_REQUEST: return requestState; case FETCH_POSTS_SUCCESS: return successState; case FETCH_POSTS_FAILURE: return errorState; default: return oldState; } };
盡管這已經(jīng)是最簡單的調(diào)用接口場景,我們甚至還沒寫一行業(yè)務(wù)邏輯代碼,但講道理的話代碼還是比較繁瑣的。
而且其實(shí)代碼是有一定的“套路”的,比如其實(shí)整個代碼都是針對請求、成功、失敗三部分來處理的,這讓我們自然聯(lián)想到 Promise,同樣也是分為 pending、fulfilled、rejected 三種狀態(tài)。
那么這兩者可以結(jié)合起來讓模版代碼精簡一下么?
2.2.1. redux-promise 中間件[[8]]首先開門見山地使用 redux-promise 中間件來改寫之前的代碼看看效果。
// Constants const FETCH_POSTS_REQUEST = "FETCH_POSTS_REQUEST"; // Actions const fetchPosts = (id) => ({ type: FETCH_POSTS_REQUEST, payload: api.getData(id), // payload 為 Promise 對象 }); // reducer const reducer = (oldState, action) => { switch (action.type) { case FETCH_POSTS_REQUEST: // requestState 被“吃掉”了 // 而成功、失敗的狀態(tài)通過 status 來判斷 if (action.status === "success") { return successState; } else { return errorState; } default: return oldState; } };
可以看出 redux-promise 中間件比較激進(jìn)、比較原教旨。
不但將發(fā)起請求的初始狀態(tài)被攔截了(原因見下文源碼),而且使用 action.status 而不是 action.type 來區(qū)分兩個 action 這一做法也值得商榷(個人傾向使用 action.type 來判斷)。
// redux-promise 源碼 import { isFSA } from "flux-standard-action"; function isPromise(val) { return val && typeof val.then === "function"; } export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) // 直接調(diào)用 Promise.then(所以發(fā)不出請求開始的 action) ? action.payload.then( // 自動 dispatch result => dispatch({ ...action, payload: result }), // 自動 dispatch error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); } ) : next(action); }; }
以上是 redux-promise 的源碼,十分簡單。主要邏輯是判斷如果是 Promise 就執(zhí)行 then 方法。此外還根據(jù)是不是 FSA 決定調(diào)用的是 action 本身還是 action.payload 并且對于 FSA 會自動 dispatch 成功和失敗的 FSA。
2.2.2. redux-promise-middleware 中間件盡管 redux-promise 中間件節(jié)省了大量代碼,然鵝它的缺點(diǎn)除了攔截請求開始的 action,以及使用 action.status 來判斷成功失敗狀態(tài)以外,還有就是由此引申出的一個無法實(shí)現(xiàn)的場景————樂觀更新(Optimistic Update)。
樂觀更新比較直觀的栗子就是在微信、QQ等通訊軟件中,發(fā)送的消息立即在對話窗口中展示,如果發(fā)送失敗了,在消息旁邊展示提示即可。由于在這種交互方式中“樂觀”地相信操作會成功,因此稱作樂觀更新。
因?yàn)闃酚^更新發(fā)生在用戶發(fā)起操作時,所以要實(shí)現(xiàn)它,意味著必須有表示用戶初始動作的 action。
因此為了解決這些問題,相對于比較原教旨的 redux-promise 來說,更加溫和派一點(diǎn)的 redux-promise-middleware 中間件應(yīng)運(yùn)而生。先看看代碼怎么說。
// Constants const FETCH_POSTS = "FETCH_POSTS"; // 前綴 // Actions const fetchPosts = (id) => ({ type: FETCH_POSTS, // 傳遞的是前綴,中間件會自動生成中間狀態(tài) payload: { promise: api.getData(id), data: id, }, }); // reducer const reducer = (oldState, action) => { switch (action.type) { case `${FETCH_POSTS}_PENDING`: return requestState; // 可通過 action.payload.data 獲取 id case `${FETCH_POSTS}_FULFILLED`: return successState; case `${FETCH_POSTS}_REJECTED`: return errorState; default: return oldState; } };
如果不需要樂觀更新,fetchPosts 函數(shù)可以更加簡潔。
// 此時初始 actionGET_DATA_PENDING 仍然會觸發(fā),但是 payload 為空。 const fetchPosts = (id) => ({ type: FETCH_POSTS, // 傳遞的是前綴 payload: api.getData(id), // 等價于 payload: { promise: api.getData(id) }, });
相對于 redux-promise 簡單粗暴地直接過濾初始 action,從 reducer 可以看出,redux-promise-middleware 會首先自動觸發(fā)一個 FETCH_POSTS_PENDING 的 action,以此保留樂觀更新的能力。
并且,在狀態(tài)的區(qū)分上,回歸了通過 action.type 來判斷狀態(tài)的“正途”,其中 _PENDING、_FULFILLED、_REJECTED 后綴借用了 Promise 規(guī)范 (當(dāng)然它們是可配置的) 。
后綴可以配置全局或局部生效,例如全局配置可以這么寫。
applyMiddleware( promiseMiddleware({ promiseTypeSuffixes: ["LOADING", "SUCCESS", "ERROR"] }) )
源碼地址點(diǎn)我,類似 redux-promise 也是在中間件中攔截了 payload 中有 Promise 的 action,并主動 dispatch 三種狀態(tài)的 action,注釋也很詳細(xì)在此就不贅述了。
注意:redux-promise、redux-promise-middleware 與 redux-thunk 之間并不是互相替代的關(guān)系,而更像一種補(bǔ)充優(yōu)化。2.3. redux-loop 中間件
簡單小結(jié)一下,Redux 的數(shù)據(jù)流如下所示:
UI => action => action creator => reducer => store => react => v-dom => UI
redux-thunk 的思路是保持 action 和 reducer 簡單純粹,然鵝副作用操作(在前端主要體現(xiàn)在異步操作上)的復(fù)雜度是不可避免的,因此它將其放在了 action creator 步驟,通過 thunk 函數(shù)手動控制每一次的 dispatch。
redux-promise 和 redux-promise-middleware 只是在其基礎(chǔ)上做一些輔助性的增強(qiáng),處理異步的邏輯本質(zhì)上是相同的,即將維護(hù)復(fù)雜異步操作的責(zé)任推到了用戶的身上。
這種實(shí)現(xiàn)方式固然很好理解,而且理論上可以應(yīng)付所有異步場景,但是由此帶來的問題就是模版代碼太多,一旦流程復(fù)雜那么異步代碼就會到處都是,很容易導(dǎo)致出現(xiàn) bug。
因此有一些其他的中間件,例如 redux-loop 就將異步處理邏輯放在 reducer 中。(Redux 的思想借鑒了 Elm,注意并不是“餓了么”,而 Elm 就是將異步處理放在 update(reducer) 層中)。
Synchronous state transitions caused by returning a new state from the reducer in response to an action are just one of all possible effects an action can have on application state.
這種通過響應(yīng)一個 action,在 reducer 中返回一個新 state,從而引起同步狀態(tài)轉(zhuǎn)換的方式,只是在應(yīng)用狀態(tài)中一個 action 能擁有的所有可能影響的一種。(可能沒翻好~歡迎勘誤~)
redux-loop 認(rèn)為許多其他的處理異步的中間件,尤其是通過 action creator 方式實(shí)現(xiàn)的中間件,錯誤地讓用戶認(rèn)為異步操作從根本上與同步操作并不相同。這樣一來無形中鼓勵了中間件以許多特殊的方式來處理異步狀態(tài)。
與之相反,redux-loop 專注于讓 reducer 變得足夠強(qiáng)大以便處理同步和異步操作。在具體實(shí)現(xiàn)上 reducer 不僅能夠根據(jù)特定的 action 決定當(dāng)前的轉(zhuǎn)換狀態(tài),而且還能決定接著發(fā)生的操作。
應(yīng)用中所有行為都可以在一個地方(reducer)中被追蹤,并且這些行為可以輕易地分割和組合。(redux 作者 Dan 開了個至今依然 open 的 issue:Reducer Composition with Effects in JavaScript,討論關(guān)于對 reducer 進(jìn)行分割組合的問題。)
redux-loop 模仿 Elm 的模式,引入了 Effect 的概念,在 reducer 中對于異步等操作使用 Effect 來處理。如下官方示例所示:
import { Effects, loop } from "redux-loop"; function fetchData(id) { return fetch(`endpoint/${id}`) .then((r) => r.json()) .then((data) => ({ type: "FETCH_SUCCESS", payload: data })) .catch((error) => ({ type: "FETCH_FAILURE", payload: error.message })); } function reducer(state, action) { switch(action.type) { case "FETCH_START": return loop( // <- 并沒有直接返回 state,實(shí)際上了返回數(shù)組 [state, effect] { ...state, loading: true }, Effects.promise(fetchData, action.payload.id) ); case "FETCH_SUCCESS": return { ...state, loading: false, data: action.payload }; case "FETCH_FAILURE": return { ...state, loading: false, errorMessage: action.payload }; } }
雖然這個想法很 Elm 很函數(shù)式,不過由于修改了 reducer 的返回類型,這樣一來會導(dǎo)致許多已有的 Api 和第三方庫無法使用,甚至連 redux 庫中的 combineReducers 方法都需要使用 redux-loop 提供的定制版本。因此這也是 redux-loop 最終無法轉(zhuǎn)正的原因:
"If a solution doesn’t work with vanilla combineReducers(), it won’t get into Redux core."三、復(fù)雜異步操作 3.1. 更復(fù)雜的通知場景[[9]]
讓我們的思路重新回到通知的場景,之前的代碼實(shí)現(xiàn)了:
展示一個通知并在數(shù)秒后消失
可以同時展示多個通知。
現(xiàn)在假設(shè)可親可愛的產(chǎn)品又提出了新需求:
同時不展示多于3個的通知
如果已有3個通知正在展示,此時的新通知請求將排隊(duì)延遲展示。
“這個實(shí)現(xiàn)不了...”(全文完)
這個當(dāng)然可以實(shí)現(xiàn),只不過如果只用之前的 redux-thunk 實(shí)現(xiàn)起來會很麻煩。例如可以在 store 中增加兩個數(shù)組分別表示當(dāng)前展示列表和等待隊(duì)列,然后在 reducer 中手動控制各個狀態(tài)時這倆數(shù)組的變化。
3.2. redux-saga 中間件首先來看看使用了 redux-saga 后代碼會變成怎樣~(代碼來自生產(chǎn)環(huán)境的某 app)
function* toastSaga() { const MaxToasts = 3; const ToastDisplayTime = 4000; let pendingToasts = []; // 等待隊(duì)列 let activeToasts = []; // 展示列表 function* displayToast(toast) { if ( activeToasts >= MaxToasts ) { throw new Error("can"t display more than " + MaxToasts + " at the same time"); } activeToasts = [...activeToasts, toast]; // 新增通知到展示列表 yield put(events.toastDisplayed(toast)); // 展示通知 yield call(delay, ToastDisplayTime); // 通知的展示時間 yield put(events.toastHidden(toast)); // 隱藏通知 activeToasts = _.without(activeToasts,toast); // 從展示列表中刪除 } function* toastRequestsWatcher() { while (true) { const event = yield take(Names.TOAST_DISPLAY_REQUESTED); // 監(jiān)聽通知展示請求 const newToast = event.data.toastData; pendingToasts = [...pendingToasts, newToast]; // 將新通知放入等待隊(duì)列 } } function* toastScheduler() { while (true) { if (activeToasts.length < MaxToasts && pendingToasts.length > 0) { const [firstToast,...remainingToasts] = pendingToasts; pendingToasts = remainingToasts; yield fork(displayToast, firstToast); // 取出隊(duì)頭的通知進(jìn)行展示 // 增加一點(diǎn)延遲,這樣一來兩個并發(fā)的通知請求不會同時展示 yield call(delay, 300); } else { yield call(delay, 50); } } } yield [ call(toastRequestsWatcher), call(toastScheduler) ] } // reducer const reducer = (state = {toasts: []}, event) => { switch (event.name) { case Names.TOAST_DISPLAYED: return { ...state, toasts: [...state.toasts, event.data.toastData] }; case Names.TOAST_HIDDEN: return { ...state, toasts: _.without(state.toasts, event.data.toastData) }; default: return state; } };
先不要在意代碼的細(xì)節(jié),簡單分析一下上述代碼的邏輯:
store 上只有一個 toasts 節(jié)點(diǎn),且 reducer 十分干凈
排隊(duì)等具體的業(yè)務(wù)邏輯都放到了 toastSaga 函數(shù)中
displayToast 函數(shù)負(fù)責(zé)單個通知的展示和消失邏輯
toastRequestsWatcher 函數(shù)負(fù)責(zé)監(jiān)聽請求,將其加入等待隊(duì)列
toastScheduler 函數(shù)負(fù)責(zé)將等待隊(duì)列中的元素加入展示列表
基于這樣邏輯分離的寫法,還可以繼續(xù)滿足更加復(fù)雜的需求:
如果在等待隊(duì)列中有太多通知,動態(tài)減少通知的展示時間
根據(jù)窗口大小的變化,改變最多展示的通知數(shù)量
...
redux-saga V.S. redux-thunk[[11]]
redux-saga 的優(yōu)點(diǎn):
易于測試,因?yàn)?redux-saga 中所有操作都 yield 簡單對象,所以測試只要判斷返回的對象是否正確即可,而測試 thunk 通常需要你在測試中引入一個 mockStore
redux-saga 提供了一些方便的輔助方法。(takeLatest、cancel、race 等)
在 saga 函數(shù)中處理業(yè)務(wù)邏輯和異步操作,這樣一來通常代碼更加清晰,更容易增加和更改功能
使用 ES6 的 generator,以同步的方式寫異步代碼
redux-saga 的缺點(diǎn):
generator 的語法("又是 * 又是 yield 的,很難理解誒~")
學(xué)習(xí)曲線陡峭,有許多概念需要學(xué)習(xí)("fork、join 這不是進(jìn)程的概念么?這些 yield 是以什么順序執(zhí)行的?")
API 的穩(wěn)定性,例如新增了 channel 特性,并且社區(qū)也不是很大。
通知場景各種中間件寫法的完整代碼可以看這里3.3. 理解 Saga Pattern[[14]] 3.3.1. Saga 是什么
Sagas 的概念來源于這篇論文,該論文從數(shù)據(jù)庫的角度談了 Saga Pattern。
Saga 就是能夠滿足特定條件的長事務(wù)(Long Lived Transaction)
暫且不提這個特定條件是什么,首先一般學(xué)過數(shù)據(jù)庫的都知道事務(wù)(Transaction)是啥~
如果不知道的話可以用轉(zhuǎn)賬來理解,A 轉(zhuǎn)給 B 100 塊錢的操作需要保證完成 A 先減 100 塊錢然后 B 加 100 塊錢這兩個操作,這樣才能保證轉(zhuǎn)賬前后 A 和 B 的存款總額不變。3.3.2. 長事務(wù)的問題
如果在給 B 加 100 塊錢的過程中發(fā)生了異常,那么就要返回轉(zhuǎn)賬前的狀態(tài),即給 A 再加上之前減的 100 塊錢(不然錢就不翼而飛了),這樣的一次轉(zhuǎn)賬(要么轉(zhuǎn)成功,要么失敗返回轉(zhuǎn)賬前的狀態(tài))就是一個事務(wù)。
長事務(wù)顧名思義就是一個長時間的事務(wù)。
一般來說是通過給正在進(jìn)行事務(wù)操作的對象加鎖,來保證事務(wù)并發(fā)時不會出錯。
例如 A 和 B 都給 C 轉(zhuǎn) 100 塊錢。
如果不加鎖,極端情況下 A 先轉(zhuǎn)給 C 100 塊,而 B 讀取到了 C 轉(zhuǎn)賬前的數(shù)值,這時 B 的轉(zhuǎn)賬會覆蓋 A 的轉(zhuǎn)賬,C 只加了 100 塊錢,另 100 塊不翼而飛了。
如果加了鎖,這時 B 的轉(zhuǎn)賬會等待 A 的轉(zhuǎn)賬完成后再進(jìn)行。所以 C 能正確地收到 200 塊錢。
以押尾光太郎的指彈演奏會售票舉例,在一個售票的時間段后,最終舉辦方需要確定售票數(shù)量,這就是一個長事務(wù)。
然鵝,對于長事務(wù)來說總不能一直鎖住對應(yīng)數(shù)據(jù)吧?
為了解決這個問題,假設(shè)一個長事務(wù):T,
可以被拆分成許多相互獨(dú)立的子事務(wù)(subtransaction):t_1 ~ t_n。
以上述押尾桑的表演為例,每個 t 就是一筆售票記錄。
假如每次購票都一次成功,且沒有退票的話,整個流程就如下圖一般被正常地執(zhí)行。
那假如有某次購票失敗了怎么辦?
3.3.3. Saga 的特殊條件A LLT is a saga if it can be written as a sequence of transactions that can be interleaved with other transactions.
Saga 就是能夠被寫成事務(wù)的序列,并且能夠在執(zhí)行過程中被其他事務(wù)插入執(zhí)行的長事務(wù)。
Saga 通過引入補(bǔ)償事務(wù)(Compensating Transaction)的概念,解決事務(wù)失敗的問題。
即任何一個 saga 中的子事務(wù) t_i,都有一個補(bǔ)償事務(wù) c_i 負(fù)責(zé)將其撤銷(undo)。
注意是撤銷該子事務(wù),而不是回到子事務(wù)發(fā)生前的時間點(diǎn)。
根據(jù)以上邏輯,可以推出很簡單的公式:
Saga 如果全部執(zhí)行成功那么子事務(wù)序列看起來像這樣:t_1, t_2, t_3, ..., t_n
Saga 如果執(zhí)行全部失敗那么子事務(wù)序列看起來像這樣:t_1, t_2, t_3, ..., t_n, c_n, ..., c_1
注意到圖中的 c_4 其實(shí)并沒有必要,不過因?yàn)槊看纬蜂N執(zhí)行都應(yīng)該是冪等(Idempotent)的,所以也不會出錯。
篇幅有限在此就不繼續(xù)深入介紹...
推薦看看從分布式系統(tǒng)方面講 Saga Pattern 的視頻:GOTO 2015 ? Applying the Saga Pattern ? Caitie McCaffrey
MSDN 的文章:A Saga on Sagas
3.4. 響應(yīng)式編程(Reactive Programming)[[15]]redux-saga 中間件基于 Sagas 的理論,通過監(jiān)聽 action,生成對應(yīng)的各種子 saga(子事務(wù))解決了復(fù)雜異步問題。
而接下來要介紹的 redux-observable 中間件背后的理論是響應(yīng)式編程(Reactive Programming)。
In computing, reactive programming is a programming paradigm oriented around data flows and the propagation of change.
簡單來說,響應(yīng)式編程是針對異步數(shù)據(jù)流的編程并且認(rèn)為:萬物皆流(Everything is Stream)。
流(Stream)就是隨著時間的流逝而發(fā)生的一系列事件。
例如點(diǎn)擊事件的示意圖就是這樣。
用字符表示【上上下下左右左右BABA】可以像這樣。(注意順序是從左往右)
--上----上-下---下----左---右-B--A-B--A---X-|-> 上, 下, 左, 右, B, A 是數(shù)據(jù)流發(fā)射的值 X 是數(shù)據(jù)流發(fā)射的錯誤 | 是完成信號 ---> 是時間線
那么我們要根據(jù)一個點(diǎn)擊流來計算點(diǎn)擊次數(shù)的話可以這樣。(一般響應(yīng)式編程庫都會提供許多輔助方法如 map、filter、scan 等)
clickStream: ---c----c--c----c------c--> map(c becomes 1) ---1----1--1----1------1--> scan(+) counterStream: ---1----2--3----4------5-->
如上所示,原始的 clickStream 經(jīng)過 map 后產(chǎn)生了一個新的流(注意原始流不變),再對該流進(jìn)行 scan(+) 的操作就生成了最終的 counterStream。
再來個栗子~,假設(shè)我們需要從點(diǎn)擊流中得到關(guān)于雙擊的流(250ms 以內(nèi)),并且對于大于兩次的點(diǎn)擊也認(rèn)為是雙擊。先想一想應(yīng)該怎么用傳統(tǒng)的命令式、狀態(tài)式的方式來寫,然后再想想用流的思考方式又會是怎么樣的~。
這里我們用了以下輔助方法:
節(jié)流:throttle(250ms),將原始流在 250ms 內(nèi)的所有數(shù)據(jù)當(dāng)作一次事件發(fā)射
緩沖(不造翻譯成啥比較好):buffer,將 250ms 內(nèi)收集的數(shù)據(jù)放入一個數(shù)據(jù)包裹中,然后發(fā)射這些包裹
映射:map,這個不解釋
過濾:filter,這個也不解釋
更多內(nèi)容請繼續(xù)學(xué)習(xí) RxJS。
3.5. redux-observable 中間件[[16]]redux-observable 就是一個使用 RxJS 監(jiān)聽每個 action 并將其變成可觀測流(observable stream)的中間件。
其中最核心的概念叫做 epic,就是一個監(jiān)聽流上 action 的函數(shù),這個函數(shù)在接收 action 并進(jìn)行一些操作后可以再返回新的 action。
At the highest level, epics are “actions in, actions out”
redux-observable 通過在后臺執(zhí)行 .subscribe(store.dispatch) 實(shí)現(xiàn)監(jiān)聽。
Epic 像 Saga 一樣也是 Long Lived,即在應(yīng)用初始化時啟動,持續(xù)運(yùn)行到應(yīng)用關(guān)閉。雖然 redux-observable 是一個中間件,但是類似于 redux-saga,可以想象它就像新開的進(jìn)/線程,監(jiān)聽著 action。
在這個運(yùn)行流程中,epic 不像 thunk 一樣攔截 action,或阻止、改變?nèi)魏卧?redux 的生命周期的其他東西。這意味著每個 dispatch 的 action 總會經(jīng)過 reducer 處理,實(shí)際上在 epic 監(jiān)聽到 action 前,action 已經(jīng)被 reducer 處理過了。
所以 epic 的功能就是監(jiān)聽所有的 action,過濾出需要被監(jiān)聽的部分,對其執(zhí)行一些帶副作用的異步操作,然后根據(jù)你的需要可以再發(fā)射一些新的 action。
舉個自動保存的栗子,界面上有一個輸入框,每次用戶輸入了數(shù)據(jù)后,去抖動后進(jìn)行自動保存,并在向服務(wù)器發(fā)送請求的過程中顯示正在保存的 UI,最后顯示成功或失敗的 UI。
使用 redux-observable 中間件編寫代碼,可以僅用十幾行關(guān)鍵代碼就實(shí)現(xiàn)上述功能。
import { Observable } from "rxjs/Observable"; import "rxjs/add/observable/dom/ajax"; import "rxjs/add/observable/of"; import "rxjs/add/operator/catch"; import "rxjs/add/operator/debounceTime"; import "rxjs/add/operator/map"; import "rxjs/add/operator/mergeMap"; import "rxjs/add/operator/startWith"; import { isSaving, savingSuccess, savingError, } from "../actions/autosave-actions.js"; const saveField = (action$) => // 一般在變量后面加 $ 表示是個 stream action$ .ofType("SAVE_FIELD") // 使用 ofType 監(jiān)聽 "SAVE_FIELD" action .debounceTime(500) // 防抖動 // 即 map + mergeAll 因?yàn)楫惒綄?dǎo)致 map 后有多個流需要 merge .mergeMap(({ payload }) => Observable.ajax({ // 發(fā)起請求 method: "PATCH", url: payload.url, body: JSON.stringify(payload), }) .map(res => savingSuccess(res)) // 發(fā)出成功的 action .catch(err => Observable.of(savingError(err))) // 捕捉錯誤并發(fā)出 action .startWith(isSaving()) // 發(fā)出請求開始的 action ); export default saveField;
篇幅有限在此就不繼續(xù)深入介紹...
關(guān)于 redux-observable 的前世今生推薦看看 Netfix 工程師的這個視頻:Netflix JavaScript Talks - RxJS + Redux + React = Amazing!
如果覺得看視頻聽英語麻煩的話知乎有人翻譯了...
RxJS + Redux + React = Amazing!(譯一)
RxJS + Redux + React = Amazing!(譯二)
四、總結(jié)本文從為 Redux 應(yīng)用添加日志功能(記錄每一次的 dispatch)入手,引出 redux 的中間件(middleware)的概念和實(shí)現(xiàn)方法。
接著從最簡單的 setTimeout 的異步操作開始,通過對比各種實(shí)現(xiàn)方法引出 redux 最基礎(chǔ)的異步中間件 redux-thunk。
針對 redux-thunk 使用時模版代碼過多的問題,又介紹了用于優(yōu)化的 redux-promise 和 redux-promise-middleware 兩款中間件。
由于本質(zhì)上以上中間件都是基于 thunk 的機(jī)制來解決異步問題,所以不可避免地將維護(hù)異步狀態(tài)的責(zé)任推給了開發(fā)者,并且也因?yàn)殡y以測試的原因。在復(fù)雜的異步場景下使用起來難免力不從心,容易出現(xiàn) bug。
所以還簡單介紹了一下將處理副作用的步驟放到 reducer 中并通過 Effect 進(jìn)行解決的 redux-loop 中間件。然鵝因?yàn)槠錈o法使用官方 combineReducers 的原因而無法被納入 redux 核心代碼中。
此外社區(qū)根據(jù) Saga 的概念,利用 ES6 的 generator 實(shí)現(xiàn)了 redux-saga 中間件。雖然通過 saga 函數(shù)將業(yè)務(wù)代碼分離,并且可以用同步的方式流程清晰地編寫異步代碼,但是較多的新概念和 generator 的語法可能讓部分開發(fā)者望而卻步。
同樣是基于觀察者模式,通過監(jiān)聽 action 來處理異步操作的 redux-observable 中間件,背后的思想是響應(yīng)式編程(Reactive Programming)。類似于 saga,該中間件提出了 epic 的概念來處理副作用。即監(jiān)聽 action 流,一旦監(jiān)聽到目標(biāo) action,就處理相關(guān)副作用,并且還可以在處理后再發(fā)射新的 action,繼續(xù)進(jìn)行處理。盡管在處理異步流程時同樣十分方便,但對于開發(fā)者的要求同樣很高,需要開發(fā)者學(xué)習(xí)關(guān)于函數(shù)式的相關(guān)理論。
五、參考資料Redux 英文原版文檔
Redux 中文文檔
Dan Abramov - how to dispatch a redux action with a timeout
阮一峰 - Redux 入門教程(二):中間件與異步操作
Redux 莞式教程
redux middleware 詳解
Thunk 函數(shù)的含義和用法
Redux異步方案選型
Sebastien Lorber - how to dispatch a redux action with a timeout
Sagas 論文
Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES7 async/await
Redux-saga 英文文檔
Redux-saga 中文文檔
Saga Pattern 在前端的應(yīng)用
The introduction to Reactive Programming you"ve been missing
Epic Middleware in Redux
以上 to be continued...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96875.html
摘要:不斷更新筆記效果有待進(jìn)一步完善搭建一個基于的多人功能登錄注冊上傳頭像發(fā)表博文發(fā)表留言參考自前端部分以的腳手架搭起的全家桶后端采用開發(fā)環(huán)境開發(fā)環(huán)境要求以上目錄結(jié)構(gòu)如何運(yùn)行后端默認(rèn)配置在中請確保本地端口默認(rèn)可用發(fā)布到目錄中默 Full-stack-blog(不斷更新筆記) 效果Demo(有待進(jìn)一步完善)搭建一個基于Koa2的多人blog功能(登錄注冊上傳頭像,發(fā)表博文,發(fā)表留言)參考自ht...
摘要:今天給大家?guī)砹撕贸绦騿T實(shí)戰(zhàn)項(xiàng)目商城管理后臺。配合項(xiàng)目學(xué)習(xí)會讓你更快掌握它的使用方法下面就來看看好程序員這套實(shí)戰(zhàn)項(xiàng)目課程介紹好程序員項(xiàng)目本項(xiàng)目是一個使用開發(fā)的商城系統(tǒng)的管理后臺,里面登錄判斷,接口調(diào)用,數(shù)據(jù)展示和編輯,文件上傳等后臺功能。 眾所周知,項(xiàng)目經(jīng)驗(yàn)對于一個程序員變得越來越重要。在面...
摘要:二基礎(chǔ)就是一個普通的。其他屬性用來傳遞此次操作所需傳遞的數(shù)據(jù),對此不作限制,但是在設(shè)計時可以參照標(biāo)準(zhǔn)。對于異步操作則將其放到了這個步驟為添加一個變化監(jiān)聽器,每當(dāng)?shù)臅r候就會執(zhí)行,你可以在回調(diào)函數(shù)中使用來得到當(dāng)前的。 注:這篇是16年10月的文章,搬運(yùn)自本人 blog...https://github.com/BuptStEve/... 零、環(huán)境搭建 參考資料 英文原版文檔 中文文檔 墻...
摘要:前端日報精選大前端公共知識梳理這些知識你都掌握了嗎以及在項(xiàng)目中的實(shí)踐深入貫徹閉包思想,全面理解閉包形成過程重溫核心概念和基本用法前端學(xué)習(xí)筆記自定義元素教程阮一峰的網(wǎng)絡(luò)日志中文譯回調(diào)是什么鬼掘金譯年,一個開發(fā)者的好習(xí)慣知乎專 2017-06-23 前端日報 精選 大前端公共知識梳理:這些知識你都掌握了嗎?Immutable.js 以及在 react+redux 項(xiàng)目中的實(shí)踐深入貫徹閉包思...
摘要:全家桶仿簡書部分功能前言前段時間接觸了下,一直想要自己寫一個小練手。在眾多應(yīng)用中,考慮之后選擇了簡書來模仿,這段時間就利用了工作之余的時間進(jìn)行開發(fā)。在這里簡單敘述一下我仿簡書部分布局以及功能實(shí)現(xiàn)的過程,僅做學(xué)習(xí)用途。 React-全家桶仿簡書部分功能 前言 前段時間接觸了下React,一直想要自己寫一個小Demo練手。在眾多應(yīng)用中,考慮之后選擇了簡書來模仿,這段時間就利用了工作之余的時...
閱讀 2844·2021-11-19 11:35
閱讀 2591·2021-11-02 14:40
閱讀 1412·2021-09-04 16:48
閱讀 3019·2019-08-30 15:55
閱讀 1773·2019-08-30 13:11
閱讀 1965·2019-08-29 11:12
閱讀 1101·2019-08-27 10:52
閱讀 3169·2019-08-26 18:36