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

cTomorrow發表於2019-05-12

前言

react和狀態管理redux是緊密結合的,而本身又沒有任何聯絡。react可以不使用redux管理狀態,redux也可以脫離react獨立存在。隨著react的專案越來越複雜,state變的繁重,各種propstate的轉變讓我們在開發過程中變得頭暈眼花,react本來就是一個專注於UI層的庫,本不應該讓繁雜的propstate的邏輯摻雜進來。於是Flux的架構出現了,Flux架構模式用於抽離reactstate能更好的去構建專案,Flux架構模式的實踐有好多中,顯然redux是成功的。

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

redux

前面提到redux是可以脫離react存在的,這句話的意思是redux並不是依附於react的,即便是用jQuery+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

我當時在學的時候也是有這樣的疑問:為什麼要使純函式來寫,什麼是純函式?

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

明白了純函式,那麼在寫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,為什麼呢?

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

其實當使用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。我個人認為,要學習一個東西一定要看一下它的原始碼,學習它的思想。技術更新迭代,思想是不變的,無非就是思想的轉變。如果有不對的地方,還望大佬們指點。

相關文章