Redux 的第一次代碼提交是在 2015 年 5 月底(也就是一年多前的樣子),那個(gè)時(shí)候 React 的最佳實(shí)踐還不是明晰训裆,作為一個(gè) View 層,有人會(huì)用 backbone 甚至是 angular 和它搭配廉邑,也有人覺(jué)得這層 View 功能已經(jīng)足夠強(qiáng)大埂淮,簡(jiǎn)單地搭配一些 utils 就直接上。后來(lái)便有了 FLUX 的演講伞梯,React 社區(qū)開(kāi)始注意到這種新的類(lèi)似函數(shù)式編程的理念玫氢,Redux 也作為 FLUX 的一種變體開(kāi)始受到關(guān)注,再后來(lái)順理成章地得到 React 的『欽點(diǎn)』谜诫,作者也加入了 Facebook 從事 React 的開(kāi)發(fā)漾峡。生態(tài)圈經(jīng)過(guò)了這一年的成熟,現(xiàn)在很多第三方庫(kù)已經(jīng)非常完善猜绣,所以這里想介紹一下目前 Redux 的一些最佳實(shí)踐灰殴。
1. 復(fù)習(xí)一下 Redux 的基本概念
首先我們復(fù)習(xí)一下 Redux 的基本概念, 如果你已經(jīng)很熟悉了掰邢,就直接跳過(guò)這一章吧牺陶。
Redux 把界面視為一種狀態(tài)機(jī),界面里的所有狀態(tài)辣之、數(shù)據(jù)都可以由一個(gè)狀態(tài)樹(shù)來(lái)描述掰伸。所以對(duì)于界面的任何變更都簡(jiǎn)化成了狀態(tài)機(jī)的變化:
(State, Input) => NewState
這其中切分成了三個(gè)階段:
- action
- reducer
- store
所謂的 action,就是用一個(gè)對(duì)象描述發(fā)生了什么怀估,Redux 中一般使用一個(gè)純函數(shù)狮鸭,即 actionCreator
來(lái)生成 action 對(duì)象合搅。
// actionCreator => action
// 這是一個(gè)純函數(shù),只是簡(jiǎn)單地返回 action
function somethingHappened(data){
return {
type: 'foo',
data: data
}
}
隨后這個(gè) action 對(duì)象和當(dāng)前的狀態(tài)樹(shù) state 會(huì)被傳入到 reducer 中歧蕉,產(chǎn)生一個(gè)新的 state
//reducer(action, state) => newState
function reducer(action, state){
switch(action.type){
case 'foo':
return { data: data };
default:
return state;
}
}
store 的作用就是儲(chǔ)存 state灾部,并且監(jiān)聽(tīng)其變化。
簡(jiǎn)單地說(shuō)就是你可以這樣產(chǎn)生一個(gè) store :
import { createStore } from 'redux'
//這里的 reducer 就是剛才的 Reducer 函數(shù)
let store = createStore(reducer);
然后你可以通過(guò) dispatch 一個(gè) action 來(lái)讓它改變狀態(tài):
store.getState();//{}
store.dispatch(somethingHappened('aaa'));
store.getState(); // { data: 'aaa'}
好了惯退,這就是 Redux 的全部功能赌髓。對(duì)的,它就是如此簡(jiǎn)單催跪,以至于它本體只有 3KB 左右的代碼锁蠕,因?yàn)樗皇菍?shí)現(xiàn)了一個(gè)簡(jiǎn)單的狀態(tài)機(jī)而已,任何稍微有點(diǎn)編程能力的人都能很快寫(xiě)出這個(gè)東西懊蒸。至于和 React 的結(jié)合荣倾,則需要 react-redux 這個(gè)庫(kù),這里我們就不講怎么用了骑丸。
2. Redux的一些痛點(diǎn)
大體上舌仍,Redux 的數(shù)據(jù)流是這樣的:
界面 => action => reducer => store => react => virtual dom => 界面
每一步都很純凈,看起來(lái)很美好對(duì)吧者娱?對(duì)于一些小小的嘗試性質(zhì)的 DEMO 來(lái)說(shuō)確實(shí)很美好抡笼。但其實(shí)當(dāng)應(yīng)用變得越來(lái)越大的時(shí)候,這其中存在諸多問(wèn)題:
- 如何優(yōu)雅地寫(xiě)異步代碼黄鳍?(從簡(jiǎn)單的數(shù)據(jù)請(qǐng)求到復(fù)雜的異步邏輯)
- 狀態(tài)樹(shù)的結(jié)構(gòu)應(yīng)該怎么設(shè)計(jì)推姻?
- 如何避免重復(fù)冗余的 actionCreator?
- 狀態(tài)樹(shù)中的狀態(tài)越來(lái)越多框沟,結(jié)構(gòu)越來(lái)越復(fù)雜的時(shí)候藏古,和 react 的組件映射如何避免混亂?
- 每次狀態(tài)的細(xì)微變化都會(huì)生成全新的 state 對(duì)象忍燥,其中大部分無(wú)變化的數(shù)據(jù)是不用重新克隆的拧晕,這里如何提高性能?
你以為我會(huì)在下面一一介紹這些問(wèn)題是怎么解決的梅垄?還真不是厂捞,這里大部分問(wèn)題的回答都可以在官方文檔中看到: 技巧 | Redux 中文文檔 ,文檔里講得已經(jīng)足夠詳細(xì)(有些甚至詳細(xì)得有些啰嗦了)队丝。所以下面只挑 Redux 生態(tài)圈里幾個(gè)比較成熟且流行的組件來(lái)講講靡馁。
3. Redux 異步控制
官方文檔里介紹了一種很樸素的異步控制中間件 redux-thunk (如果你還不了解中間件的話請(qǐng)看 Middleware | Redux 中文文檔 ,事實(shí)上 redux-thunk 的代碼很簡(jiǎn)單机久,簡(jiǎn)單到只有幾行代碼:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
它其實(shí)只干了一件事情臭墨,判斷 actionCreator 返回的是不是一個(gè)函數(shù),如果不是的話膘盖,就很普通地傳給下一個(gè)中間件(或者 reducer)胧弛;如果是的話尤误,那么把 dispatch 、 getState 结缚、 extraArgument 作為參數(shù)傳入這個(gè)函數(shù)里损晤,實(shí)現(xiàn)異步控制。
比如我們可以這樣寫(xiě):
//普通action
function foo(){
return {
type: 'foo',
data: 123
}
}
//異步action
function fooAsync(){
return dispatch => {
setTimeout(_ => dispatch(123), 3000);
}
}
但這種簡(jiǎn)單的異步解決方法在應(yīng)用變得復(fù)雜的時(shí)候红竭,并不能滿足需求沉馆,反而會(huì)使 action 變得十分混亂。
舉個(gè)比較簡(jiǎn)單的例子德崭,我們現(xiàn)在要實(shí)現(xiàn)『圖片上傳』功能,用戶(hù)點(diǎn)擊開(kāi)始上傳之后揖盘,顯示出加載效果眉厨,上傳完畢之后,隱藏加載效果兽狭,并顯示出預(yù)覽圖憾股;如果發(fā)生錯(cuò)誤,那么顯示出錯(cuò)誤信息箕慧,并且在2秒后消失服球。
用普通的 redux-thunk 是這樣寫(xiě)的:
function upload(data){
return dispatch => {
// 顯示出加載效果
dispatch({ type: 'SHOW_WAITING_MODAL' });
// 開(kāi)始上傳
api.upload(data)
.then(res => {
// 成功,隱藏加載效果颠焦,并顯示出預(yù)覽圖
dispatch({ type: 'PRELOAD_IMAGES', data: res.images });
dispatch({ type: 'HIDE_WAITING_MODAL' });
})
.catch(err => {
// 錯(cuò)誤斩熊,隱藏加載效果,顯示出錯(cuò)誤信息伐庭,2秒后消失
dispatch({ type: 'SHOW_ERROR', data: err });
dispatch({ type: 'HIDE_WAITING_MODAL' });
setTimeout(_ => dispatch({ type: 'HIDE_ERROR' }), 2000);
})
}
}
這里的問(wèn)題在于粉渠,一個(gè)異步的 upload action 執(zhí)行過(guò)程中會(huì)產(chǎn)生好幾個(gè)新的 action,更可怕的是這些新的 action 也是包含邏輯的(比如要判斷是否錯(cuò)誤)圾另,這直接導(dǎo)致異步代碼中到處都是 dispatch(action) 霸株,是很不可控的情況。如果還要進(jìn)一步考慮取消集乔、超時(shí)去件、隊(duì)列的情況,就更加混亂了扰路。
所以我們需要更強(qiáng)大的異步流控制尤溜,這就是 GitHub - yelouafi/redux-saga: An alternative side effect model for Redux apps 。下面我們來(lái)看看如果換成 redux-saga 的話會(huì)怎么樣:
import { take, put, call, delay } from 'redux-saga/effects'
// 上傳的異步流
function *uploadFlow(action) {
// 顯示出加載效果
yield put({ type: 'SHOW_WAITING_MODAL' });
// 簡(jiǎn)單的 try-catch
try{
const response = yield call(api.upload, action.data);
yield put({ type: 'PRELOAD_IMAGES', data: response.images });
yield put({ type: 'HIDE_WAITING_MODAL' });
}catch(err){
yield put({ type: 'SHOW_ERROR', data: err });
yield put({ type: 'HIDE_WAITING_MODAL' });
yield delay(2000);
yield put({ type: 'HIDE_ERROR' });
}
}
function* watchUpload() {
yield* takeEvery('BEGIN_REQUEST', uploadFlow)
}
是不是規(guī)整很多呢幼衰?redux-saga 允許我們使用簡(jiǎn)單的 try-catch 來(lái)進(jìn)行錯(cuò)誤處理靴跛,更神奇的是竟然可以直接使用 delay 來(lái)替代 setTimeout 這種會(huì)造成回調(diào)和嵌套的不優(yōu)雅的方法。
本質(zhì)上講渡嚣,redux-sage 提供了一系列的『副作用(side-effects)方法』梢睛,比如以下幾個(gè):
- put (產(chǎn)生一個(gè) action)
- call (阻塞地調(diào)用一個(gè)函數(shù))
- fork (非阻塞地調(diào)用一個(gè)函數(shù))
- take (監(jiān)聽(tīng)且只監(jiān)聽(tīng)一次 action)
- delay (延遲)
- race (只處理最先完成的任務(wù))
并且通過(guò) Generator 實(shí)現(xiàn)對(duì)于這些副作用的管理肥印,讓我們可以用同步的邏輯寫(xiě)一個(gè)邏輯復(fù)雜的異步流。
下面這個(gè)例子出自于 官方文檔 绝葡,實(shí)現(xiàn)了一個(gè)對(duì)于請(qǐng)求的隊(duì)列深碱,即讓程序同一時(shí)刻只會(huì)進(jìn)行一個(gè)請(qǐng)求,其它請(qǐng)求則排隊(duì)等待藏畅,直到前一個(gè)請(qǐng)求結(jié)束:
import { buffers } from 'redux-saga';
import { take, actionChannel, call, ... } from 'redux-saga/effects';
function* watchRequests() {
// 1- 創(chuàng)建一個(gè)針對(duì)請(qǐng)求事件的 channel
const requestChan = yield actionChannel('REQUEST');
while (true) {
// 2- 從 channel 中拿出一個(gè)事件
const {payload} = yield take(requestChan);
// 3- 注意這里我們使用的是阻塞的函數(shù)調(diào)用
yield call(handleRequest, payload);
}
}
function* handleRequest(payload) { ... }
更多關(guān)于 redux-saga 的內(nèi)容敷硅,請(qǐng)參考 Read Me | redux-saga (中文文檔: 自述 | Redux-saga 中文文檔 )。
4. 提高 selector 的性能
把 react 與 redux 結(jié)合的時(shí)候愉阎,react-redux 提供了一個(gè)極其重要的方法: connect 绞蹦,它的作用就是選取 redux store 中的需要的 state 與 dispatch , 交由 connect 去綁定到 react 組件的 props 中:
import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
// 我們需要向 TodoList 中注入一個(gè)名為 todos 的 prop
// 它通過(guò)以下這個(gè)函數(shù)從 state 中提取出來(lái):
const mapStateToProps = (state) => {
// 下面這個(gè)函數(shù)就是所謂的selector
todos: state.todos.filter(i => i.completed)
// 其它props...
}
const mapDispatchToProps = (dispatch) => {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
// 綁定到組件上
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
在這里需要指定哪些 state 屬性被注入到 component 的 props 中,這是通過(guò)一個(gè)叫 selector 的函數(shù)完成的榜旦。
上面這個(gè)例子存在一個(gè)明顯的性能問(wèn)題幽七,每當(dāng)組件有任何更新時(shí)都會(huì)調(diào)用一次 state.todos.filter 來(lái)計(jì)算 todos ,但我們實(shí)際上只需要在 state.todos 變化時(shí)重新計(jì)算即可溅呢,每次更新都重算一遍是非常不合適的做法澡屡。下面介紹的這個(gè) reselect 就能幫你省去這些沒(méi)必要的重新計(jì)算。
你可能會(huì)注意到咐旧, selector 實(shí)際上就是一個(gè)『 純函數(shù)』 :
selector(state) => some props
而純函數(shù)是具有可緩存性的驶鹉,即對(duì)于同樣的輸入?yún)?shù),永遠(yuǎn)會(huì)得到相同的輸出值 (如果對(duì)這個(gè)不太熟悉的同學(xué)可以參考 JavaScript函數(shù)式編程 铣墨,reselect 的原理就是如此室埋,每次調(diào)用 selector 函數(shù)之前,它會(huì)判斷參數(shù)與之前緩存的是否有差異踏兜,若無(wú)差異词顾,則直接返回緩存的結(jié)果,反之則重新計(jì)算:
import { createSelector } from 'reselect';
var state = {
a: 100
}
var naiveSelector = state => state.a;
// mySelector 會(huì)緩存輸入 a 對(duì)應(yīng)的輸出值
var mySelector = createSelector(
naiveSelector,
a => {
console.log('做一次乘法!!!');
return a * a;
}
)
console.log(mySelector(state)); // 第一次計(jì)算碱妆,需要做一次乘法
console.log(mySelector(state)); // 輸入值未變化肉盹,直接返回緩存的結(jié)果
console.log(mySelector(state)); // 同上
state.a = 5; // 改變 a 的值
console.log(mySelector(state)); // 輸入值改變,做一次乘法
console.log(mySelector(state)); // 輸入值未變化疹尾,直接返回緩存的結(jié)果
console.log(mySelector(state)); // 同上
上面的輸出值是:
做一次乘法!!!
10000
10000
10000
做一次乘法!!!
25
25
25
之前那個(gè)關(guān)于 todos 的范例可以這樣改上忍,就可以避免 todos 數(shù)組被重復(fù)計(jì)算的性能問(wèn)題:
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const todoSelector = createSelector(
state => state.todos,
todos => todos.filter(i => i.completed)
)
const mapStateToProps = (state) => {
todos: todoSelector
// 其它props...
}
const mapDispatchToProps = (dispatch) => {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
// 綁定到組件上
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
更多可以參考 GitHub - reactjs/reselect: Selector library for Redux
5. 減少冗余代碼
redux 中的 action 一般都類(lèi)似這樣寫(xiě):
function foo(data){
return {
type: 'FOO',
data: data
}
}
//或者es6寫(xiě)法:
var foo = data => ({ type: 'FOO', data})
當(dāng)應(yīng)用越來(lái)越大之后,action 的數(shù)量也會(huì)大大增加纳本,為每個(gè) action 對(duì)象顯式地寫(xiě)上 type 和 data 或者其它屬性會(huì)造成大量的代碼冗余窍蓝,這一塊是完全可以?xún)?yōu)化的。
比如我們可以寫(xiě)一個(gè)最簡(jiǎn)單的 actionCreator:
function actionCreator(type){
return function(data){
return {
type: type,
data: data
}
}
}
var foo = actionCreator('FOO');
foo(123); // {type: 'FOO', data: 123}
redux-actions 就可以為我們做這樣的事情繁成,除了上面這種樸素的做法吓笙,它還有其它比較好用的功能,比如它提供的 createActions 方法可以接受不同類(lèi)型的參數(shù)巾腕,以產(chǎn)生不同效果的 actionCreator面睛,下面這個(gè)范例來(lái)自官方文檔:
import { createActions } from 'redux-actions';
const { actionOne, actionTwo, actionThree } = createActions({
// 函數(shù)類(lèi)型
ACTION_ONE: (key, value) => ({ [key]: value }),
// 數(shù)組類(lèi)型
ACTION_TWO: [
(first) => first, // payload
(first, second) => ({ second }) // meta
],
// 最簡(jiǎn)單的字符串類(lèi)型
}, 'ACTION_THREE');
actionOne('key', 1));
//=>
//{
// type: 'ACTION_ONE',
// payload: { key: 1 }
//}
actionTwo('Die! Die! Die!', 'It\'s highnoon~');
//=>
//{
// type: 'ACTION_TWO',
// payload: ['Die! Die! Die!'],
// meta: { second: 'It\'s highnoon~' }
//}
actionThree(76);
//=>
//{
// type: 'ACTION_THREE',
// payload: 76,
//}
更多可以參考 GitHub - acdlite/redux-actions: Flux Standard Action utilities for Redux.