上手redux中間件,有這篇文章就夠了!

用過react的同學(xué)都知道在redux的存在,redux就是一種前端用來存儲(chǔ)數(shù)據(jù)的倉庫,并對(duì)改倉庫進(jìn)行增刪改查操作的一種框架,它不僅僅適用于react,也使用于其他前端框架。研究過redux源碼的人都覺得該源碼很精妙,而本博文就針對(duì)redux中對(duì)中間件的處理進(jìn)行介紹。

在講redux中間件之前,先用兩張圖來大致介紹一下redux的基本原理:


381423828-5ac4fa1bdaad0_articlex.png

圖中就是redux的基本流程,這里就不細(xì)說。

一般在react中不僅僅利用redux,還利用到react-redux:


1968902651-5ac5014772c5e_articlex.png

由于工作中一直用封裝好的如redux-logger、redux-thunk、redux-saga此類的中間件,并沒有深入去了解過redux中間件的實(shí)現(xiàn)方式。正好最近有個(gè)分享,于是就開始著手,于是借此了解了下Redux Middleware的原理。

中間件概念

首先簡單提下什么是中間件,簡單來講,Redux middleware 提供了一個(gè)分類處理 action 的機(jī)會(huì)。在 middleware 中,我們可以檢閱每一個(gè)流過的 action,并挑選出特定類型的 action 進(jìn)行相應(yīng)操作,以此來改變 action。這樣說起來可能會(huì)有點(diǎn)抽象,我們直接來看圖,這是在沒有有中間件和沒有中間件情況下的 redux 的數(shù)據(jù)流:


1562386551(1).jpg

不難發(fā)現(xiàn):

1.不使用middleware時(shí),在dispatch(action)時(shí)會(huì)執(zhí)行rootReducer,并根據(jù)action的type更新返回相應(yīng)的state。
2.而在使用middleware時(shí),middleware會(huì)將我們當(dāng)前的action做相應(yīng)的處理。在增加了 middleware 后,我們就可以在這途中對(duì) action 進(jìn)行截獲,并進(jìn)行改變。隨后將新的action再交給其他中間件處理,最后產(chǎn)生新的action給rootReducer執(zhí)行。且由于業(yè)務(wù)場景的多樣性,單純的修改 dispatch 和 reduce 人顯然不能滿足大家的需要,因此對(duì) redux middleware 的設(shè)計(jì)是可以自由組合,自由插拔的插件機(jī)制。也正是由于這個(gè)機(jī)制,我們?cè)谑褂?middleware 時(shí),我們可以通過串聯(lián)不同的 middleware 來滿足日常的開發(fā),每一個(gè) middleware 都可以處理一個(gè)相對(duì)獨(dú)立的業(yè)務(wù)需求且相互串聯(lián):


4116027-993b5e2ebc72a5a11.png

如上圖所示,派發(fā)給 redux Store 的 action 對(duì)象,會(huì)被 Store 上的多個(gè)中間件依次處理,如果把 action 和當(dāng)前的 state 交給 reducer 處理的過程看做默認(rèn)存在的中間件,那么其實(shí)所有的對(duì) action 的處理都可以有中間件組成的。值得注意的是這些中間件會(huì)按照指定的順序一次處理傳入的 action,只有排在前面的中間件完成任務(wù)之后,后面的中間件才有機(jī)會(huì)繼續(xù)處理 action,同樣的,每個(gè)中間件都有自己的“熔斷”處理,當(dāng)它認(rèn)為這個(gè) action 不需要后面的中間件進(jìn)行處理時(shí),后面的中間件也就不能再對(duì)這個(gè) action 進(jìn)行處理了。
而不同的中間件之所以可以組合使用,是因?yàn)?Redux 要求所有的中間件必須提供統(tǒng)一的接口,每個(gè)中間件的尉氏縣邏輯雖然不一樣,但只要遵循統(tǒng)一的接口就能和redux以及其他的中間件對(duì)話了。

截止到這我們已經(jīng)了解了中間件的基本原理了~那么中間件在項(xiàng)目中該如何使用呢?

引入ApplyMiddleware高階函數(shù)和需要的中間件實(shí)例,并將實(shí)例作為ApplyMiddleware的參數(shù)注入store。中間件就可以使用了

import logger from 'redux-logger';//引入logger中間件
import createSagaMiddleware from 'redux-saga';引入saga中間件構(gòu)造函數(shù)
import mySaga from './saga.js'; 引入要執(zhí)行的saga文件
import todos from './reducer/reducer';
const sagaMiddleware = createSagaMiddleware();//生成saga實(shí)例
const store = createStore(todos, applyMiddleware(logger, sagaMiddleware));//將項(xiàng)目中用到的中間件以參數(shù)的形式,通過applyMiddleware函數(shù)引入項(xiàng)目。

了解了基本原理能有助于我們更快地讀懂middleware的源碼。業(yè)務(wù)中,一般我們會(huì)這樣添加中間件并使用。

createStore(rootReducer, applyMiddleware.apply(null, [...middlewares]))

接下來我們可以重點(diǎn)關(guān)注這兩個(gè)函數(shù)createStore、applyMiddleware

CreateStore

// 摘至createStore
export function createStore(reducer, rootState, enhance) {
    ...
    if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
          throw new Error('Expected the enhancer to be a function.')
        }
    /*
        若使用中間件,這里 enhancer 即為 applyMiddleware()
        若有enhance,直接返回一個(gè)增強(qiáng)的createStore方法,可以類比成react的高階函數(shù)
    */
    return enhancer(createStore)(reducer, preloadedState)
  }
...
}

ApplyMiddleware

中間件方式中核心部分就是redux提供的applyMiddleWare這個(gè)高階函數(shù),它通過多層調(diào)用后悔返回一個(gè)全新的store對(duì)象,全新的store對(duì)象和原來對(duì)象中,唯一的不同就是dispatch具備了異步的功能;
源碼:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用傳入的createStore和reducer和創(chuàng)建一個(gè)store
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 讓每個(gè) middleware 帶著 middlewareAPI 這個(gè)參數(shù)分別執(zhí)行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 接著 compose 將 chain 中的所有匿名函數(shù),組裝成一個(gè)新的函數(shù),即新的 dispatch
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

短短十幾行代碼,其中卻蘊(yùn)含著不少精妙之處,我選擇了其中三處地方進(jìn)行分析其精妙之處:
1)MiddleWareAPI主要是通過塞進(jìn)中間件,從而最終塞進(jìn)action中,讓action能具備dispatch的能力,而這里為什么要用匿名函數(shù),主要原因是因?yàn)橐孧iddleWareAPI.dispatch中的store和applyMiddleWare最終返回的store保持一致,要注意的是MiddleWareAPI.dispatch不是真正讓state改變,它可以理解為是action和中間件的一個(gè)橋梁。

2)改地方就是將MiddleWareAPI塞進(jìn)所有的中間件中,然后返回一個(gè)函數(shù),而中間件的形式后面會(huì)說到。

3)該地方是最為精妙之處,compose會(huì)將chain數(shù)組從右到左一次地柜注入到前一個(gè)中間件,而store.dispatch會(huì)注入到最右邊的一個(gè)的中間件。其實(shí)這里可以將compose理解為reduce函數(shù)。
eg:

M = [M1,M2,M3] ----> M1(M2(M3(store.dispatch)));
從這里其實(shí)就知道中間件大致是什么樣子的了:
中間件基本形式:

const MiddleWare = (dispatch, getState) => store => next => action => {
    if (typeof action === 'function') {
        ...//action為函數(shù)時(shí)執(zhí)行該中間件該有的操作
    } else {
        next(action);
    }
}

上面這個(gè)函數(shù)接受一個(gè)對(duì)象作為參數(shù),對(duì)象的參數(shù)上有兩個(gè)字段 dispatch 和 getState,分別代表著 Redux Store 上的兩個(gè)同名函數(shù),但需要注意的是并不是所有的中間件都會(huì)用到這兩個(gè)函數(shù)。然后 doNothingMidddleware 返回的函數(shù)接受一個(gè) next 類型的參數(shù),這個(gè) next 是一個(gè)函數(shù),如果調(diào)用了它,就代表著這個(gè)中間件完成了自己的職能,并將對(duì) action 控制權(quán)交予下一個(gè)中間件。但需要注意的是,這個(gè)函數(shù)還不是處理 action 對(duì)象的函數(shù),它所返回的那個(gè)以 action 為參數(shù)的函數(shù)才是。最后以 action 為參數(shù)的函數(shù)對(duì)傳入的 action 對(duì)象進(jìn)行處理,在這個(gè)地方可以進(jìn)行操作,比如:

調(diào)動(dòng)dispatch派發(fā)一個(gè)新 action 對(duì)象
調(diào)用 getState 獲得當(dāng)前 Redux Store 上的狀態(tài)
調(diào)用 next 告訴 Redux 當(dāng)前中間件工作完畢,讓 Redux 調(diào)用下一個(gè)中間件
訪問 action 對(duì)象 action 上的所有數(shù)據(jù)。

在具有上面這些功能后,一個(gè)中間件就足夠獲取 Store 上的所有信息,也具有足夠能力可用之?dāng)?shù)據(jù)的流轉(zhuǎn)??赐晟厦孢@個(gè)最簡單的中間件,下面我們來看一下 redux 中間件內(nèi),最出名的中間件 redux-thunk 的實(shí)現(xiàn):

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 中間件的功能也很簡單。首先檢查參數(shù) action 的類型,如果是函數(shù)的話,就執(zhí)行這個(gè) action 韓湖水,并把 dispatch, getState, extraArgument 作為參數(shù)傳遞進(jìn)去,否則就調(diào)用 next 讓下一個(gè)中間件繼續(xù)處理 action 。流程如下:


794743064-5ac623cabd6bb_articlex.png

異步的中間件

1.redux-thunk

上面說到的redux-thunk就是一個(gè)異步的中間件。它通過多參數(shù)的 currying 以實(shí)現(xiàn)對(duì)函數(shù)的惰性求值,從而將同步的 action 轉(zhuǎn)為異步的 action。在理解了redux-thunk后,我們?cè)趯?shí)現(xiàn)數(shù)據(jù)請(qǐng)求時(shí),action就可以這么寫了:

function getWeather(url, params) {
    return (dispatch, getState) => {
        fetch(url, params)
            .then(result => {
                dispatch({
                    type: 'GET_WEATHER_SUCCESS', payload: result,
                });
            })
            .catch(err => {
                dispatch({
                    type: 'GET_WEATHER_ERROR', error: err,
                });
            });
        };
}
redux-thunk的缺點(diǎn)

hunk的缺點(diǎn)也是很明顯的,thunk僅僅做了執(zhí)行這個(gè)函數(shù),并不在乎函數(shù)主體內(nèi)是什么,也就是說thunk使
得redux可以接受函數(shù)作為action,但是函數(shù)的內(nèi)部可以多種多樣。比如下面是一個(gè)獲取商品列表的異步操作所對(duì)應(yīng)的action:

export default ()=>(dispatch)=>{
    fetch('/api/goodList',{ //fecth返回的是一個(gè)promise
      method: 'get',
      dataType: 'json',
    }).then(function(json){
      var json=JSON.parse(json);
      if(json.msg==200){
        dispatch({type:'init',data:json.data});
      }
    },function(error){
      console.log(error);
    });
};

從這個(gè)具有副作用的action中,我們可以看出,函數(shù)內(nèi)部極為復(fù)雜。如果需要為每一個(gè)異步操作都如此定義一個(gè)action,顯然action不易維護(hù)。

action不易維護(hù)的原因:

1.action的形式不統(tǒng)一
2.就是異步操作太為分散,分散在了各個(gè)action中
盡管redux-thunk很簡單,而且也很實(shí)用,但人總是有追求的,都追求著使用更加優(yōu)雅的方法來實(shí)現(xiàn)redux異步流的控制,這就有了redux-saga。

2.redux-saga

redux-saga是一個(gè)用于管理redux應(yīng)用異步操作的中間件,redux-saga通過創(chuàng)建sagas將所有異步操作邏輯收集在一個(gè)地方集中處理,可以用來代替redux-thunk中間件。

首先來看redux-thunk的大體過程:

action1(side function)—>redux-thunk監(jiān)聽—>執(zhí)行相應(yīng)的有副作用的方法—>action2(plain object)
而redux-saga的大體過程如下:

action1(plain object)——>redux-saga監(jiān)聽—>執(zhí)行相應(yīng)的Effect方法——>返回描述對(duì)象—>恢復(fù)執(zhí)行異步和副作用函數(shù)—>action2(plain object)

redux-saga中最大的特點(diǎn)就是提供了聲明式的Effect,聲明式的Effect使得redux-saga監(jiān)聽原始js對(duì)象形式的action,并且可以方便單元測試,我們一一來看。

Effects

一個(gè)effect就是一個(gè)純文本javascript對(duì)象,包含一些將被saga middleware執(zhí)行的指令。
如何創(chuàng)建 effect ?
使用redux-saga提供的 工廠函數(shù) 來創(chuàng)建effect
比如:

你可以使用  let res = yield call(myfunc,  'arg1', 'arg2')  指示middleware調(diào)用  myfunc('arg1', 'arg2')

并將結(jié)果返回給 yield 了 effect  的那個(gè) res
Effect提供的具體方法

首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中將這一系列的api都定義為Effect。這些Effect執(zhí)行后,當(dāng)函數(shù)resolve時(shí)返回一個(gè)描述對(duì)象,然后redux-saga中間件根據(jù)這個(gè)描述對(duì)象恢復(fù)執(zhí)行g(shù)enerator中的函數(shù)。
下面來介紹幾個(gè)Effect中常用的幾個(gè)方法,從低階的API,比如take,call(apply),fork,put,select等,以及高階API,比如takeEvery和takeLatest等,從而加深對(duì)redux-saga用法的認(rèn)識(shí)
引入:

import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'

take

take這個(gè)方法,是用來監(jiān)聽action,返回的是監(jiān)聽到的action對(duì)象。比如:

const loginAction = {
   type:'login'
}

在UI Component中dispatch一個(gè)action:

dispatch(loginAction)

在saga中使用:

const action = yield take('login');

可以監(jiān)聽到UI傳遞到中間件的Action,上述take方法的返回,就是dipath的原始對(duì)象。一旦監(jiān)聽到login動(dòng)作,返回的action為:

{
  type:'login'
}

call(apply)

call和apply方法與js中的call和apply相似,我們以call方法為例:

call(fn, ...args)

call方法調(diào)用fn,參數(shù)為args,返回一個(gè)描述對(duì)象。不過這里call方法傳入的函數(shù)fn可以是普通函數(shù),也可以是generator。call方法應(yīng)用很廣泛,在redux-saga中使用異步請(qǐng)求等常用call方法來實(shí)現(xiàn)。

yield call(fetch,'/userInfo',username)

put

在前面提到,redux-saga做為中間件,工作流是這樣的:

UI——>action1————>redux-saga中間件————>action2————>reducer..

從工作流中,我們發(fā)現(xiàn)redux-saga執(zhí)行完副作用函數(shù)后,必須發(fā)出action,然后這個(gè)action被reducer監(jiān)聽,從而達(dá)到更新state的目的。相應(yīng)的這里的put對(duì)應(yīng)與redux中的dispatch,工作流程圖如下:

4116027-0cf60f03d1f7aaaf.png

從圖中可以看出redux-saga執(zhí)行副作用方法轉(zhuǎn)化action時(shí),put這個(gè)Effect方法跟redux原始的dispatch相似,都是可以發(fā)出action,且發(fā)出的action都會(huì)被reducer監(jiān)聽到。put的使用方法:

 yield put({type:'login'})

select

put方法與redux中的dispatch相對(duì)應(yīng),同樣的如果我們想在中間件中獲取state,那么需要使用select。select方法對(duì)應(yīng)的是redux中的getState,用戶獲取store中的state,使用方法:

const state= yield select()

fork

fork方法相當(dāng)于web work,fork方法不會(huì)阻塞主線程,在非阻塞調(diào)用中十分有用。

takeEvery和takeLatest

takeEvery和takeLatest用于監(jiān)聽相應(yīng)的action并執(zhí)行相應(yīng)的方法,是構(gòu)建在take和fork上面的高階api,比如要監(jiān)聽某個(gè)或者某幾個(gè)action,好用takeEvery方法可以:

takeEvery('acticonType',loginFunc)

takeEvery監(jiān)聽到某個(gè)actiontype為函數(shù)參數(shù)中的actionType的action,就會(huì)執(zhí)行l(wèi)oginFunc方法,除此之外,takeEvery可以同時(shí)監(jiān)聽到多個(gè)相同的action。

takeLatest方法跟takeEvery是相同方式調(diào)用:

takeLatest('login',loginFunc)

與takeLatest不同的是,takeLatest是會(huì)監(jiān)聽執(zhí)行最近的那個(gè)被觸發(fā)的action。

createSagaMiddleware(...sagas)
createSagaMiddleware的作用是創(chuàng)建一個(gè)redux中間件,并將sagas與Redux store建立鏈接

參數(shù)是一個(gè)數(shù)組,里面是generator函數(shù)列表
sagas: Array ---- ( generator函數(shù)列表 )

middleware.run(saga, ...args)

動(dòng)態(tài)執(zhí)行 saga。用于 applyMiddleware 階段之后執(zhí)行 Sagas。這個(gè)方法返回一個(gè)
Task 描述對(duì)象。

saga: Function: 一個(gè) Generator 函數(shù)
args: Array: 提供給 saga 的參數(shù) (除了 Store 的 getState 方法)

redux saga獲取異步數(shù)據(jù)實(shí)際操作

1.安裝redux saga
npm install redux-saga -S
//或者
yarn add redux-saga 
2.引入并配置saga
import React from 'react';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux'; //引入生成中間件的工廠函數(shù)
import createSagaMiddleware from 'redux-saga';//引入saga
import mySaga from './saga.js'; //新建一個(gè)文件,在這個(gè)文件中編寫saga
import todos from './reducer/reducer';
const sagaMiddleware = createSagaMiddleware();//生成saga實(shí)例
const store = createStore(todos, applyMiddleware(sagaMiddleware));//將saga注入store
sagaMiddleware.run(mySaga);動(dòng)態(tài)執(zhí)行 saga。用于 applyMiddleware 階段之后執(zhí)行 Sagas。這個(gè)方法返回一個(gè)Task 描述對(duì)象。

2.ui組件觸發(fā)action創(chuàng)建函數(shù)

class Saga extends Component {
    constructor (props) {
        super(props);
        this.state = {    
        }
        this.handleClickGet = this.handleClickGet.bind(this);
    }
    handleClickGet() {
      this.props.dispatch(getData());
    }
    render () {
        let {num} = this.props.state;
        return (
            <div className="saga"  onClick={this.handleClickGet}>
              異步
            </div>
        )
    }
}

3.action創(chuàng)建函數(shù),返回action ----> 傳入saga

const getData = () => ({
    type: 'GET_DATA',
});

4.新建并編寫saga文件 ------> 捕獲action創(chuàng)建函數(shù)返回的action

import { call, put, takeEvery } from 'redux-saga/effects'; // 引入相關(guān)函數(shù)

function* getGitData(action) { // 參數(shù)是action創(chuàng)建函數(shù)返回的action
    const fn = function() {
        return fetch(`https://api.github.com/users/github`, {
                method: 'GET'
            })
            .then(res => res.json())
            .then(res => {
                return res
            })
    }
    const res = yield call(fn) // 執(zhí)行p函數(shù),返回值賦值給res

    yield put({ // dispatch一個(gè)action到reducer, payload是請(qǐng)求返回的數(shù)據(jù)
        type: 'GET_DATA_SUCCESS',
        payload: res
    })
}

function* mySaga() { // 在store.js中,執(zhí)行了 sagaMiddleware.run(rootSaga)
    yield takeEvery('GET_DATA', getGitData) // 如果有對(duì)應(yīng)type的action觸發(fā),就執(zhí)行g(shù)oAge()函數(shù)
}

export default mySaga; // 導(dǎo)出rootSaga,被store.js文件import

項(xiàng)目源碼:https://github.com/zhou111222/car

參考文章:
http://www.itdecent.cn/p/6f96bdaaea22
https://www.zcfy.cc/article/async-operations-using-redux-saga-freecodecamp-2377.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 最近幾天對(duì) redux 的中間件進(jìn)行了一番梳理,又看了 redux-saga 的文檔,和 redux-thu...
    Srtian閱讀 33,685評(píng)論 9 40
  • 最近項(xiàng)目用了dva,dva對(duì)于異步action的處理是用了redux-saga,故簡單學(xué)習(xí)了下redux-saga...
    笨人不能懶閱讀 2,981評(píng)論 0 5
  • 1. redux-thunk處理副作用的缺點(diǎn) 1.1 redux的副作用處理 redux中的數(shù)據(jù)流大致是: UI—...
    Grace_ji閱讀 3,665評(píng)論 0 14
  • 看到這篇文章build an image gallery using redux saga,覺得寫的不錯(cuò),長短也適...
    smartphp閱讀 6,338評(píng)論 1 29
  • 寫在開頭 本片內(nèi)容主要為本人在閱讀redux官方文檔中基礎(chǔ)和進(jìn)階部分的學(xué)習(xí)筆記。由于本人能力有限,所以文章中可能會(huì)...
    前端開發(fā)愛好者閱讀 1,356評(píng)論 0 4

友情鏈接更多精彩內(nèi)容