揭開Redux神祕面紗:手寫一個min-Redux

前言

react和狀態管理redux是緊密結合的,而自己又沒有任何聯繫。react能夠不使用redux管理狀態,redux也能夠脫離react獨立存在。隨着react的項目愈來愈複雜,state變的繁重,各類propstate的轉變讓咱們在開發過程當中變得頭暈眼花,react原本就是一個專一於UI層的庫,本不該該讓繁雜的propstate的邏輯摻雜進來。因而Flux的架構出現了,Flux架構模式用於抽離reactstate能更好的去構建項目,Flux架構模式的實踐有好多中,顯然redux是成功的。前端

我在接觸reactredux以前老是聽好多人提起redux這個東西,我心想它到底有什麼魔力,讓那麼多的人爲之驚歎,今天就來揭開redux的真面目。react

redux

前面提到redux是能夠脫離react存在的,這句話的意思是redux並非依附於react的,即使是用jQuery+redux也是能夠的。redux提供的是一種狀態管理的方式,同時也定義了一種管理狀態的規則,全部須要使用這個小而美的庫的項目都必須遵循這個規則,也正是這個規則使用redux再書寫過程當中有了可預測性和可追溯性。面試

redux的設計原則

redux必然要談談它的設計原則,就如同想要更明白的瞭解同樣東西,就須要先了解它是怎麼來的,固然歷史明白上面這些就夠了。編程

redux有三大設計原則redux

  1. 單一數據源
  2. 狀態是隻讀的
  3. 使用純函數編寫reducer

1.單一數據源

單一數據源的意思是說整個react項目的state都存放在一塊兒,也能夠認爲存在一個大對象中,單一數據源可讓咱們在項目中更專一於數據源的設計與構建上。數組

2.狀態是隻讀的

使用過redux都知道,視圖是經過store.getState()方法來獲取狀態的,經過dispatch派發action來改變狀態。狀態是隻讀的也就是說咱們只能經過stiore.getState()來獲取狀態,只能經過dispatch派發action來改變狀態。這也體現了單一數據流動,讓咱們在構建項目的時候只須要關於一個方向的數據流動。架構

3.使用純函數編寫reducer

我當時在學的時候也是有這樣的疑問:爲何要使純函數來寫,什麼是純函數?併發

所謂純函數:對於一個函數來講相同的輸入一定有相同的輸出, 即不依賴外部環境,也不改變外部環境,這樣的函數就叫作純函數。純函數純的,是沒有反作用的。app

明白了純函數,那麼在寫reducer的時候必定見過這麼一段代碼。異步

const state = reducer(initstate = {},action);
複製代碼

上面代碼,再結合純函數,就能夠說對於特定的actioninitstate一定會獲得相同的state,這裏正是體現了redux的可預測性。

redux的四個角色

redux提供了一系列規則來規定咱們來寫代碼。能夠大體分爲四個角色:

  1. action
  2. reducer
  3. dispatch
  4. store

1.action

action是承載狀態的載體,通常action將視圖所產出的數據,發送到reducer進行處理。action的書寫格式通常是這樣:

const addAction = {
    type:"ADD",
    value:.....
}
複製代碼

action其實就是一個JavaScript對象,它必需要有一個type屬性用來標識這個action是幹嗎的(也能夠認爲家的地址,去reducer中找家),value屬性是action攜帶來自視圖的數據。

action的表示方式也能夠是一個函數,這樣能夠更方面的構建action,但這個函數必須返回一個對象。

const addAction = (val) => ({
    type:"ADD",
    value: val
})
複製代碼

這樣拿到的數據就靈活多了。

對於action的type屬性,通常若是action變的龐大的話會把全部的type抽離出來到一個constants中,例如:

const ADDTODO = 'ADDTODO',
const DELETETODO = 'DELETEDOTO'

export {
    ADDTODO,
    DELETETODO,
}
複製代碼

這樣可讓type更清晰一些。

2.reducer

reducer指定了應用狀態的變化如何響應 actions 併發送到 store。 在redux的設計原則中提到使用純函數來編寫reducer,目的是爲了讓state變的可預測。reducer的書寫方式通常是這樣:

const reducer = (state ={},action){
    switch(action.type){
        case :
           ......
        case :
           ......
        case :
           ......
        default :
           return state;
    }
}
複製代碼

使用switch判斷出什麼樣的action應該使用什麼樣的邏輯去處理。

拆分reducer

當隨着業務的增多,那麼reducer也隨着增大,顯然一個reducer是不可能的,因而必需要拆分reducer,拆分reducer也是有必定的套路的:好比拆分一個TodoList,就能夠把todos操做放在一塊兒,把對todo無關的放在一塊兒,最終造成一個根reducer。

function visibilityFilter(state,action){
    switch(action.type){
        case :
            ......
        case :
            ......
        default :
            return state;
    }
}
function todos(state,action){
    switch(action.type){
        case :
            ......
        case :
            ......
        default :
            return state;
    }
}
//根reducer
function rootReducer(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action), 
    todos: todos(state.todos, action)
  }
}
複製代碼

這樣作的好處在於業務邏輯的分離,讓根reducer再也不那麼繁重。好在redux提供了combineReducers方法用於構建rootReducer

const rootReducer = combineReducers({
    visibilityFilter,
    todos,
})
複製代碼

這部分代碼和上面rootReducer的做用徹底相同。它的原理是經過傳入對象的key-value把全部的state進行一個糅合。

3.dispatch

dispatch的做用是派發一個action去執行reducer。我以爲dispatch就是一個發佈者,和subscribe一塊兒組合成訂閱發佈者模式。使dispatch派發:

const action = {
    type: "ADD",
    value: "Hello Redux",
}
dispatch(action);
複製代碼

4.store

store能夠說是redux的核心了。開頭也提到storeredux狀態管理的惟一數據源,除此以外,store仍是將dispatchreducer等聯繫起來的命脈。

store經過redux提供的createStore建立,它是一個對象,有以下屬性:

  • store.getState() 獲取狀態的惟一途徑
  • store.dispatch(action) 派發action響應reducer
  • store.subscribe(handler) 監聽狀態的變化

建立store:

const store = Redux.createStore(reducer,initialState,enhancer);
//1. reducer就是咱們書寫的reducer
//2. initialState是初始化狀態
//3. enhancer是中間件
複製代碼

Middleware

在建立store的時候createStore是能夠傳入三個參數的,第三個參數就是中間件,使用redux提供的applyMiddleware來調用,applyMiddleware至關因而對dispatch的一種加強,經過中間件能夠在dispatch過程當中作一些事情,好比打logger、thunk(異步action)等等。

使用方式以下:

//異步action中間件
import thunk from "redux-thunk";
const store = Redux.createStore(reducer,initialState,applMiddleware(thunk));
複製代碼

思想先告一段落,既然懂得了redux的思想,那麼接下來手下一個簡易版的redux

手寫一個min-Redux

新的react-hooks中除了useReducer,集成了redux的功能,爲何還要深刻了解redux呢?

隨着前端技術的迭代,技術的快速更新,咱們目前並無能力去預知或者去引領前端的發展,惟一能作的就是在時代中吸取知識並消化知識,雖然將來有可能redux會被useReducer所取代,可是思想是不變的,redux這個小而美的庫設計是奇妙的,也許有哪一天在寫業務的時候遇到了某種類似的需求,咱們也能夠經過藉助於這個庫的思想去作一些事情。

createStore

要想了解redux,必然要先了解它的核心,它的核心就是createStore這個函數,storegetState,dispatch都在這裏產出。我我的以爲createStore是一個提供一系列方法的訂閱發佈者模式:經過subscribe訂閱store的變化,經過dispatch派發。那麼下面就來實現一下這個createStore

從上面store中能夠看出。建立一個store須要三個參數;

//1.接受的rootReducer
//2.初始化的狀態
//3.dispatch的加強器(中間件)
const createStore = (reducer,initialState,enhancer) => {
    
};
複製代碼

createStore還返回一些列函數接口提供調用

const crateStore = (reducer, initialState, enhancer) => {
    
    return {
        getState,
        dispatch,
        subscribe,
        replaceReducer,
    }
}
複製代碼

如下代碼都是在createStore內部

getState的實現

getStore方法的做用就是返回當前的store

let state = initialState;
const getState = () => {
    return state;
}
複製代碼

subscribe的實現

subscribecreateStore的訂閱者,開發者經過這個方法訂閱,當store改變的時候執行監聽函數。subscribe是典型的高階函數,它的返回值是一個函數,執行該函數移除當前監聽函數。

//建立一個監聽時間隊列
let subQueue = [];

const subscribe = (listener) => {
    //把監聽函數放入到監聽隊列裏面
    subQueue.push(listener);
    return () => {
        //找到當前監聽函數的索引
        let idx = subQueue.indexOf(listener);
        if(idx > -1){
            //經過監聽函數的索引把監聽函數移除掉。
            subQueue.splice(idx,1);
        }
    }
}
複製代碼

dispatch的實現

dispatchcreateStore的發佈者,dispatch接受一個action,來執行reducerdispatch在執行reducer的同時會執行全部的監聽函數(也就是發佈)。

let currentReducer = reducer;
let isDispatch = false;
const dispatch = (action) => {
    //這裏使用isDispatch作標示,就是說只有當上一個派發完成以後才能派發下一個
    if(isDispatch){
        throw new Error("dispatch error");
    }
    try{
        state = currentReducer(state,action);
        isDispatch = true;
    }finally{
        isDispatch = false;
    }
    
    //執行全部的監聽函數
    subQueue.forEach(sub => sub.apply(null));
    return action;
}
複製代碼

replaceReducer

replaceReducer顧名思義就是替換reducer的意思。再執行createState方法的時候reducer就做爲第一個參數傳進去,若是後面想要從新換一個reducer,來代碼寫一下。

const replaceReducer = (reducer) => {
    //傳入一個reduce做爲參數,把它賦予currentReducer就能夠了。
    currentReducer = reducer;
    //更該以後會派發一次dispatch,爲何會派發等下再說。
    dispatch({type:"REPLACE"});
}
複製代碼

dispatch({type:"INIT"});

上面已經實現了createStore的四個方法,剩下的就是replaceReducer中莫名的派發了一個typeREPLACEaction,並且翻到源碼的最後,也派發一個typeINITaction,爲何呢?

其實當使用createStore建立Store的時候,咱們都知道,第一個參數爲reducer,第二個參數爲初始化的state。當若是不寫第二個參數的時候,咱們再來看一下reducer的寫法

const reducer = (state = {}, action){
    switch(action.type){
        default:
            return state;
    }
}
複製代碼

通常在寫reducer的時候都會給state寫一個默認值,而且default出默認的state。當createStore不存在,這個默認值如何存儲在Store中呢?就是這個最後派發的type:INIT的做用。在replaceReducer中派發也是這個緣由,更換reducer後派發。

完整的createStore

如今已經實現的差很少了,只要再加一些容錯就能夠了。

/** * * @param {*} reducer //reducer * @param {*} initState //初始狀態 * @param {*} middleware //中間件 */
const createStore = (reducer, initState,enhancer) => {

    let initialState;       //用於保存狀態
    let currentReducer = reducer;        //reducer
    let listenerQueue = []; //存放全部的監聽函數
    let isDispatch = false;

    if(initState){
        initialState = initState;
    }

    if(enhancer){
        return enhancer(createStore)(reducer,initState);
    }
    /** * 獲取Store */
    const getState = () => {
        //判斷是否正在派發
        if(isDispatch){
            throw new Error('dispatching...')
        }
        return initialState;
    }

    /** * 派發action 並觸發全部的listeners * @param {*} action */
    const dispatch = (action) => {
        //判斷是否正在派發
        if(isDispatch){
            throw new Error('dispatching...')
        }
        try{
           isDispatch = true;
           initialState = currentReducer(initialState,action);
        }finally{
            isDispatch = false;
        }
        //執行全部的監聽函數
        for(let listener of listenerQueue){
            listener.apply(null);
        }
    }
    /** * 訂閱監聽 * @param {*} listener */
    const subscribe = (listener) => {
        listenerQueue.push(listener);
        //移除監聽
        return function unscribe(){
            let index = listenerQueue.indexOf(listener);
            let unListener = listenerQueue.splice(index,1);
            return unListener;
        }
    }

    /** * 替換reducer * @param {*} reducer */
    const replaceReducer = (reducer) => {
        if(reducer){
            currentReducer = reducer;
        }
        dispatch({type:'REPLACE'});

    }
    dispatch({type:'INIT'});
    return {
        getState,
        dispatch,
        subscribe,
        replaceReducer
    }
}

export default createStore;
複製代碼

compose

redux中提供了一個組合函數,若是你知道函數式編程的話,那麼對compose必定不陌生。若是不瞭解的話,那我說一個場景確定就懂了。

//有fn1,fn2,fn3這三個函數,寫出一個compose函數實現一下功能
//1. compose(fn1,fn2,fn3) 從右到左執行。
//2. 上一個執行函數的結果做爲下一個執行函數的參數。
const compose = (...) => {
    
}
複製代碼

上面的需求就是compose函數,也是一個常考的面試題。如何實現實現一個compose?一步一步來。

首先compose接受的是一系列函數。

const compose = (...fns) => {
    
}
複製代碼

從右到左執行,咱們採用數組的reduce方法,利用惰性求值的方式。

const compose = (...fns) => fns.reduce((f,g) => (...args) => f(g(args)));
複製代碼

這就是一個compose函數。

揭開中間件的祕密-applayMiddleware

redux中的中間件就是對dispatch的一種加強,在createStore中實現這個東西很簡單。源碼以下:

const createStore = (reducer,state,enhancer) => {
    //判斷第三個參數的存在。
    if(enhancer && type enhancer === 'function') {
        //知足enhance存在的條件,直接return,組織後面的運行。
        //經過柯里化的方式傳參
        //爲何傳入createStore?
            //雖然是加強,天然返回以後依然是一個store對象,因此要使用createStore作一些事情。
        //後面兩個參數
            //中間件是加強,必要的reducer和state也必要經過createStore傳進去。
        return enhancer(crateStore)(reducer,state);
    }
}
複製代碼

上面就是中間件再createStore中的實現。

中間件的構建經過applyMiddleware實現,來看一下applyMiddleware是怎麼實現。由上面能夠看出applyMiddleware是一個柯里化函數。

const applyMiddleware = (crateStore) => (...args) => {
    
}
複製代碼

applyMiddleware中須要執行createStore來獲得接口方法。

const applyMiddleware =(...middlewares) => (createStore) => (...args) => {
    let store = createStore(...args);
    //佔位dispatch,避免在中間件過程當中調用
    let dispatch = () => {
        throw new Error('error')
    }
    let midllewareAPI = {
        getState: store.getState,
        dispatch,
    }
    //把middlewareAPI傳入每個中間件中
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    //加強dispatch生成,重寫佔位dispatch,把store的默認dispatch傳進去,
    dispatch = compose(...chain)(store.dispatch);
    
    //最後把加強的dispatch和store返回出去。
    return {
        ...store,
        dispatch
    }
}
複製代碼

上面就是applyMiddleware的實現方法。

如何寫一箇中間件

根據applyMiddleware中間件參數的傳入,能夠想出一個基本的中間件是這樣的:

const middleware = (store) => next => action => {
    //業務邏輯
    //store是傳入的middlewareAPI
    //next是store基礎的dispatch
    //action是dispatch的action
}
複製代碼

這就是一箇中間件的邏輯了。

異步action

在寫邏輯的時候必然會用到異步數據的,咱們知道reducer是純函數,不容許有反作用操做的,從上面到如今也能夠明白整個redux都是函數式編程的思想,是不存在反作用的,那麼異步數據怎麼實現呢?必然是經過applyMiddleware提供的中間件接口實現了。

異步中間件必需要求action是一個函數,根據上面中間件的邏輯,咱們來寫一下。

const middleware = (store) => next => action => {
    if(typeof action === 'function'){
        action(store.dispatch,store.getState);
    }
    next(action);
}
複製代碼

判斷傳入的action是不是一個函數,若是是函數使用加強dispatch,若是不是函數使用普通的dispatch

總結

到此爲止就是我能力範圍內所理解的Redux。我我的認爲,要學習一個東西必定要看一下它的源碼,學習它的思想。技術更新迭代,思想是不變的,無非就是思想的轉變。若是有不對的地方,還望大佬們指點。

相關文章
相關標籤/搜索