Redux的全家桶與最佳實(shí)踐

image.png

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è)階段:

  1. action
  2. reducer
  3. 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)題:

  1. 如何優(yōu)雅地寫(xiě)異步代碼黄鳍?(從簡(jiǎn)單的數(shù)據(jù)請(qǐng)求到復(fù)雜的異步邏輯)
  2. 狀態(tài)樹(shù)的結(jié)構(gòu)應(yīng)該怎么設(shè)計(jì)推姻?
  3. 如何避免重復(fù)冗余的 actionCreator?
  4. 狀態(tài)樹(shù)中的狀態(tài)越來(lái)越多框沟,結(jié)構(gòu)越來(lái)越復(fù)雜的時(shí)候藏古,和 react 的組件映射如何避免混亂?
  5. 每次狀態(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)胧弛;如果是的話尤误,那么把 dispatchgetState 结缚、 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è):

  1. put (產(chǎn)生一個(gè) action)
  2. call (阻塞地調(diào)用一個(gè)函數(shù))
  3. fork (非阻塞地調(diào)用一個(gè)函數(shù))
  4. take (監(jiān)聽(tīng)且只監(jiān)聽(tīng)一次 action)
  5. delay (延遲)
  6. 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.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末絮蒿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子叁鉴,更是在濱河造成了極大的恐慌土涝,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幌墓,死亡現(xiàn)場(chǎng)離奇詭異但壮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)常侣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)蜡饵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人胳施,你說(shuō)我怎么就攤上這事验残。” “怎么了巾乳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鸟召。 經(jīng)常有香客問(wèn)我胆绊,道長(zhǎng),這世上最難降的妖魔是什么欧募? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任压状,我火速辦了婚禮,結(jié)果婚禮上跟继,老公的妹妹穿的比我還像新娘种冬。我一直安慰自己,他們只是感情好舔糖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布娱两。 她就那樣靜靜地躺著,像睡著了一般金吗。 火紅的嫁衣襯著肌膚如雪十兢。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,708評(píng)論 1 305
  • 那天摇庙,我揣著相機(jī)與錄音旱物,去河邊找鬼。 笑死卫袒,一個(gè)胖子當(dāng)著我的面吹牛宵呛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播夕凝,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宝穗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼户秤!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起讽营,我...
    開(kāi)封第一講書(shū)人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤虎忌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后橱鹏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體膜蠢,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年莉兰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挑围。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡糖荒,死狀恐怖杉辙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捶朵,我是刑警寧澤蜘矢,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站综看,受9級(jí)特大地震影響品腹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜红碑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一舞吭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧析珊,春花似錦羡鸥、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至奕剃,卻和暖如春赶舆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背祭饭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工芜茵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人倡蝙。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓九串,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猪钮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容