【中高級前端】從零實現 redux 和 react-redux(吐血整理)

1. 前言

在前端圈子有這樣一種說法,Vue 入門最簡單,React 學習曲線太陡,Angular...我仍是選擇狗帶吧。 在 React 誕生之初,Facebook 宣傳這是一個用於前端開發的界面庫,僅僅是一個 View 層。前面咱們也介紹過 React 的組件通訊,在大型應用中,處理好 React 組件通訊和狀態管理就顯得很是重要。 爲了解決這一問題,Facebook 最早提出了單向數據流的 Flux 架構,彌補了使用 React 開發大型網站的不足。html

Flux:前端

image_1dvghf5eo1b121baocp1dbek119.png-10.5kB

隨後,Dan Abramov 受到 Flux 和函數式編程語言 Elm 啓發,開發了 Redux 這個狀態管理庫。 Redux 源碼很是精簡,實現也很巧妙,這篇文章將帶你從零手寫一個 Redux 和 react-redux 庫,以及告訴你該如何設計 Redux 中的 store。 在開始前,我已經將這篇文章的完整代碼都整理到 GitHub 上,你們能夠參考一下。 Redux:simple-redux React-redux:simple-react-reduxreact

2. 狀態管理

2.1 理解數據驅動

在開始講解狀態管理前,咱們先來了解一下現代前端框架都作了些什麼。 以 Vue 爲例子,在剛開始的時候,Vue 官網首頁寫的賣點是數據驅動、組件化、MVVM 等等(如今首頁已經改版了)。 那麼數據驅動的意思是什麼呢?無論是原生 JS 仍是 jQuery,他們都是經過直接修改 DOM 的形式來實現頁面刷新的。 而 Vue/React 之類的框架不是粗暴地直接修改 DOM,而是經過修改 data/state 中的數據,實現了組件的從新渲染。也就是說,他們封裝了從數據變化到組件渲染這一個過程。git

image_1e3c2ig3oj2m1d9d10m31ugf1tmo9.png-32.2kB

本來咱們用 jQuery 開發應用,除了要實現業務邏輯,還要操做 DOM 來手動實現頁面的更新。尤爲是涉及到渲染列表的時候,更新起來很是麻煩。github

var ul = document.getElementById("todo-list");
$.each(todos, function(index, todo) {
    var li = document.createElement('li');
    li.innerHTML = todo.content;
    li.dataset.id = todo.id;
    li.className = "todo-item";
    ul.appendChild(li);
})
複製代碼

因此後來出現了 jQuery.tpl 和 Underscore.template 之類的模板,這些讓操做 DOM 變得容易起來,有了數據驅動和組件化的雛形,惋惜咱們仍是要手動去渲染一遍。chrome

<script type="text/template" id="tpl">
    <ul id="todo-list">
        <% _.each(todos, function(todo){ %>
            <li data-id="<%=todo.id%>" class="todo-item">
                <%= todo.content %>
            </li>
        <% }); %>
    </ul>
</script>
複製代碼

若是說用純原生 JS 或者 jQuery 開發頁面是原始農耕時代,那麼 React/Vue 等現代化框架則是自動化的時代。 有了前端框架以後,咱們不須要再去關注怎麼生成和修改 DOM,只須要關心頁面上的這些數據以及流動。 因此如何管理好這些數據流動就成了重中之重,這也是咱們常說的「狀態管理」。數據庫

2.2 什麼狀態須要管理?

前面講了不少例子,可狀態管理到底要管理什麼呢?在我看來,狀態管理的通常就是這兩種數據。編程

  1. Domain State Domain State 就是服務端的狀態,這個通常是指經過網絡請求來從服務端獲取到的數據,好比列表數據,一般是爲了和服務端數據保持一致。
{
    "data": {
        "hotels": [
            {
                "id": "31231231",
                "name": "希爾頓",
                "price": "1300"
            }
        ]
    }
}
複製代碼
  1. UI State UI State 經常和交互相關。例如模態框的開關狀態、頁面的 loading 狀態、單(多)選項的選中狀態等等,這些狀態經常分散在不一樣的組件裏面。
{
    "isLoading": true,
    "isShowModal": false,
    "isSelected": false
}
複製代碼

2.3 全局狀態管理

咱們用 React 寫組件的時候,若是須要涉及到兄弟組件通訊,常常須要將狀態提高到二者父組件裏面。一旦這種組件通訊多了起來,數據管理就是個問題。 結合上面的例子,若是想要對應用的數據流進行管理,那是否是能夠將全部的狀態放到頂層組件中呢? 將數據按照功能或者組件來劃分,將多個組件共享的數據單獨放置,這樣就造成了一個大的樹形 store。這裏更建議按照功能來劃分。redux

image_1e3c34ubd19i0edh191pdit1tp3m.png-34.9kB

這個大的 store 能夠放到頂層組件中維護,也能夠放到頂層組件以外來維護,這個頂層組件咱們通常稱之爲「容器組件」。 容器組件能夠將組件依賴的數據以及修改數據的方法一層層傳給子組件。 咱們能夠將容器組件的 state 按照組件來劃分,如今這個 state 就是整個應用的 store。將修改 state 的方法放到 actions 裏面,按照和 state 同樣的結構來組織,最後將其傳入各自對應的子組件中。segmentfault

class App extends Component {
    constructor(props) {
        this.state = {
            common: {},
            headerProps: {},
            bodyProps: {
                sidebarProps: {},
                cardProps: {},
                tableProps: {},
                modalProps: {}
            },
            footerProps: {}
        }
        this.actions = {
            header: {
                changeHeaderProps: this.changeHeaderProps
            },
            footer: {
                changeFooterProps: this.changeFooterProps
            },
            body: {
                sidebar: {
                    changeSiderbarProps: this.changeSiderbarProps
                }
            }
        }
    }
    
    changeHeaderProps(props) {
        this.setState({
            headerProps: props
        })
    }
    changeFooterProps() {}
    changeSiderbarProps() {}
    ...
    
    render() {
        const {
            headerProps,
            bodyProps,
            footerProps
        } = this.state;
        const {
            header,
            body,
            footer
        } = this.actions;
        return (
            <div className="main">
                <Header {...headerProps} {...header} />
                <Body {...bodyProps} {...body} />
                <Footer {...footerProps} {...footer} />
            </div>
        )
    }
}
複製代碼

咱們能夠看到,這種方式能夠很完美地解決子組件之間的通訊問題。只須要修改對應的 state 就好了,App 組件會在 state 變化後從新渲染,子組件接收新的 props 後也跟着渲染。

image_1e3c3kn7n17tg1q54g6j4op74k13.png-70.2kB

這種模式還能夠繼續作一些優化,好比結合 Context 來實現向深層子組件傳遞數據。

const Context = createContext(null);
class App extends Component {
    ...
    render() {
        return (
            <div className="main"> <Context.Provider value={...this.state, ...this.events}> <Header /> <Body /> <Footer /> </Context.Provider> </div> ) } } const Header = () => { // 獲取到 Context 數據 const context = useContext(Context); } 複製代碼

若是你已經接觸過 Redux 這個狀態管理庫,你會驚奇地發現,若是咱們把 App 組件中的 state 移到外面,這不就是 Redux 了嗎? 沒錯,Redux 的核心原理也是這樣,在組件外部維護一個 store,在 store 修改的時候會通知全部被 connect 包裹的組件進行更新。這個例子能夠看作 Redux 的一個雛形。

3. 實現一個 Redux

根據前面的介紹咱們已經知道了,Redux 是一個狀態管理庫,它並不是綁定於 React 使用,你還能夠將其和其餘框架甚至原生 JS 一塊兒使用,好比這篇文章:如何在非 React 項目中使用 Redux

Redux 工做原理:

image_1e3bj67tdkstv46np1bgfcibm.png-40.8kB

在學習 Redux 以前須要先理解其工做原理,通常來講流程是這樣的:

  1. 用戶觸發頁面上的某種操做,經過 dispatch 發送一個 action。
  2. Redux 接收到這個 action 後經過 reducer 函數獲取到下一個狀態。
  3. 將新狀態更新進 store,store 更新後通知頁面從新渲染。

從這個流程中不難看出,Redux 的核心就是一個 發佈-訂閱 模式。一旦 store 發生了變化就會通知全部的訂閱者,view 接收到通知以後會進行從新渲染。

Redux 有三大原則:

  • 單一數據源

    前面的那個例子,最終將全部的狀態放到了頂層組件的 state 中,這個 state 造成了一棵狀態樹。在 Redux 中,這個 state 則是 store,一個應用中通常只有一個 store。

  • State 是隻讀的

    在 Redux 中,惟一改變 state 的方法是觸發 action,action 描述了此次修改行爲的相關信息。只容許經過 action 修改可使應用中的每一個狀態修改都很清晰,便於後期的調試和回放。

  • 經過純函數來修改

    爲了描述 action 使狀態如何修改,須要你編寫 reducer 函數來修改狀態。reducer 函數接收前一次的 state 和 action,返回新的 state。不管被調用多少次,只要傳入相同的 state 和 action,那麼就必定返回一樣的結果。

關於 Redux 的用法,這裏不作詳細講解,建議參考阮一峯老師的《Redux 入門》系列的教程:Redux 入門教程

3.1 實現 store

在 Redux 中,store 通常經過 createStore 來建立。

import { createStore } from 'redux'; 
const store = createStore(rootReducer, initalStore, middleware);
複製代碼

先看一下 Redux 中暴露出來的幾個方法。

image_1e3bjfhpfplfnea198bj7lf5e13.png-40.9kB

其中 createStore 返回的方法主要有 subscribedispatchreplaceReducergetState

createStore 接收三個參數,分別是 reducers 函數、初始值 initalStore、中間件 middleware。

store 上掛載了 getStatedispatchsubscribe 三個方法。

getState 是獲取到 store 的方法,能夠經過 store.getState() 獲取到 store

dispatch 是發送 action 的方法,它接收一個 action 對象,通知 store 去執行 reducer 函數。

subscribe 則是一個監聽方法,它能夠監聽到 store 的變化,因此能夠經過 subscribe 將 Redux 和其餘框架結合起來。

replaceReducer 用來異步注入 reducer 的方法,能夠傳入新的 reducer 來代替當前的 reducer。

3.2 實現 getState

store 的實現原理比較簡單,就是根據傳入的初始值來建立一個對象。利用閉包的特性來保留這個 store,容許經過 getState 來獲取到 store。 之因此經過 getState 來獲取 store 是爲了獲取到當前 store 的快照,這樣便於打印日誌以對比先後兩次 store 變化,方便調試。

const createStore = (reducers, initialState, enhancer) => {
    let store = initialState;
    const getState = () => store;
    return {
        getState
    }
}
複製代碼

固然,如今這個 store 實現的比較簡單,畢竟 createStore 還有兩個參數沒用到呢。 先別急,這倆參數後面會用到的。

3.3 實現 subscribe && unsubscribe

既然 Redux 本質上是一個 發佈-訂閱 模式,那麼就必定會有一個監聽方法,相似 jQuery 中的 $.on,在 Redux 中提供了監聽和解除監聽的兩個方法。 實現方式也比較簡單,使用一個數組來保存全部監聽的方法。

const createStore = (...) => {
    ...
    let listeners = [];
    const subscribe = (listener) => {
        listeners.push(listener);
    }
    const unsubscribe = (listener) => {
        const index = listeners.indexOf(listener)
        listeners.splice(index, 1)
    }
}
複製代碼

3.4 實現 dispatch

dispatch 和 action 是息息相關的,只有經過 dispatch 才能發送 action。而發送 action 以後纔會執行 subscribe 監聽到的那些方法。 因此 dispatch 作的事情就是將 action 傳給 reducer 函數,將執行後的結果設置爲新的 store,而後執行 listeners 中的方法。

const createStore = (reducers, initialState) => {
    ...
    let store = initialState;
    const dispatch = (action) => {
        store = reducers(store, action);
        listeners.forEach(listener => listener())
    }
}
複製代碼

這樣就好了嗎?固然還不夠。若是有多個 action 同時發送,這樣很難說清楚最後的 store 究竟是什麼樣的,因此須要加鎖。在 Redux 中 dispatch 執行後的返回值也是當前的 action。

const createStore = (reducers, initialState) => {
    ...
    let store = initialState;
    let isDispatch = false;
    const dispatch = (action) => {
        if (isDispatch) return action
        // dispatch必須一個個來
        isDispatch = true
        store = reducers(store, action);
        isDispatch = false
        listeners.forEach(listener => listener())
        return action;
    }
}
複製代碼

至此爲止,Redux 工做流程的原理就已經實現了。但你可能還會有不少疑問,若是沒有傳 initialState,那麼 store 的默認值是什麼呢?若是傳入了中間件,那麼又是什麼工做原理呢?

3.5 實現 combineReducers

在剛開始接觸 Redux 的 store 的時候,咱們都會有一種疑問,store 的結構到底是怎麼定的?combineReducers 會揭開這個謎底。 如今來分析 createStore 接收的第一個參數,這個參數有兩種形式,一種直接是一個 reducer 函數,另外一個是用 combineReducers 把多個 reducer 函數合併到一塊兒。

image_1e3c3pvnlknf7f5139n1fuqu7j1g.png-48.3kB

能夠猜想 combineReducers 是一個高階函數,接收一個對象做爲參數,返回了一個新的函數。這個新的函數應當和普通的 reducer 函數傳參保持一致。

const combineReducers = (reducers) => {
    return function combination(state = {}, action) {
    }
}
複製代碼

那麼 combineReducers 作了什麼工做呢?主要是下面幾步:

  1. 收集全部傳入的 reducer 函數
  2. 在 dispatch 中執行 combination 函數時,遍歷執行全部 reducer 函數。若是某個 reducer 函數返回了新的 state,那麼 combination 就返回這個 state,不然就返回傳入的 state。
const combineReducers = reducers => {
    const finalReducers = {},
        nativeKeys = Object.keys
    // 收集全部的 reducer 函數
    nativeKeys(reducers).forEach(reducerKey => {
        if(typeof reducers[reducerKey] === "function") {
            finalReducers[reducerKey] = reducers[reducerKey]
        }
    })
    return function combination(state, action) {
        let hasChanged = false;
        const store = {};
        // 遍歷執行 reducer 函數
        nativeKeys(finalReducers).forEach(key => {
            const reducer = finalReducers[key];
            // 很明顯,store 的 key 來源於 reducers 的 key 值
            const nextState = reducer(state[key], action)
            store[key] = nextState
            hasChanged = hasChanged || nextState !== state[key];
        })
        return hasChanged ? nextState : state;
    }
}
複製代碼

細心的童鞋必定會發現,每次調用 dispatch 都會執行這個 combination 的話,那豈不是無論我發送什麼類型的 action,全部的 reducer 函數都會被執行一遍? 若是 reducer 函數不少,那這個執行效率不會很低嗎?但不執行貌似又沒法徹底匹配到 switch...case 中的 action.type。 若是能經過鍵值對的形式來匹配 action.type 和 reducer 是否是效率更高一些?相似這樣:

// redux
const count = (state = 0, action) => {
    switch(action.type) {
        case 'increment':
            return state + action.payload;
        case 'decrement':
            return state - action.payload;
        default:
            return state;
    }
}
// 改進後的
const count = {
    state: 0, // 初始 state
    reducers: {
        increment: (state, payload) => state + payload,
        decrement: (state, payload) => state - payload
    }
}
複製代碼

這樣每次發送新的 action 的時候,能夠直接用 reducers 下面的 key 值來匹配了,無需進行暴力的遍歷。 天啊,你實在太聰明瞭。小聲告訴你,社區中一些類 Redux 的方案就是這樣作的。以 rematch 和 relite 爲例: rematch:

import { init, dispatch } from "@rematch/core";
import delay from "./makeMeWait";

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) => state + payload,
    decrement: (state, payload) => state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload);
    }
  }
};

const store = init({
  models: { count }
});

dispatch.count.incrementAsync(1);
複製代碼

relite:

const increment = (state, payload) => {
    state.count = state.count + payload;
    return state;
}
const decrement = (state, payload) => {
    state.count = state.count - payload;
    return state;
}
複製代碼

3.6 中間件 和 Store Enhancer

考慮到這樣的狀況,我想要打印每次 action 的相關信息以及 store 先後的變化,那我只能到每一個 dispatch 處手動打印信息,這樣繁瑣且重複。 createStore 中提供的第三個參數,能夠實現對 dispatch 函數的加強,咱們稱之爲 Store EnhancerStore Enhancer 是一個高階函數,它的結構通常是這樣的:

const enhancer = () => {
    return (createStore) => (reducer, initState, enhancer) => {
        ...
    }
}
複製代碼

enhancer 接收 createStore 做爲參數,最後返回的是一個增強版的 store,本質上是對 dispatch 函數進行了擴展。 logger:

const logger = () => {
    return (createStore) => (reducer, initState, enhancer) => {
        const store = createStore(reducer, initState, enhancer);
        const dispatch = (action) => {
            console.log(`action=${JSON.stringify(action)}`);
            const result = store.dispatch(action);
            const state = store.getState();
            console.log(`state=${JSON.stringify(state)}`);
            return result;
        }
        return {
            ...state,
            dispatch
        }
    }
}
複製代碼

createStore 中如何使用呢?通常在參數的時候,會直接返回。

const createStore = (reducer, initialState, enhancer) => {
    if (enhancer && typeof enhancer === "function") {
        return enhancer(createStore)(reducer, initialState)
    }
}
複製代碼

若是你有看過 applyMiddleware 的源碼,會發現這二者實現方式很類似。applyMiddleware 本質上就是一個 Store Enhancer

3.7 實現 applyMiddleware

在建立 store 的時候,常常會使用不少中間件,經過 applyMiddleware 將多箇中間件注入到 store 之中。

const store = createStore(reducers, initialStore, applyMiddleware(thunk, logger, reselect));
複製代碼

applyMiddleware 的實現相似上面的 Store Enhancer。因爲多箇中間件能夠串行使用,所以最終會像洋蔥模型同樣,action 傳遞須要通過一個個中間件處理,因此中間件作的事情就是加強 dispatch 的能力,將 action 傳遞給下一個中間件。 那麼關鍵就是將新的 store 和 dispatch 函數傳給下一個中間件。

image_1e3c4b74o17qbbkg1l10e7t1p8h1t.png-15.1kB

來看一下 applyMiddleware 的源碼實現:

const applyMiddleware = (...middlewares) => {
    return (createStore) => (reducer, initState, enhancer) => {
        const store = createStore(reducer, initState, enhancer)
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        let chain = middlewares.map(middleware => middleware(middlewareAPI))
        store.dispatch = compose(...chain)(store.dispatch)
        return {
          ...store,
          dispatch
        }
      }
}
複製代碼

這裏用到了一個 compose 函數,compose 函數相似管道,能夠將多個函數組合起來。compose(m1, m2)(dispatch) 等價於 m1(m2(dispatch))。 使用 reduce 函數能夠實現函數組合。

const compose = (...funcs) => {
    if (!funcs) {
        return args => args
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))
}
複製代碼

再來看一下 redux-logger 中間件的精簡實現,會發現二者剛好能匹配到一塊兒。

function logger(middlewareAPI) {
  return function (next) { // next 即 dispatch
    return function (action) {
      console.log('dispatch 前:', middlewareAPI.getState());
      var returnValue = next(action);
      console.log('dispatch 後:', middlewareAPI.getState(), '\n');
      return returnValue;
    };
  };
}
複製代碼

至此爲止,Redux 的基本原理就很清晰了,最後整理一個精簡版的 Redux 源碼實現。

// 這裏須要對參數爲0或1的狀況進行判斷
const compose = (...funcs) => {
    if (!funcs) {
        return args => args
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))
}

const bindActionCreator = (action, dispatch) => {
    return (...args) => dispatch(action(...args))
}

const createStore = (reducer, initState, enhancer) => {
    if (!enhancer && typeof initState === "function") {
        enhancer = initState
        initState = null
    }
    if (enhancer && typeof enhancer === "function") {
        return enhancer(createStore)(reducer, initState)
    }
    let store = initState, 
        listeners = [],
        isDispatch = false;
    const getState = () => store
    const dispatch = (action) => {
        if (isDispatch) return action
        // dispatch必須一個個來
        isDispatch = true
        store = reducer(store, action)
        isDispatch = false
        listeners.forEach(listener => listener())
        return action
    }
    const subscribe = (listener) => {
        if (typeof listener === "function") {
            listeners.push(listener)
        }
        return () => unsubscribe(listener)
    }
    const unsubscribe = (listener) => {
        const index = listeners.indexOf(listener)
        listeners.splice(index, 1)
    }
    return {
        getState,
        dispatch,
        subscribe,
        unsubscribe
    }
}

const applyMiddleware = (...middlewares) => {
    return (createStore) => (reducer, initState, enhancer) => {
        const store = createStore(reducer, initState, enhancer);
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        let chain = middlewares.map(middleware => middleware(middlewareAPI))
        store.dispatch = compose(...chain)(store.dispatch)
        return {
          ...store
        }
      }
}

const combineReducers = reducers => {
    const finalReducers = {},
        nativeKeys = Object.keys
    nativeKeys(reducers).forEach(reducerKey => {
        if(typeof reducers[reducerKey] === "function") {
            finalReducers[reducerKey] = reducers[reducerKey]
        }
    })
    return (state, action) => {
        const store = {}
        nativeKeys(finalReducers).forEach(key => {
            const reducer = finalReducers[key]
            const nextState = reducer(state[key], action)
            store[key] = nextState
        })
        return store
    }
}
複製代碼

4. 實現一個 react-redux

若是想要將 Redux 結合 React 使用的話,一般可使用 react-redux 這個庫。 看過前面 Redux 的原理後,相信你也知道 react-redux 是如何實現的了吧。 react-redux 一共提供了兩個 API,分別是 connect 和 Provider,前者是一個 React 高階組件,後者是一個普通的 React 組件。react-redux 實現了一個簡單的***發佈-訂閱***庫,來監聽當前 store 的變化。 二者的做用以下:

  1. Provider:將 store 經過 Context 傳給後代組件,註冊對 store 的監聽。
  2. connect:一旦 store 變化就會執行 mapStateToProps 和 mapDispatchToProps 獲取最新的 props 後,將其傳給子組件。

image_1e3bk2sig1tt81eid19d61h4koso1t.png-36.9kB

使用方式:

// Provider
ReactDOM.render({
    <Provider store={store}></Provider>,
    document.getElementById('app')
})
// connect
@connect(mapStateToProps, mapDispatchToProps)
class App extends Component {}
複製代碼

4.1 實現 Provider

先來實現簡單的 Provider,已知 Provider 會使用 Context 來傳遞 store,因此 Provider 直接經過 Context.Provider 將 store 給子組件。

// Context.js
const ReactReduxContext = createContext(null);

// Provider.js
const Provider = ({ store, children }) => {
    return (
        <ReactReduxContext.Provider value={store}>
            {children}
        </ReactReduxContext.Provider>
    )
}
複製代碼

Provider 裏面還須要一個***發佈-訂閱器***。

class Subscription {
    constructor(store) {
        this.store = store;
        this.listeners = [this.handleChangeWrapper];
    }
    notify = () => {
        this.listeners.forEach(listener => {
            listener()
        });
    }
    addListener(listener) {
        this.listeners.push(listener);
    }
    // 監聽 store
    trySubscribe() {
        this.unsubscribe = this.store.subscribe(this.notify);
    }
    // onStateChange 須要在組件中設置
    handleChangeWrapper = () => {
        if (this.onStateChange) {
          this.onStateChange()
        }
    }
    unsubscribe() {
        this.listeners = null;
        this.unsubscribe();
    }
}

複製代碼

將 Provider 和 Subscription 結合到一塊兒,在 useEffect 裏面註冊監聽。

// Provider.js
const Provider = ({ store, children }) => {
    const contextValue = useMemo(() => {
        const subscription = new Subscription(store);
        return {
            store,
            subscription
        }
    }, [store]);
    // 監聽 store 變化
    useEffect(() => {
        const { subscription } = contextValue;
        subscription.trySubscribe();
        return () => {
            subscription.unsubscribe();
        }
    }, [contextValue]);
    return (
        <ReactReduxContext.Provider value={contextValue}>
            {children}
        </ReactReduxContext.Provider>
    )
}
複製代碼

4.2 實現 connect

再來看 connect 的實現,這裏主要有三步:

  1. 使用 useContext 獲取到傳入的 store 和 subscription。
  2. 對 subscription 添加一個 listener,這個 listener 的做用就是一旦 store 變化就從新渲染組件。
  3. store 變化以後,執行 mapStateToProps 和 mapDispatchToProps 兩個函數,將其和傳入的 props 進行合併,最終傳給 WrappedComponent。

image_1e3c4uqjk1gvc2n148g24q1tgk2a.png-52.3kB

先來實現簡單的獲取 Context。

const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
    return function Connect(props) {
        const { store, subscription } = useContext(ReactReduxContext);
        return <WrappedComponent {...props} />
    }
}
複製代碼

接下來就要來實現如何在 store 變化的時候更新這個組件。 咱們都知道在 React 中想實現更新組件只有手動設置 state 和調用 forceUpdate 兩種方法,這裏使用 useState 每次設置一個 count 來觸發更新。

const connect = (mapStateToProps, mapDispatchToProps) => {
    return (WrappedComponent) => {
        return (props) => {
            const { store, subscription } = useContext(ReactReduxContext);
            const [count, setCount] = useState(0)
            useEffect(() => {
                subscription.onStateChange = () => setCount(count + 1)
            }, [count])
            const newProps = useMemo(() => {
                const stateProps = mapStateToProps(store.getState()),
                    dispatchProps = mapDispatchToProps(store.dispatch);
                return {
                    ...stateProps,
                    ...dispatchProps,
                    ...props
                }
            }, [props, store, count])
            return <WrappedComponent {...newProps} />
        }
    }
}
複製代碼

react-redux 的原理和上面比較相似,這裏只做爲學習原理的一個例子,不建議用到生產環境中。

5. 如何設計 store

在開發中,若是想要查看當前頁面的 store 結構,可使用 Redux-DevTools 或者 React Developer Tools 這兩個 chrome 插件來查看。 前者通常用於開發環境中,能夠將 store 及其變化可視化展現出來。後者主要用於 React,也能夠查看 store。 關於 Redux 中 store 如何設計對初學者來講一直都是難題,在我看來這不只是 Redux 的問題,在任何前端 store 設計中應該都是同樣的。

5.1 store 設計誤區

這裏以知乎的問題頁 store 設計爲例。在開始以前,先安裝 React Developer Tools,在 RDT 的 Tab 選中根節點。

image_1dvtb7mgm1d8rrsi3kfuh3lrpm.png-107.3kB

而後在 Console 裏面輸入 $r.state.store.getState(),將 store 打印出來。

image_1dvt9vggc1kntcuh18uq4fd1bn99.png-276.9kB

能夠看到 store 中有一個 entities 屬性,這個屬性中分別有 users、questions、answer 等等。

這是一個問題頁,天然包括問題、回答、回答下面的評論 等等。

image_1dvtbehkr1cbrivcm1splb1ttv13.png-147.9kB

通常狀況下,這裏應該是當進入頁面的時候,根據 question_id 來分批從後端獲取到全部的回答。點開評論的時候,會根據 answer_id 來分批從後端獲取到全部的評論。 因此你可能會想到 store 結構應當這樣設計,就像俄羅斯套娃同樣,一層套着一套。

{
    questions: [
        {
            content: 'LOL中哪一個英雄最能表達出你對刺客的想象?',
            question_id: '1',
            answers: [
                {   
                    answer_id: '1-1',
                    content: '我就是來提名一個已經式微的英雄的。沒錯,就是提莫隊長...'
                    comments: [
                        {  
                            comment_id: '1-1-1',
                            content: '言語精煉,每一句話都是一幅畫面,一組鏡頭'
                        }
                    ]
                }
            ]
        }
    ]
}
複製代碼

看圖能夠更直觀感覺數據結構:

image_1e3bjm5kc1hl11d7r1onc10d01c9d1g.png-25.3kB

這是初學者常常進入的一個誤區,按照 API 來設計 store 結構,這種方法是錯誤的。 以評論區回覆爲例子,如何將評論和回覆的評論關聯起來呢?也許你會想,把回覆的評論當作評論的子評論不就好了嗎?

{
    comments: [
        {
            comment_id: '1-1-1',
            content: '言語精煉,每一句話都是一幅畫面,一組鏡頭',
            children: [
                {
                    comment_id: '1-1-2',
                    content: '我感受是好多畫面,一部電影。。。'
                }
            ]
        },
        {
            comment_id: '1-1-2',
            content: '我感受是好多畫面,一部電影。。。'
        }
    ]
}
複製代碼

這樣挺好的,知足了咱們的需求,但 children 中的評論和 comments 中的評論數據亢餘了。

5.2 扁平化 store

聰明的你必定會想到,若是 children 中只保存 comment_id 不就行了嗎?展現的時候只要根據 comment_id 從 comments 中查詢就好了。 這就是設計 store 的精髓所在了。咱們能夠將 store 當作一個數據庫,store 中的狀態按照領域(domain)來劃分紅一張張數據表。不一樣的數據表之間以主鍵來關聯。 所以上面的 store 能夠設計成三張表,分別是 questions、answers、comments,以它們的 id 做爲 key,增長一個新的字段來關聯子級。

{
    questions: {
        '1': {
            id: '1',
            content: 'LOL中哪一個英雄最能表達出你對刺客的想象?',
            answers: ['1-1']
        }
    },
    answers: {
        '1-1': {
            id: '1-1',
            content: '我就是來提名一個已經式微的英雄的。沒錯,就是提莫隊長...',
            comments: ['1-1-1', '1-1-2']
        }
    },
    comments: {
        '1-1-1': {
            id: '1-1-1',
            content: '言語精煉,每一句話都是一幅畫面,一組鏡頭',
            children: ['1-1-2']
        },
        '1-1-2': {
            id: '1-1-2',
            content: '我感受是好多畫面,一部電影。。。'
        }
    }
}
複製代碼

你會發現數據結構變得很是扁平化,避免了數據亢餘以及嵌套過深的問題。在查找的時候也能夠直接經過 id 來查找,避免了經過索引來查找某一具體項。

6. 推薦閱讀

  1. 解析Twitter前端架構 學習複雜場景數據設計
  2. JSON數據範式化(normalizr)
  3. React+Redux打造「NEWS EARLY」單頁應用

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注公衆號「前端小館」,或者加我微信號「testygy」拉你進羣討論,不按期分享原創知識。
  3. 也看看其它文章

相關文章
相關標籤/搜索