Redux的全家桶與最佳實踐
Redux 的第一次代碼提交是在 2015 年 5 月底(也就是一年多前的樣子),那個時候 React 的最佳實踐還不是明晰,作為一個 View 層,有人會用 backbone 甚至是 angular 和它搭配,也有人覺得這層 View 功能已經足夠強大,簡單地搭配一些 utils 就直接上。后來便有了 FLUX 的演講,React 社區開始注意到這種新的類似函數式編程的理念,Redux 也作為 FLUX 的一種變體開始受到關注,再后來順理成章地得到 React 的『欽點』,作者也加入了 非死book 從事 React 的開發。生態圈經過了這一年的成熟,現在很多第三方庫已經非常完善,所以這里想介紹一下目前 Redux 的一些最佳實踐。
一、復習一下 Redux 的基本概念
首先我們復習一下 Redux 的基本概念, 如果你已經很熟悉了,就直接跳過這一章吧。
Redux 把界面視為一種狀態機,界面里的所有狀態、數據都可以由一個狀態樹來描述。所以對于界面的任何變更都簡化成了狀態機的變化:
(State, Input) => NewState
這其中切分成了三個階段:
- action
- reducer
- store
所謂的 action,就是用一個對象描述發生了什么,Redux 中一般使用一個純函數,即 actionCreator 來生成 action 對象。
// actionCreator => action
// 這是一個純函數,只是簡單地返回 action
function somethingHappened(data){
return {
type: 'foo',
data: data
}
}
隨后這個 action 對象和當前的狀態樹 state 會被傳入到 reducer 中,產生一個新的 state
//reducer(action, state) => newState
function reducer(action, state){
switch(action.type){
case 'foo':
return { data: data };
default:
return state;
}
}
store 的作用就是儲存 state,并且監聽其變化。
簡單地說就是你可以這樣產生一個 store :
import { createStore } from 'redux'
//這里的 reducer 就是剛才的 Reducer 函數
let store = createStore(reducer);
然后你可以通過 dispatch 一個 action 來讓它改變狀態:
store.getState(); // {}
store.dispatch(somethingHappened('aaa'));
store.getState(); // { data: 'aaa'}
好了,這就是 Redux 的全部功能。對的,它就是如此簡單,以至于它本體只有 3KB 左右的代碼,因為它只是實現了一個簡單的狀態機而已,任何稍微有點編程能力的人都能很快寫出這個東西。至于和 React 的結合,則需要 react-redux 這個庫,這里我們就不講怎么用了。
二、Redux 的一些痛點
大體上,Redux 的數據流是這樣的:
界面 => action => reducer => store => react => virtual dom => 界面
每一步都很純凈,看起來很美好對吧?對于一些小小的嘗試性質的 DEMO 來說確實很美好。但其實當應用變得越來越大的時候,這其中存在諸多問題:
- 如何優雅地寫異步代碼?(從簡單的數據請求到復雜的異步邏輯)
- 狀態樹的結構應該怎么設計?
- 如何避免重復冗余的 actionCreator?
- 狀態樹中的狀態越來越多,結構越來越復雜的時候,和 react 的組件映射如何避免混亂?
- 每次狀態的細微變化都會生成全新的 state 對象,其中大部分無變化的數據是不用重新克隆的,這里如何提高性能?
你以為我會在下面一一介紹這些問題是怎么解決的?還真不是,這里大部分問題的回答都可以在官方文檔中看到: 技巧 | Redux 中文文檔 ,文檔里講得已經足夠詳細(有些甚至詳細得有些啰嗦了)。所以下面只挑 Redux 生態圈里幾個比較成熟且流行的組件來講講。
三、Redux 異步控制
官方文檔里介紹了一種很樸素的異步控制中間件 redux-thunk (如果你還不了解中間件的話請看 Middleware | Redux 中文文檔 ,事實上 redux-thunk 的代碼很簡單,簡單到只有幾行代碼:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
它其實只干了一件事情,判斷 actionCreator 返回的是不是一個函數,如果不是的話,就很普通地傳給下一個中間件(或者 reducer);如果是的話,那么把 dispatch 、 getState 、 extraArgument 作為參數傳入這個函數里,實現異步控制。
比如我們可以這樣寫:
//普通action
function foo(){
return {
type: 'foo',
data: 123
}
}
//異步action
function fooAsync(){
return dispatch => {
setTimeout(_ => dispatch(123), 3000);
}
}
但這種簡單的異步解決方法在應用變得復雜的時候,并不能滿足需求,反而會使 action 變得十分混亂。
舉個比較簡單的例子,我們現在要實現『圖片上傳』功能,用戶點擊開始上傳之后,顯示出加載效果,上傳完畢之后,隱藏加載效果,并顯示出預覽圖;如果發生錯誤,那么顯示出錯誤信息,并且在2秒后消失。
用普通的 redux-thunk 是這樣寫的:
function upload(data){
return dispatch => {
// 顯示出加載效果
dispatch({ type: 'SHOW_WAITING_MODAL' });
// 開始上傳
api.upload(data)
.then(res => {
// 成功,隱藏加載效果,并顯示出預覽圖
dispatch({ type: 'PRELOAD_IMAGES', data: res.images });
dispatch({ type: 'HIDE_WAITING_MODAL' });
})
.catch(err => {
// 錯誤,隱藏加載效果,顯示出錯誤信息,2秒后消失
dispatch({ type: 'SHOW_ERROR', data: err });
dispatch({ type: 'HIDE_WAITING_MODAL' });
setTimeout(_ => dispatch({ type: 'HIDE_ERROR' }), 2000);
})
}
}
這里的問題在于,一個異步的 upload action 執行過程中會產生好幾個新的 action,更可怕的是這些新的 action 也是包含邏輯的(比如要判斷是否錯誤),這直接導致異步代碼中到處都是 dispatch(action) ,是很不可控的情況。如果還要進一步考慮取消、超時、隊列的情況,就更加混亂了。
所以我們需要更強大的異步流控制,這就是 GitHub - yelouafi/redux-saga: An alternative side effect model for Redux apps 。下面我們來看看如果換成 redux-saga 的話會怎么樣:
import { take, put, call, delay } from 'redux-saga/effects'
// 上傳的異步流
function *uploadFlow(action) {
// 顯示出加載效果
yield put({ type: 'SHOW_WAITING_MODAL' });
// 簡單的 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)
}
是不是規整很多呢?redux-saga 允許我們使用簡單的 try-catch 來進行錯誤處理,更神奇的是竟然可以直接使用 delay 來替代 setTimeout 這種會造成回調和嵌套的不優雅的方法。
本質上講,redux-sage 提供了一系列的『副作用(side-effects)方法』,比如以下幾個:
- put (產生一個 action)
- call (阻塞地調用一個函數)
- fork (非阻塞地調用一個函數)
- take (監聽且只監聽一次 action)
- delay (延遲)
- race (只處理最先完成的任務)
并且通過 Generator 實現對于這些副作用的管理,讓我們可以用同步的邏輯寫一個邏輯復雜的異步流。
下面這個例子出自于 官方文檔 ,實現了一個對于請求的隊列,即讓程序同一時刻只會進行一個請求,其它請求則排隊等待,直到前一個請求結束:
import { buffers } from 'redux-saga';
import { take, actionChannel, call, ... } from 'redux-saga/effects';
function* watchRequests() {
// 1- 創建一個針對請求事件的 channel
const requestChan = yield actionChannel('REQUEST');
while (true) {
// 2- 從 channel 中拿出一個事件
const {payload} = yield take(requestChan);
// 3- 注意這里我們使用的是阻塞的函數調用
yield call(handleRequest, payload);
}
}
function* handleRequest(payload) { ... }
更多關于 redux-saga 的內容,請參考 Read Me | redux-saga (中文文檔: 自述 | Redux-saga 中文文檔 )。
四、提高 selector 的性能
把 react 與 redux 結合的時候,react-redux 提供了一個極其重要的方法: connect ,它的作用就是選取 redux store 中的需要的 state 與 dispatch , 交由 connect 去綁定到 react 組件的 props 中:
import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
// 我們需要向 TodoList 中注入一個名為 todos 的 prop
// 它通過以下這個函數從 state 中提取出來:
const mapStateToProps = (state) => {
// 下面這個函數就是所謂的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 中,這是通過一個叫 selector 的函數完成的。
上面這個例子存在一個明顯的性能問題,每當組件有任何更新時都會調用一次 state.todos.filter 來計算 todos ,但我們實際上只需要在 state.todos 變化時重新計算即可,每次更新都重算一遍是非常不合適的做法。下面介紹的這個 reselect 就能幫你省去這些沒必要的重新計算。
你可能會注意到, selector 實際上就是一個『 純函數』 :
selector(state) => some props
而純函數是具有可緩存性的,即對于同樣的輸入參數,永遠會得到相同的輸出值 (如果對這個不太熟悉的同學可以參考我之前寫的 JavaScript函數式編程(一) - 一只碼農的技術日記 - 知乎專欄 ,reselect 的原理就是如此,每次調用 selector 函數之前,它會判斷參數與之前緩存的是否有差異,若無差異,則直接返回緩存的結果,反之則重新計算:
import { createSelector } from 'reselect';
var state = {
a: 100
}
var naiveSelector = state => state.a;
// mySelector 會緩存輸入 a 對應的輸出值
var mySelector = createSelector(
naiveSelector,
a => {
console.log('做一次乘法!!!');
return a * a;
}
)
console.log(mySelector(state)); // 第一次計算,需要做一次乘法
console.log(mySelector(state)); // 輸入值未變化,直接返回緩存的結果
console.log(mySelector(state)); // 同上
state.a = 5; // 改變 a 的值
console.log(mySelector(state)); // 輸入值改變,做一次乘法
console.log(mySelector(state)); // 輸入值未變化,直接返回緩存的結果
console.log(mySelector(state)); // 同上
上面的輸出值是:
做一次乘法!!!
10000
10000
10000
做一次乘法!!!
25
25
25
之前那個關于 todos 的范例可以這樣改,就可以避免 todos 數組被重復計算的性能問題:
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
五、減少冗余代碼
redux 中的 action 一般都類似這樣寫:
function foo(data){
return {
type: 'FOO',
data: data
}
}
//或者es6寫法:
var foo = data => ({ type: 'FOO', data})
當應用越來越大之后,action 的數量也會大大增加,為每個 action 對象顯式地寫上 type 和 data 或者其它屬性會造成大量的代碼冗余,這一塊是完全可以優化的。
比如我們可以寫一個最簡單的 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 方法可以接受不同類型的參數,以產生不同效果的 actionCreator,下面這個范例來自官方文檔:
import { createActions } from 'redux-actions';
const { actionOne, actionTwo, actionThree } = createActions({
// 函數類型
ACTION_ONE: (key, value) => ({ [key]: value }),
// 數組類型
ACTION_TWO: [
(first) => first, // payload
(first, second) => ({ second }) // meta
],
// 最簡單的字符串類型
}, '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.
六、更多
我總是覺得,輪子永遠是造不完的,也是看不完的,這么多輪子的取舍其實終究還是要看開發者的能力以及實際項目的需求,有時你或許根本不需要這些東西,有時甚至連 Redux 本身也是多余的,畢竟,第三方庫其實也是另一種意義上的『復雜度』嘛。
來自:https://zhuanlan.zhihu.com/p/22405838