[譯]開發類 redux 庫來理解狀態管理

原文地址javascript

對於想要跳過文章直接看結果的人,我已經把我寫的內容製做成了一個庫:use-simple-state,無任何依賴(除了依賴 react ),只有3kb,至關輕量。html

  • 近幾年,應咱們的 app 增加的須要,web 應用數量增加迅速,隨之而來的還有複雜性。爲了使增長的複雜性易於處理,應用某些新增的技巧和模式使得開發者能夠更簡單的處理以及幫助咱們創建更加健壯的應用。
  • 其中一個複雜性增加的主要領域是管理咱們應用的狀態,所以爲了不這種複雜性,開發者使用了包含更新和獲取 app 狀態的庫。最著名的例子是 redux,它是 flux 模式的一種應用。
  • 一旦開發者開始學習使用像 redux 的庫,他們可能不太瞭解庫的內部運行機制,由於一開始這並不明顯,即便很容易瞭解到這是更新一個全局可用的對象這樣的一個概念。
  • 在這篇文章中,咱們將會從零開始爲 react 應用創建一個咱們本身的狀態管理解決方案。咱們的解決方案最初只有幾行代碼,逐漸增長更高級的特性,最終將相似於 redux。

基本概念

  • 任何狀態管理工具只須要兩件東西:對整個應用均可用的全局狀態,和讀取以及更新它的能力。只有這些,真的。
  • 這裏展現一個狀態管理的簡單例子:
const state = {};

export const getState = () => state;

export const setState = nextState => {
  state = nextState;
};
複製代碼
  • 上面的例子已經儘量的簡單,但它仍然包含了全部的要素:

一個全局可用的用於展示咱們應用狀態的值:state;java

讀取狀態的能力:getState;react

更新狀態的能力:setState。git

  • 上面的例子對於咱們真實應用來講太過簡單,所以接下來咱們將要構建一個能讓 react 可用的解決方案。首先咱們來重構咱們的例子,以讓它在 react 中可用。

react 狀態管理

  • 爲了製做一個咱們以前解決方案的 react 版本,咱們須要應用兩個 react 功能。第一個功能是普通經典類組件,也就是衆所周知的有狀態組件。
  • 第二個功能是 context API,它可讓數據在整個 react 應用可用。context 有兩部分:provider (生產者) 和 consumer (消費者),provider 就像它的名字所說的那樣,給應用提供 context (data 數據),消費者意指當咱們讀取 context 時,咱們就是消費者。
  • 能夠這樣理解 context:若是說 props 是顯式的傳送數據,那麼 context 就是隱式的傳送數據。

建造咱們本身的狀態管理器

  • 如今咱們知道了須要哪些工具,如今只要把它們合在一塊兒就能夠了。咱們準備建立一個上下文環境來存放全局狀態,而後把它的 provider 包裹在一個有狀態組件中,而後用 provider 來管理狀態。github

  • 首先,咱們使用 React.createContext 來建立上下文,它能夠給咱們提供 provider 和 consumer。web

  • import { createContext } from 'react';
    
    const { Provider, Consumer } = createContext();
    複製代碼
  • 接下來咱們須要用有狀態組件包裹咱們的 provider,利用它進行應用狀態的管理。咱們也應該把 consumer 導出爲一個更加準確的名稱。express

  • import React, { Component, createContext } from 'react';
    
    const { Provider, Consumer } = createContext();
    
    export const StateConsumer = Consumer;
    
    export class StateProvider extends Component {
      static defaultProps = {
        state: {}
      };
    
      state = this.props.state;
    
      render () {
        return (
          <Provider value={this.state}> {this.props.children} </Provider>
        );
      }
    }
    複製代碼
  • 在上面的例子中,StateProvider 是接收一個 state 來做爲初始狀態的組件,而且使組件樹中當前組件下面的任何組件均可以訪問到這個屬性。若是沒有提供 state,默認會有一個空對象代替。redux

  • 用咱們的 StateProvider 包裹住根組件:api

  • import { StateProvider } from './stateContext';
    import MyApp from './MyApp';
    
    const initialState = {
      count: 0
    };
    
    export default function Root () {
      return (
        <StateProvider state={initialState}> <MyApp /> </StateProvider>
      );
    }
    複製代碼
  • 在咱們完成上述代碼以後,就能夠做爲一個消費者從 MyApp 的任何地方得到應用的狀態。在這裏咱們會初始化咱們的狀態爲一個有一個 count 屬性的對象,因此不管何時咱們想要當即獲取應用的狀態,咱們就能夠從這裏得到。

  • 消費者使用 render 屬性 來傳遞上下文數據,咱們能夠經過下面的一個函數做爲 StateConsumer 的子組件的例子來查看。state 參數傳遞給函數用以展示咱們應用的當前狀態,做爲咱們的初始狀態,state.count 爲 0.

  • import { StateConsumer } from './stateContext';
    
    export default function SomeCount () {
      return (
        <StateConsumer> {state => ( <p> Count: {state.count} </p> )} </StateConsumer>
      );
    }
    複製代碼
  • 關於 StateConsumer 咱們須要知道的很重要一點是在上下文中它會自動訂閱狀態的改變,所以當咱們的狀態改變後會從新渲染以顯示更新。這就是消費者的默認行爲,咱們暫時還沒作可以用到這個特性的功能。

更新狀態

  • 目前爲止咱們已經能夠讀取應用的狀態,以及在狀態改變時自動更新。如今咱們須要一種更新狀態的方法,爲了作到這一點咱們僅僅只須要在 StateProvider 裏面更新狀態。

  • 你以前可能已經注意到了,咱們給 StateProvider 傳遞了一個 state 屬性,也就是以後會傳遞給組件的 state 屬性。咱們將使用 react 內置的 this.setState 來更新:

  • export class StateProvider extends Component {
      static defaultProps = {
        state: {}
      };
    
      state = this.props.state;
    
      render () {
        return (
          <Provider value={{ state: this.state, setState: this.setState.bind(this) }}> {this.props.children} </Provider>
        );
      }
    複製代碼
  • 繼續保持簡單的風格,咱們只給上下文傳遞 this.setState,這意味着咱們須要稍微改變咱們的上下文傳值,不僅是傳遞 this.state,咱們如今同時傳遞 statesetState

  • 當咱們用 StateConsumer 時能夠用解構賦值獲取 statesetState,而後咱們就能夠讀寫咱們的狀態對象了:

  • export default function SomeCount () {
      return (
        <StateConsumer> {({ state, setState }) => ( <> <p> Count: {state.count} </p> <button onClick={() => setState({ count: state.count + 1 })}> + 1 </button> <button onClick={() => setState({ count: state.count - 1 })}> - 1 </button> </> )} </StateConsumer>
      );
    }
    複製代碼
  • 有一點要注意的是因爲咱們傳遞了 react 內置的 this.setState 做爲咱們的 setState 方法,新增的屬性將會和已有的狀態合併。這意味着若是咱們有 count 之外的第二個屬性,它也會被自動保存。

  • 如今咱們的做品已經能夠用在真實項目中了(儘管還不是頗有效率)。對 react 開發者來講應該會以爲 API 很熟悉,因爲使用了內置的工具所以咱們沒有引用任何新的依賴項。假如以前以爲狀態管理有點神奇,但願如今咱們多少可以瞭解它內部的結構。

華麗的點綴

  • 熟悉 redux 的人可能會注意到咱們的解決方案缺乏一些特性:

    • 沒有內置的處理反作用的方法,你須要經過 redux 中間件來作這件事
    • 咱們的 setState 依賴 react 默認的 this.setState 來處理咱們的狀態更新邏輯,當使用內聯方式更新複雜狀態時將可能引起混亂,同時也沒有內置的方法來複用狀態更新邏輯,也就是 redux reducer 提供的功能。
    • 也沒有辦法處理異步的操做,一般由 redux thunk 或者 redux saga等庫來提供解決辦法。
    • 最關鍵的是,咱們沒辦法讓消費者只訂閱部分狀態,這意味着只要狀態的任何部分更新都會讓每一個消費者更新。
  • 爲了解決這些問題,咱們模仿 redux 來應用咱們本身的 actionsreducers,和 middleware。咱們也會爲異步 actions 增長內在支持。以後咱們將會讓消費者只監聽狀態內的子狀態的改變。最後咱們來看看如何重構咱們的代碼以使用新的 hooks api

redux 簡介

免責聲明:接下來的內容只是爲了讓你更容易理解文章,我強烈推薦你閱讀 redux 官方完整的介紹。

若是你已經很是瞭解 redux,那你能夠跳過這部分。

  • 下面是一個 redux 應用的數據流簡化流程圖:

redux-data-flow

  • 如你所見,這就是單向數據流,從咱們的 reducers 接收到狀態改變以後,觸發 actions,數據不會回傳,也不會在應用的不一樣部分來回流動。

  • 說的更詳細一點:

  • 首先,咱們觸發一個描述改變狀態的 action,例如 dispatch({ type: INCREMENT_BY_ONE }) 來加1,同咱們以前不一樣,以前咱們是經過 setState({ count: count + 1 })來直接改變狀態。

  • action 隨後進入咱們的中間件,redux 中間件是可選的,用於處理 action 反作用,並將結果返回給 action,例如,假如在 action 到達 reducer 以前觸發一個 SIGN_OUT 的 action 用於從本地存儲裏刪除全部用戶數據。若是你熟悉的話,這有些相似於 express 中間件的概念。

  • 最後,咱們的 action 到達了接收它的 reducer,伴隨而來的還有數據,而後利用它和已有的狀態合併生成一個新的狀態。讓咱們觸發一個叫作 ADD 的 action,同時把咱們想發送過去增長到狀態的值也發送過去(叫作 payload )。咱們的 reducer 會查找叫作 ADD 的 action,當它發現後就會將 payload 裏面的值和咱們現有的狀態裏的值加到一塊兒並返回新的狀態。

  • reducer 的函數以下所示:

  • (state, action) => nextState
    複製代碼
  • reducer 應當只是處理 state 和 action ,雖然簡單卻很強大。關鍵是要知道 reducer 應當永遠是純函數,這樣它們的結果就永遠是肯定的。

actions + dispatch

  • 如今咱們已通過了幾個 redux app 的關鍵部分,咱們須要修改 app 來模仿一些相似的行爲。首先,咱們須要一些 actions 和觸發它們的方法。

  • 咱們的 action 會使用 action 建立器來建立,它們其實就是能生成 action 的簡單函數,action 建立器使得測試,複用,傳遞 payload 數據更加簡單,咱們也會建立一些 action type,其實就是字符串常量,爲了讓他們能夠被 reducer 複用,所以咱們把它存儲到變量裏:

  • // Action types
    const ADD_ONE = 'ADD_ONE';
    const ADD_N = 'ADD_N';
    
    // Actions
    export const addOne = () => ({ type: ADD_ONE });
    export const addN = amount => ({ type: ADD_N, payload: amount });
    複製代碼
  • 如今咱們來作一個 dispatch 的佔位符函數,咱們的佔位符只是一個空函數,將會被用於替換上下文中的 setState 函數,咱們一會再回到這兒,由於咱們還沒作接收 action 的 reducer 呢。

  • export class Provider extends React.PureComponent {
      static defaultProps = {
        state: {}
      };
    
      state = this.props.state;
    
      _dispatch = action => {};
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 複製代碼

reducers

  • 如今咱們已經有了一些 action,只須要一些 reducer 來接收就行了。回到以前的 reducer 函數標記,它只是一個關於 action 和 state 的純函數:

  • (state, action) => nextState
    複製代碼
  • 知道了這個,咱們只須要傳遞組件的狀態,而後在 reducer 裏觸發 action。對 reducer 來講,咱們只想要一個對應上面標記的函數數組。咱們之因此使用一個數組是由於可使用數組的 Array.reduce 方法來迭代數組,最終生成咱們的新狀態:

  • export class Provider extends React.PureComponent {
      static defaultProps = {
        state: {},
        reducers: []
      };
    
      state = this.props.state;
    
      _dispatch = action => {
        const { reducers } = this.props;
        const nextState = reducers.reduce((state, reducer) => {
          return reducer(state, action) || state;
        }, this.state);
    
        this.setState(nextState);
      };
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 複製代碼
  • 如你所見,咱們所作的就是使用 reducer 來計算並得到新狀態,而後就像以前所作的,咱們調用 this.setState 來更新 StateProvider 組件的狀態。

  • 如今咱們只須要一個實際的 reducer:

  • function countReducer ({ count, ...state }, { type, payload }) {
      switch (type) {
        case ADD_N:
          return { ...state, count: count + payload };
        case ADD_ONE:
          return { ...state, count: count + 1 };
      }
    }
    複製代碼
  • 咱們的 reducer 只是檢查傳入的 action.type,而後假如匹配到以後將會更新相對應的狀態,不然就會在通過 switch 判斷語句以後返回函數默認的 undefined。咱們的 reducer 和 redux 的 reducer 的一個重要的區別在當咱們不想更新狀態時,通常狀況下咱們會由於未匹配到 action type 而返回一個falsy 值,而 redux 則會返回未變化的狀態。

  • 而後把咱們的 reducer 傳進 StateProvider:

  • export default function Root () {
      return (
        <StateProvider state={initialState} reducers={[countReducer]}> <MyApp /> </StateProvider>
      );
    }
    複製代碼
  • 如今咱們終於能夠觸發一些 action,而後就會觀察到相對應的狀態更新:

  • export default function SomeCount () {
      return (
        <StateConsumer> {({ state, dispatch }) => ( <> <p> Count: {state.count} </p> <button onClick={() => dispatch(addOne())}> + 1 </button> <button onClick={() => dispatch(addN(5))}> + 5 </button> <button onClick={() => dispatch(addN(10))}> + 10 </button> </> )} </StateConsumer>
      );
    
    複製代碼

中間件

  • 如今咱們的做品已經跟 redux 比較像了,只須要再增長一個處理反作用的方法就能夠。爲了達到這個目的,咱們須要容許用戶傳遞中間件函數,這樣當 action 被觸發時就會被調用了。

  • 咱們也想讓中間件函數幫助咱們處理狀態更新,所以假如返回的 null 就不會被 action 傳遞給 reducer。redux 的處理稍微不一樣,在 redux 中間件你須要手動傳遞 action 到下一個緊鄰的中間件,假如沒有使用 redux 的 next 函數來傳遞,action 將不會到達 reducer,並且狀態也不會更新。

  • 如今讓咱們寫一個簡單的中間件,咱們想經過它來尋找 ADD_N action,若是它找到了那就應當把 payload 和當前狀態裏面的 count 加和並輸出,可是阻止實際狀態的更新。

function countMiddleware ({ type, payload }, { count }) {
  if (type === ADD_N) {
    console.log(`${payload} + ${count} = ${payload + count}`);
    return null;
  }
}
複製代碼
  • 跟咱們的 reducer 相似,咱們會將中間件用數組傳進咱們的 StateProvider

  • export default function Root () {
      return (
        <StateProvider state={initialState} reducers={[countReducer]} middleware={[countMiddleware]} > <MyApp /> </StateProvider>
      );
    }
    複製代碼
  • 最終咱們會調用全部全部中間件,而後根據返回的結果決定是否應當阻止更新。因爲咱們傳進了一個數組,然而咱們須要的是一個單個值,所以咱們準備使用 Array.reduce 來得到咱們的值。跟 reducer 相似,咱們也會迭代數組依次調用每一個函數,而後將結果賦值給一個變量 continueUpdate

  • 因爲中間件被認爲是一個高級特性,所以咱們不想它變成強制性的,所以若是沒有在StateProvider 裏面找到 middleware 屬性,咱們會將 continueUpdate 置爲默認的 undefined。咱們也會增長一個 middleware 數組來做默認屬性,這樣的話 middleware.reduce 就不會由於沒傳東西而拋出錯誤。

  • export class StateProvider extends React.PureComponent {
      static defaultProps = {
        state: {},
        reducers: [],
        middleware: []
      };
    
      state = this.props.state;
    
      _dispatch = action => {
        const { reducers, middleware } = this.props;
        const continueUpdate = middleware.reduce((result, middleware) => {
          return result !== null ? middleware(action, this.state) : result;
        }, undefined);
    
        if (continueUpdate !== null) {
          const nextState = reducers.reduce((state, reducer) => {
            return reducer(state, action) || state;
          }, this.state);
    
          this.setState(nextState);
        }
      };
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 複製代碼
  • 如你所見在第13行,咱們會查看中間件函數的返回值。若是返回 null 咱們就會跳過剩下的中間件函數,continueUpdate 將爲 null,意味着咱們會中斷更新。

異步 action

  • 由於咱們想讓咱們的狀態管理器對真實生產環境有用,因此咱們須要增長對異步 action 的支持,這意味着咱們將能夠處理像網絡請求相似案例的通用任務。咱們借鑑下 Redux Thunk ,由於它的 API 很簡單,直觀並且有效。

  • 咱們所要作的就是檢查是否有爲被調用的函數被傳遞到 dispatch,若是找到的話咱們就會在傳遞 dispatchstate 時調用函數,這樣就能夠給用戶所寫的異步 action 執行的機會。拿這個受權 action 做爲例子來看下:

  • const logIn = (email, password) => async dispatch => {
      dispatch({ type: 'LOG_IN_REQUEST' });
    
      try {
        const user = api.authenticateUser(email, password);
        dispatch({ type: 'LOG_IN_SUCCESS', payload: user });
      catch (error) {
        dispatch({ type: 'LOG_IN_ERROR', payload: error });
      }
    };
    複製代碼
  • 在上面的例子中咱們寫了一個叫作 logIn 的 action 建立器,不是返回一個對象,它返回一個接收 dispatch 的函數,這可讓用戶在一個異步 API 請求的前面和後面觸發異步 action,根據 API 不一樣的返回結果觸發不一樣的 action,這裏咱們在發生錯誤時發送一個錯誤 action。

  • 作到這一點只須要在 StateProvider 裏的 _dispatch 方法裏檢查 action 的類型是否是 function:

  • export class StateProvider extends React.PureComponent {
      static defaultProps = {
        state: {},
        reducers: [],
        middleware: []
      };
    
      state = this.props.state;
    
      _dispatch = action => {
        if (typeof action === 'function') {
          return action(this._dispatch, this.state);
        }
        
        const { reducers, middleware } = this.props;
        const continueUpdate = middleware.reduce((result, middleware) => {
          return result !== null ? middleware(action, this.state) : result;
        }, undefined);
    
        if (continueUpdate !== null) {
          const nextState = reducers.reduce((state, reducer) => {
            return reducer(state, action) || state;
          }, this.state);
    
          this.setState(nextState);
        }
      };
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 複製代碼
  • 這裏須要注意兩點:咱們調用 action 爲函數,傳入 this.state,這樣用戶能夠訪問異步 action 中已有的狀態,咱們也會返回函數調用的結果,容許開發者從他們的異步 action 中得到一個返回值從而開啓更多的可能性,例如從 dispatch 觸發的 promise 鏈。

避免沒必要要的從新渲染

  • Redux 的一個常常被忽視的必要特性是它能在必須時纔會對組件從新渲染(或者更準確的說是 React-Redux  — react 跟 redux 的綁定)。爲了作到這一點,它使用了 connect 高階組件,它提供了一個映射函數 — mapStateToProps — 僅僅只在關聯的 mapStateToProps 的輸出改變時(只映射從如今開始的狀態)纔會觸發組件的從新渲染。若是不這樣的話,那麼每次狀態更新都會讓組件使用 connect 來訂閱存儲改變而後從新渲染。

  • 想一想咱們須要作的,咱們須要一種方法來存儲 mapState 前面的輸出,這樣咱們就能夠比較二者看看有沒有差別來決定咱們是否須要繼續向前和從新渲染咱們的組件。爲了作到這一點咱們須要使用一種叫作記憶化的進程,跟咱們這行的許多事情同樣,對於一個想到簡單的進程來講,這是一個重要的詞,尤爲是咱們可使用 React.Component 來存儲咱們狀態的子狀態,而後僅在咱們檢測到 mapState 的輸出改變以後再更新。

  • 接下來咱們須要一種可以跳過沒必要要的組件更新的方法。react 提供了一個生命週期方法 shouldComponentUpdate 可讓咱們達到目的。它將接收到的 props 和 state 當作參數來讓咱們同現有的 props 和 state 進行比較,若是咱們返回 true 那麼更新繼續,若是返回 false 那麼 react 將會跳過渲染。

  • class ConnectState extends React.Component {
      state = this.props.mapState(this.props.state);
    
      static getDerivedStateFromProps (nextProps, nextState) {}
    
      shouldComponentUpdate (nextProps) {
        if (!Object.keys(this.state).length) {
          this.setState(this.props.mapDispatch(this.props.state));
          return true;
        }
    
        console.log({
          s: this.state,
          nextState: nextProps.mapState(nextProps.state),
          state: this.props.mapState(this.state)
        });
        
        return false;
      }
    
      render () {
        return this.props.children({ state: this.props.state, dispatch: this.props.dispatch });
      }
    }
    
    export function StateConsumer ({ mapState, mapDispatch, children }) {
      return (
        <StateContext.Consumer> {({ state, dispatch }) => ( <ConnectState state={state} dispatch={dispatch} mapState={mapState} mapDispatch={mapDispatch}> {children} </ConnectState> )} </StateContext.Consumer> ); } 複製代碼
  • 上面只是對咱們接下來要作的事情的概述。它有了因此主要的部分:從咱們的 context 接收更新,它使用了 getDerivedStateFromPropsshouldComponentUpdate,它也接收一個 render 屬性來做爲子組件,就像默認的消費者同樣。咱們也會經過使用傳遞的 mapState 函數來初始化咱們的消費者初始狀態。

  • 如今這樣的話,shouldComponentUpdate 將只會在接收到第一次狀態更新以後渲染一次。以後它會記錄傳進的狀態和現有的狀態,而後返回 false,阻止任何更新。

  • 上面的解決方案中在 shouldComponentUpdate 內部也調用了 this.setState,而咱們都知道 this.setState 老是會觸發從新渲染。因爲咱們也會從 shouldComponentUpdate 裏返回 true,這會產生一次額外的從新渲染,因此爲了解決這個問題,咱們將使用生命週期 getDerivedStateFromProps 來獲取咱們的狀態,而後咱們再使用 shouldComponentUpdate 來決定基於咱們獲取的狀態是否繼續更新進程。

  • 若是咱們檢查控制檯也能夠看到全局的狀態更新,同時咱們的組件阻止任何更新到 this.state 對象以致組件跳過更新:

  • redux-like-state-update

  • 如今咱們知道了如何阻止沒必要要的更新,咱們還須要一個能夠智能的決定咱們的消費者什麼時候應當更新的方法。若是咱們想要遞歸循環傳進來的 state 對象來查看每一個屬性來看狀態是否有改變,可是這對於幫助咱們理解有幫助卻對性能不利。咱們沒辦法知道傳進來的 state 對象層級有多深或者多複雜,若是條件永遠不知足,那麼遞歸函數將會無限期的執行下去,所以咱們準備限制咱們比較的做用域。

  • 跟 redux 相似,咱們準備使用一個比較函數。在這裏意味着咱們在比較咱們的對象是否相等的屬性的層級,意味着咱們只會比較一層。所以咱們將會檢查咱們每一個新狀態的頂層屬性是否等於咱們現有狀態的同名屬性,若是同名屬性不存在,或者它們的值不一樣,咱們將會繼續渲染,不然咱們就認爲咱們的狀態時相同的,而後阻止渲染。

  • function shallowCompare (state, nextState) {
      if ((typeof state !== 'object' || state === null || typeof nextState !== 'object' || nextState === null)) return false;
    
      return Object.entries(nextState).reduce((shouldUpdate, [key, value]) => state[key] !== value ? true : shouldUpdate, false);
    }
    複製代碼
  • 首先咱們從檢查是否兩個 state 都是對象開始,若是不是那麼咱們就跳過渲染。在初始檢查以後咱們把現有的狀態轉化爲一個鍵值對的數組,並經過將數組減小爲單個布爾值來檢查每一個屬性的值與傳進來的 state 對象的值。

  • 這是困難的部分,如今咱們想用咱們的 shallowCompare 函數,實際上只是調用並檢查結果。若是它返回 true,咱們就返回 true 來容許組件從新渲染,不然咱們就返回 false 來跳過更新(而後咱們得到的狀態被放棄掉)。咱們也想在 mapDispatch 存在的時候調用它。

  • class ConnectState extends React.Component {
      state = {};
    
      static getDerivedStateFromProps ({ state, mapState = s => s }) {
        return mapState(state);
      }
    
      shouldComponentUpdate (nextProps, nextState) {
        return shallowCompare(this.state, nextState);
      }
    
      render () {
        return this.props.children({
          state: this.state,
          dispatch: this.props.mapDispatch ? this.props.mapDispatch(this.props.dispatch) : this.props.dispatch
        });
      }
    }
    複製代碼
  • 最後咱們須要傳遞一個 mapState 函數讓咱們消費者以只匹配咱們的部分狀態,這樣咱們就會將它做爲一個屬性傳給咱們的 StateConsumer

  • return (
      <StateConsumer mapState={state => ({ greeting: state.greeting })} mapDispatch={dispatch => dispatch}> // ... 複製代碼
  • 如今咱們只訂閱 greeting 裏面的改變,所以假如咱們更新了組件裏的 count 將會被咱們的全局狀態改變所忽略而且避免了一次從新渲染。

快速回顧

  • 若是你已經作到了這一步,你就已經見到了如何開發一個帶有 reducer 和 action 的 類 redux 的狀態管理庫。咱們也覆蓋了更高級的特性,例如異步 action,中間件,以及如何讓咱們只接收咱們想要的狀態更新,從而避免咱們的消費者每次全局狀態更新進而引發的從新渲染。
  • 儘管 redux 其實作的比咱們的解決方案要多得多,但願這個方案有助於澄清一些核心概念,而 redux 一般被認爲是一個更加高級的特性,但它的實現其實相對簡單。
  • 想要對 redux 的內部瞭解更加完全,我強烈推薦你閱讀它在 github 的源碼
  • 咱們目前爲止的解決方案已經有了真實項目所必須的工具和特性了。咱們能夠在一個 react 項目中使用它,不須要使用 redux,除非咱們想要接入一些真正高級的功能。

Hooks

  • 若是你還沒聽過它,它正在快速變成 react 的下一個大特性。這裏有一段來自官方描述的簡單解釋:

    Hooks 是一個讓你沒必要寫 class 就可使用 state 和 react 其餘特性的新功能。

  • hooks 提供給咱們高階組件的全部能力,以及更加清晰和直觀的 API 來渲染屬性。

  • 咱們來看一個使用基本的 useState 鉤子的例子來看看它們是如何工做的:

    import React, { useState } from 'react';
    
    function Counter () {
      const [count, setCount] = useState(0);
      return (
        <> <span> Count: {count} </span> <button onClick={() => setCount(count + 1)}> Increment </button> </> ); } 複製代碼
  • 在上面的例子中,咱們經過給 useState 傳遞一個 0 來初始化新狀態,它會返回咱們的狀態:count,以及一個更新函數 setCount。若是你之前沒見過的話,可能會奇怪 useState 是如何不在每次渲染時初始化,這是由於 react 在內部處理了,所以咱們無需擔憂這一點。

  • 讓咱們暫時先忘掉中間件和異步 action,用 useReducer 鉤子來從新應用咱們的 provider,就像咱們正在作的同樣,除了將 action 觸發到一個得到新狀態的 reducer,它就像 useState 同樣工做。

  • 知道了這個,咱們只須要將 reducer 的邏輯從老的 StateProvider 拷貝到新的函數組件 StateProvider裏就能夠了:

    export function StateProvider ({ state: initialState, reducers, middleware, children }) {
      const [state, dispatch] = useReducer((state, action) => {
        return reducers.reduce((state, reducer) => reducer(state, action) || state, state);
      }, initialState);
    
      return (
        <StateContext.Provider value={{ state, dispatch }}> {children} </StateContext.Provider> ); } 複製代碼
  • 能夠如此的簡單,可是當咱們想要保持簡單時,咱們仍然沒有徹底掌握 hooks 的全部能力。咱們也可使用 hooks 來把咱們的 StateConsumer 換爲咱們本身的鉤子,咱們能夠經過包裹 useContext 鉤子來作到:

    const StateContent = createContext();
    
    export function useStore () {
      const { state, dispatch } = useContext(StateContext);
      return [state, dispatch];
    }
    複製代碼
  • 儘管以前當咱們建立咱們的上下文時使用瞭解構的 ProviderConsumer,可是此次咱們會將它存到咱們傳遞到 useContext 單個變量從而讓咱們可不用 Consumer 就能夠接入上下文。咱們也將它命名爲咱們本身的 useStore 鉤子,由於 useState 是一個默認的鉤子。

  • 接下來咱們來簡單地重構下咱們消費上下文數據的方法:

    export default function SomeCount () {
      const [state, dispatch] = useStore();
      return (
        <>
          <p>
            Count: {state.count}
          </p>
          <button onClick={() => dispatch(addOne())}>
            + 1
          </button>
          <button onClick={() => dispatch(addN(5))}>
            + 5
          </button>
          <button onClick={() => dispatch(addN(10))}>
            + 10
          </button>
        </>
      );
    }
    複製代碼
  • 但願這些例子能讓你感覺到 hooks 是如何的直觀、簡單和有效。咱們減小了所需的代碼數量,而且給了咱們一個友好、簡單的 API 供使用。

  • 咱們也想讓咱們的中間件和內置的異步 action 從新開始工做。爲了作到這一點,咱們將咱們的 useReducer 包裹進一個自定義鉤子,在咱們的 StateProvider 中被特殊的使用,而後簡單地重用咱們老的狀態組件的邏輯就行了。

  • export function useStateProvider ({ initialState, reducers, middleware = [] }) {
      const [state, _dispatch] = useReducer((state, action) => {
        return reducers.reduce((state, reducer) => reducer(state, action) || state, state);
      }, initialState);
    
      function dispatch (action) {
        if (typeof action === 'function') {
          return action(dispatch, state);
        }
    
        const continueUpdate = middleware.reduce((result, middleware) => {
          return result !== null ? middleware(action, state) : result;
        }, undefined);
    
        if (continueUpdate !== null) {
          _dispatch(action);
        }
      }
    
      return { state, dispatch };
    }
    複製代碼
  • 在咱們的老的解決方案中,咱們想讓中間件是可選的,因此咱們添加了一個空數組做爲默認值,一樣地此次咱們也使用一個默認的參數來替換默認屬性。相似於咱們老的 dispatch 函數,咱們調用了中間件,而後若是 continueUpdate !== null 咱們就繼續更新狀態。咱們也不會改變處理異步 action 的方式。

  • 最終,咱們將 useStateProvider 的結果和它的參數到咱們的 provider,這咱們以前沒怎麼考慮:

  • export function StateProvider ({ state: initialState, reducers, middleware, children }) {
      return (
        <StateContext.Provider value={useStateProvider({ initialState, reducers, middleware })}> {children} </StateContext.Provider> ); } 複製代碼
  • 完結!

然而...

  • 你可能已經注意到的一件事是咱們對 hooks 的應用可能沒有辦法跳過沒必要要的更新。這是由於 hooks 是在函數組件體內被調用的,在這個階段 react 無法擺脫渲染進程(在不適用一些技巧的前提下)。這沒有必要擔憂,react 團隊已經考慮到這一點並且計劃提供一種方法讓咱們可以在函數組件內部終止更新。
  • 在函數組件內一旦咱們有官方提供的方法來讓咱們擺脫渲染,我就會回到這裏來更新這篇文章。與此同時,我用 hooks 實現的這個庫還有消費者,這樣咱們就能夠訪問此功能。

總結

  • 綜上所述,咱們已經寫完了狀態管理的大部分功能,還逐步在它基礎上增長了一些相似 redux 的功能,包括 action,reducer,中間件以及一種比較狀態差別的方法來提高性能。咱們也看到了可使用新的 hooks API 能夠如何簡化咱們的代碼。
  • 但願你能從這篇文章中得到一些有用的東西,可以對一些高級概念有些許瞭解,同時可讓咱們在使用一些工具時比咱們初次見到它時更加簡單。

在開始的時候提到過,我寫了一個庫,Use Simple State,看完這篇文章以後,你能夠在個人 github頁面看到,我已經使用 hooks 最終實現了,包括幾個新增的功能。

相關文章
相關標籤/搜索