redux 源碼解析 - 從零實(shí)現(xiàn)一個(gè) redux

1. Redux 簡(jiǎn)單介紹

Redux 是 JavaScript 應(yīng)用狀態(tài)容器,提供可預(yù)測(cè)化的狀態(tài)管理。

可以讓你構(gòu)建一致化的應(yīng)用,運(yùn)行于不同的環(huán)境(客戶(hù)端、服務(wù)器、原生應(yīng)用),并且易于測(cè)試。不僅于此,它還提供 超爽的開(kāi)發(fā)體驗(yàn),比如有一個(gè)時(shí)間旅行調(diào)試器可以編輯后實(shí)時(shí)預(yù)覽

2. 為什么要用 redux,什么情況下需要用到 redux

曾經(jīng)有人說(shuō)過(guò)這樣一句話(huà)。

"如果你不知道是否需要 Redux,那就是不需要它。"

Redux 的創(chuàng)造者 Dan Abramov 又補(bǔ)充了一句。

"只有遇到 React 實(shí)在解決不了的問(wèn)題,你才需要 Redux 。"

如果一個(gè)應(yīng)用不是很復(fù)雜,可以通過(guò) react 的內(nèi)部 state 就滿(mǎn)足對(duì)數(shù)據(jù)的管理,那就沒(méi)必要使用 redux。

因?yàn)槭褂?redux 每次寫(xiě)代碼會(huì)額外增加代碼的編寫(xiě)以及可能需要多創(chuàng)建多幾個(gè)文件。

那什么時(shí)候適合用 redux?

  1. 組件樹(shù)龐大,不同的組件節(jié)點(diǎn)需要共享狀態(tài)
  2. 某個(gè)組件需要去修改全局的狀態(tài)或修改其他組件的狀態(tài)
  3. 與后端服務(wù)器有比較多的數(shù)據(jù)交互,并且組件視圖依賴(lài)后端返回的數(shù)據(jù)

使用 redux 帶來(lái)什么好處?

  1. redux 讓 state 的變化變得可預(yù)測(cè),因?yàn)樾薷?state 只能通過(guò) dispatch 一個(gè) action,這個(gè)過(guò)程是可監(jiān)控的,在開(kāi)發(fā)環(huán)境中,結(jié)合 redux-devtools 還可以實(shí)現(xiàn)時(shí)間旅行、錄制、重放等
  2. redux 將 state 統(tǒng)一管理,會(huì)使代碼更加有規(guī)律,易于維護(hù)管理。

3. redux 基礎(chǔ)知識(shí)

3.1 redux API

3.1.1 createStore

createStore 用于創(chuàng)建一個(gè) store 實(shí)例,創(chuàng)建時(shí),第一個(gè)參數(shù)會(huì)傳入一個(gè) reducer 函數(shù),store 保存了 state 數(shù)據(jù),傳入的 reducer 函數(shù)定義了修改 state 的規(guī)則。

3.1.2 store.getState

store.getSate() 返回 store 內(nèi)部的 state 數(shù)據(jù),state 數(shù)據(jù)在外部無(wú)法直接訪(fǎng)問(wèn),必須通過(guò) getState 方法獲取

3.1.3 store.dispatch

store.dispatch() 派發(fā)一個(gè) action,action 是一個(gè)對(duì)象,一般至少包含一個(gè) type 字段,例如:{ type: 'add' },除了 type 字段外,也可以包含其他數(shù)據(jù),type 用于匹配 reducer 中的修改規(guī)則,其他數(shù)據(jù)用于更新 state。

想要修改 state 中的數(shù)據(jù),必須通過(guò) dispatch

3.1.4 store.subscribe

store.subscribe 用于訂閱 store 的數(shù)據(jù)變化,監(jiān)聽(tīng)函數(shù)會(huì)在 dispatch 調(diào)用時(shí)執(zhí)行

3.2 redux flow

redux-flow.png

redux 與 react 結(jié)合使用的數(shù)據(jù)流:

  1. 在 react 組件中調(diào)用 dispatch(action),派發(fā)一個(gè) action,action 是一個(gè)描述 “發(fā)生了什么” 的普通對(duì)象,例如:{ type: 'add', value: 1 },這個(gè)action 對(duì)象可以理解為 “增加數(shù)值 1”
  2. store 取到 action 后,調(diào)用構(gòu)建 store 傳入到 reducer 函數(shù),并且傳入當(dāng)前的 stateaction
  3. reducer 取到 stateaction 后,會(huì)通過(guò) action.type 匹配到修改 state 的規(guī)則,然后修改并返回新的 state
  4. store 保存新的 state,然后所有訂閱了 store.subscribe() 的監(jiān)聽(tīng)器都被調(diào)用,并通過(guò) store.getState() 獲取新的 state,重新渲染組件。
redux state change

4. redux 使用

4.1 reducer 介紹

創(chuàng)建 store 的時(shí)候,需要傳入一個(gè) reducer 函數(shù)。

reducer = (state, action) => {}

Reducer (也稱(chēng)為 reducing function) 函數(shù)接受兩個(gè)參數(shù):

  • state: 之前累積運(yùn)算的結(jié)果和當(dāng)前被累積的值

  • action: 描述了發(fā)生什么事情的一個(gè)對(duì)象,一般帶有 type 字段。例如:{ type: 'ADD' }

返回的是一個(gè)新的累積結(jié)果(state)。

Reducers 指定了應(yīng)用狀態(tài)的變化如何響應(yīng) actions 并發(fā)送到 store 的,記住 actions 只是描述了有事情發(fā)生了這一事實(shí),并沒(méi)有描述應(yīng)用如何更新 state。

reducer 是一個(gè)純函數(shù),即接收的參數(shù)不變的情況下,返回值也不會(huì)發(fā)生變化。

永遠(yuǎn)不要在 reducer 里做這些操作:

  • 修改傳入?yún)?shù);
  • 執(zhí)行有副作用的操作,如 API 請(qǐng)求和路由跳轉(zhuǎn);
  • 調(diào)用非純函數(shù),如 Date.now()Math.random()

reducer 一定要保持純凈。只要傳入?yún)?shù)相同,返回計(jì)算得到的下一個(gè) state 就一定相同。沒(méi)有特殊情況、沒(méi)有副作用,沒(méi)有 API 請(qǐng)求、沒(méi)有變量修改,單純執(zhí)行計(jì)算。

4.2 創(chuàng)建一個(gè) store

安裝 redux :npm install reduxyarn add redux

src/store/index.js

import { createStore } from 'redux';
// 定義 reducer 函數(shù)
const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
};
// 使用 createStore ,傳入 reducer 函數(shù),生成一個(gè) store
const store = createStore(reducer);

export default store;

4.3 使用 store

src/pages/ReduxPage.js

import React, { Component } from 'react';
import store from '../store';

export default class ReduxPage extends Component {
  componentDidMount() {
    // 到目前為止,通過(guò) dispath 一個(gè) action 修改 state 后,頁(yè)面并不會(huì)自動(dòng)更新
    // 在沒(méi)有使用其他庫(kù)配合前,暫時(shí)使用這種方式更新界面,訂閱 store 變化,然后強(qiáng)制更新頁(yè)面
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }

  add = () => {
    store.dispatch({ type: 'ADD' });
  };

  minus = () => {
    store.dispatch({ type: 'MINUS' });
  };

  render() {
    return (
      <div>
        ReduxPage
        <div>count: {store.getState()}</div>
        <button onClick={this.add}>add</button>
        <button onClick={this.minus}>minus</button>
        <button onClick={() => this.unsubscribe()}>unsubscribe</button>
      </div>
    );
  }
}

src/App.js

import React from 'react';
import ReduxPage from './page/ReduxPage';

function App() {
  return (
    <div className="App">
      <ReduxPage />
    </div>
  );
}

export default App;

到此就已經(jīng)可以簡(jiǎn)單的使用 redux 進(jìn)行狀態(tài)管理

5. redux 實(shí)現(xiàn)

5.1 createStore 實(shí)現(xiàn)

創(chuàng)建 redux 并暴露 createStore 方法

src/redux/index.js

import createStore from './createStore'

export {
  createStore
}

5.1.1 實(shí)現(xiàn) createStore

createStore 創(chuàng)建了一個(gè) store 實(shí)例,這個(gè)實(shí)例包含了 getState、subscribe、dispatch 等方法

export default function createStore(reducer) {
  function getSate() {}
  function subscribe() {}
  function dispatch() {}

  return {
    getSate,
    subscribe,
    dispatch,
  }
}

5.1.2 實(shí)現(xiàn) getState 方法

getState 只需要返回當(dāng)前等 state

export default function createStore(reducer) {
  // 定義 currentState,保存當(dāng)前的 state
  let currentState;

  function getSate() {
    // 調(diào)用 getState 時(shí),返回當(dāng)前的 state
    return currentState;
  }
  function subscribe() {}
  function dispatch() {}

  return {
    getSate,
    subscribe,
    dispatch,
  }
}

5.1.3 實(shí)現(xiàn) subscribe 方法

subscribe,將訂閱的回調(diào)函數(shù)保存到回調(diào)函數(shù)數(shù)組中

export default function createStore(reducer) {
  let currentState;
  // 定義 currentListeners,保存訂閱的回調(diào)函數(shù)
  let lisenters = [];

  function getSate() {
    return currentState;
  }
  function subscribe(listener) {
    // 將回調(diào)函數(shù)保存到回調(diào)函數(shù)數(shù)組中
    lisenters.push(listener)
    // 返回一個(gè)取消訂閱的函數(shù)
    return function unsubscribe() {
      const index = lisenters.indexOf(lisenter);
      lisenters.splice(index, 1);
    }
  }
  function dispatch() {}

  return {
    getSate,
    subscribe,
    dispatch,
  }
}

5.1.4 實(shí)現(xiàn) dispatch 方法

dispatch 一個(gè) action (dispatch(action))是修改 state 的唯一方法。

dispatch 方法會(huì)調(diào)用創(chuàng)建 store 時(shí)傳入的 reducer 函數(shù),并把當(dāng)前的 state 和 action 傳入。

最后會(huì)循環(huán)調(diào)用 subscribe 方法收集的回調(diào)函數(shù)。

export default function createStore(reducer) {
  let currentState;
  let lisenters = [];

  function getSate() {
    return currentState;
  }

  function subscribe(listener) {
    lisenters.push(listener);

    return function unsubscribe() {
      const index = lisenters.indexOf(lisenter);
      lisenters.splice(index, 1);
    };
  }

  function dispatch(action) {
    // 調(diào)用 reducer 函數(shù),修改當(dāng)前的 state
    currentState = reducer(currentState, action)
    // 循環(huán)調(diào)用回調(diào)函數(shù)
    for (let i = 0; i < lisenters.length; i++) {
      const lisenter = lisenters[i];
      lisenter()
    }
  }

  return {
    getSate,
    subscribe,
    dispatch,
  }
}

5.1.5 初始化 state 操作

export default function createStore(reducer) {
  let currentState;
  let lisenters = [];

  function getState() {/*略*/}

  function subscribe(listener) {/*略*/}

  function dispatch(action) {/*略*/}

  // 執(zhí)行一次初始化的 dispatch,這樣可以保證整個(gè) state 樹(shù)都擁有初始狀態(tài)值,這樣在定義 reducer 函數(shù)時(shí)定義都初始 state 才會(huì)生效。
  dispatch({ type: '@@redux/INIT' });

  return {
    getState,
    subscribe,
    dispatch,
  };
}

到這里,已經(jīng)實(shí)現(xiàn)了 redux 的基礎(chǔ)功能,除此之外,上述的幾個(gè)方法中,還需要做一些參數(shù)兼容性處理,邊緣情況的處理等。

此時(shí)可以修改上面的例子中的代碼,把 redux 改為自己實(shí)現(xiàn)的 redux(src/redux/index.js),修改后功能正常使用。

目前這里 action 還只能支持 js 的普通對(duì)象(plain object),action 不支持傳入一個(gè)函數(shù)(異步)。如果要實(shí)現(xiàn)異步操作,要傳入一個(gè)值為函數(shù)的 action 還需要借助中間件。

如何實(shí)現(xiàn)異步操作?

到目前為止,使用 redux 還只能使用普通對(duì)象作為 action,如果需要異步的更新 state,例如實(shí)現(xiàn)動(dòng)態(tài)從后端獲取數(shù)據(jù),然后修改 state,還無(wú)法實(shí)現(xiàn)。

import React, { Component } from 'react';
import store from '../store';

export default class ReduxPage extends Component {
  componentDidMount() {
    store.subscribe(() => {
      this.forceUpdate();
    });
  }

  add = () => {
    store.dispatch({ type: 'ADD' });
  };

  minus = () => {
    store.dispatch({ type: 'MINUS' });
  };
    // 傳入 action 為函數(shù),進(jìn)行異步操作
  asyncAdd = () => {
    store.dispatch(() => {
      setTimeout(() => {
        store.dispatch({ type: 'ADD' });
      });
    });
  };

  render() {
    return (
      <div>
        <div>count: {store.getState()}</div>
        <button onClick={this.add}>add</button>
        <button onClick={this.minus}>minus</button>
        <button onClick={this.asyncAdd}>asyncAdd</button>
      </div>
    );
  }
}

要實(shí)現(xiàn)異步操作,需要接入 redux 中間件

6. redux 中間件的使用

Redux 只是純粹的狀態(tài)管理器,默認(rèn)只支持同步,實(shí)現(xiàn)異步任務(wù)比如延遲、網(wǎng)絡(luò)請(qǐng)求等,需要中間件的支持

store 是 redux 的核心內(nèi)容,除了 store 相關(guān)都內(nèi)容,redux 還提供了其他一些 api,用于擴(kuò)展、接入其他庫(kù),實(shí)現(xiàn)更強(qiáng)大的功能。其中 applyMiddlewares 用于中間件的接入應(yīng)用。

中間件的執(zhí)行在調(diào)用 dispatch 到最終 reducer 函數(shù)修改 state 的這個(gè)過(guò)程之間。

redux-with-middlewares.png

接下來(lái)試用一下最簡(jiǎn)單的 thunk 和 logger 中間件

src/store/index.js

import { createStore, applyMiddleware } from 'redux';
// import { createStore } from '../redux';

import thunk from 'redux-thunk';
import logger from 'redux-logger';

const reducer = function (state = 0, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    case 'MINUS':
      return state - 1;
    default:
      return state;
  }
};

// 使用 applyMiddleware 接入 logger,thunk 中間件
const store = createStore(reducer, applyMiddleware(logger, thunk));

export default store;

中間件是按照一定的順序執(zhí)行的,調(diào)整中間件的位置,可能會(huì)得到不同的執(zhí)行結(jié)果。

例如上面例子,logger 放在 thunk 前面,可以打印到異步的action 的日子,如果 logger 放到 thunk 后面,就無(wú)法打印出異步的action 的信息,因?yàn)?thunk 的邏輯代碼中,如果 action 是函數(shù),就會(huì)執(zhí)行該函數(shù),直接跳過(guò)后續(xù)中間件。這個(gè)在實(shí)際開(kāi)發(fā)中可能要了解并注意一下。

7. redux 中間件實(shí)現(xiàn)

實(shí)際上中間件是對(duì)原始的 dispatch 做了增強(qiáng)、擴(kuò)展,在 dispatch 外面又包了一層函數(shù),執(zhí)行中間件本身的功能,執(zhí)行完中間件本身的功能后,再去調(diào)用原始的 dispatch。

中間件可能是一個(gè),也可能是無(wú)數(shù)個(gè),要保證這些中間件都能有效并且有序的執(zhí)行,就需要一個(gè)好的方法將中間件串聯(lián)起來(lái)。

7.1 實(shí)現(xiàn) compose 函數(shù)

compose 函數(shù)實(shí)現(xiàn)將中間件串起來(lái),一個(gè)接一個(gè)執(zhí)行,并將上一個(gè)中間件的執(zhí)行結(jié)果,傳過(guò)下個(gè)中間件。

實(shí)現(xiàn)這樣的效果 compose(f1, f2, f3)(...args) => f1(f2(f3(...args)))

compose 的實(shí)現(xiàn)使用到 Array.prototype.reduce() 方法。

src/redux/compose.js

export default function compose(...funcs) {
  // funcs 長(zhǎng)度為 0 的話(huà),返回一個(gè)默認(rèn)的函數(shù)
  if(funcs.length === 0) {
    return arg => arg
  }
  // funcs 長(zhǎng)度為 1 的話(huà),直接返回該函數(shù)
  if(funcs.length === 1) {
    return funcs[0]
  }
  // 使用 reduce 將函數(shù)串聯(lián)起來(lái)
  return funcs.reduce((a,b) => (...args) => a(b(...args)))
  // 下面是將上面這行代碼的箭頭函數(shù)拆開(kāi),方便理解,要想理解這段代碼,必須先理解 reduce 的使用。
  // 例如:funcs 是 [logger,thunk],則最終會(huì)得到的是 logger(thunk(..args))
  // return funcs.reduce(function(a, b) {
  //     return function(...args) {
  //     return a(b(...args))
  //   }
  // })
}

7.2 實(shí)現(xiàn) applyMiddleware 函數(shù)

applyMiddleware 函數(shù)傳入中間件,并返回一個(gè)增強(qiáng)函數(shù),該函數(shù)會(huì)對(duì) createStore 函數(shù)進(jìn)行增強(qiáng)。

src/redux/applyMiddleware.js

import compose from './compose';

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer) => {
    let store = createStore(reducer);
    let dispatch = () => {
      throw new Error(
        '不允許在構(gòu)建中間件時(shí)調(diào)用 dispatch,其他中間件不會(huì)應(yīng)用此 dispatch。'
      );
    };
    // 提供給中間件到 API
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args),
    };
    // 調(diào)用中間件函數(shù)(構(gòu)建),并返回一個(gè)調(diào)用后到數(shù)據(jù)
    const chain = middlewares.map((middleware) => middleware(middlewareAPI));
    // 使用 compose 將中間件數(shù)組串起來(lái)
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch,
    };
  };
}

這里 let dispatch 定義了一個(gè)函數(shù),最后才給 dispatch 賦值為“增強(qiáng)” 后的 dispatch, 是為了防止在構(gòu)建中間件的時(shí)候就調(diào)用 dispatch。

src/redux/createStore.js

修改 createStore 函數(shù)增加一個(gè) enhancer 參數(shù),用于增強(qiáng) createStore

export default function createStore(reducer, enhancer) {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('enhancer 必須是一個(gè)函數(shù)');
    }
    return enhancer(createStore)(reducer);
  }
  /*略*/
}

當(dāng)使用 applyMiddleware 函數(shù)接入中間件的時(shí)候增加 store 功能的時(shí)候,enhancer 就是 applyMiddleware

7.3 實(shí)現(xiàn)中間件

中間件一共3層函數(shù),

第一層函數(shù),是中間件的構(gòu)建函數(shù),構(gòu)建時(shí)會(huì)傳入 getState 和 dispatch 方法,中間件的初始化也會(huì)在這里執(zhí)行,使中間件在執(zhí)行時(shí)可以用到這兩個(gè)方法。

第二層函數(shù),主要用于將中間件串連起來(lái)

第三層函數(shù),是該中間件要擴(kuò)展的功能的主要邏輯代碼實(shí)現(xiàn)

7.3.1 實(shí)現(xiàn) redux-thunk

// 構(gòu)建中間件的時(shí)候,傳入 getState, dispatch,使中間件可以用這兩個(gè)方法
const thunk = ({ getState, dispatch }) => {
  // 第二層是 compose 的時(shí)候生成的一層包一層的函數(shù),其中 next 就是下一層中間件,最后一個(gè) next 就是原始的 dispatch
  return (next) => {
    // 中間件主要邏輯代碼
    return (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }
      return next(action);
    };
  };
};

8. react-redux

單純依靠 redux 的話(huà),只能通過(guò) subscribe 訂閱數(shù)據(jù)的變化,手動(dòng)更新界面,并且每次都需要通過(guò) getState 獲取最新的數(shù)據(jù)。

這樣操作起來(lái)比較麻煩。

要更好的將 react 跟 redux 結(jié)合起來(lái),就需要借助像 react-redux 這樣的庫(kù)。

8.1 react-redux api

react-redux 提供了兩個(gè) api:Provider、connect

API

  • <Provider store>
  • connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
  1. Provider 使組件層級(jí)中的 connect() 方法都能夠獲得 Redux store,正常情況下,你的根組件應(yīng)該嵌套在 <Provider> 中才能使用 connect() 方法。
  2. connect 顧名思義,就是連接 Redux store 與組件。它是一個(gè)高階組件(HOC),傳入一個(gè)組件,并且返回一個(gè)新的組件,擴(kuò)展原來(lái)的組件使原來(lái)組件可以獲取到 sotre 中的數(shù)據(jù)與變更數(shù)據(jù)的方法。連接操作不會(huì)改變?cè)瓉?lái)的組件類(lèi)。而是返回一個(gè)新的已與 Redux store 連接的組件類(lèi)。

8.2 react-redux 使用

將上面例子改成使用 react-redux

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { Provider } from './react-redux';
import store from './store'

// 使用 Provider 為后代組件提供 store,使組件層級(jí)中的 connect() 方法都能夠獲得 Redux store
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

src/page/ReactReduxPage.js

import React, { Component } from 'react';
import { connect } from '../react-redux';

class ReactReduxPage extends Component {
  add = () => {
    this.props.dispatch({ type: 'ADD' });
  };

  minus = () => {
    this.props.dispatch({ type: 'MINUS' });
  };

  asyncAdd = () => {
    this.props.dispatch((dispatch) => {
      setTimeout(() => {
        dispatch({ type: 'ADD' });
      }, 1000);
    });
  };

  render() {
    return (
      <div>
        <div>count: {this.props.count}</div>
        {/* <button onClick={this.add}>add</button> */}
        <button onClick={this.props.add}>add</button>
        <button onClick={this.minus}>minus</button>
        <button onClick={this.asyncAdd}>asyncAdd</button>
      </div>
    );
  }
}

// mapStateToProps 用于將 redux 的 state 傳給 組件
// mapDispatchToProps 用于將 dispatch 與 action 封裝成方法,再傳給組件,方便組件里修改 state,而不用寫(xiě)大量 dispatch(xxxx)
const mapStateToProps = (state) => ({ count: state });
// mapDispatchToProps 傳一個(gè)對(duì)象的使用方式
const mapDispatchToProps = {
  add: () => ({ type: 'ADD' }),
};

// mapDispatchToProps 傳一個(gè)函數(shù)的使用方式,下面代碼與上面?zhèn)鲗?duì)象的方式實(shí)現(xiàn)同樣的功能
// const mapDispatchToProps = (dispatch) => {
//   return {
//     dispatch,
//     add: () => dispatch({ type: 'ADD' }),
//   };
// };

// 使用 connect 方法連接組件與 store
// 傳入的 mapStateToProps 和 mapDispatchToProps 定義了需要傳遞給組件的 state 與 dispatch
export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxPage);

8.3 react-redux 中用到的 react 的api:Context

這里 Context 用于將 redux 中的 state 傳遞到各個(gè)組件,使組件可以方便的使用 redux 的 state 中的數(shù)據(jù)。

在一個(gè)典型的 React 應(yīng)用中,數(shù)據(jù)是通過(guò) props 屬性自上而下(由父及子)進(jìn)行傳遞的,但這種做法對(duì)于某些類(lèi)型的屬性而言是極其繁瑣的(例如:地區(qū)偏好,UI 主題),這些屬性是應(yīng)用程序中許多組件都需要的。Context 提供了一種在組件之間共享此類(lèi)值的方式,而不必顯式地通過(guò)組件樹(shù)的逐層傳遞 props。

Context 在日常開(kāi)發(fā)中用的比較少,在第三方庫(kù)中用到比較多。react-redux 中就用到這個(gè)功能。

Context 的使用

// Context 可以讓我們無(wú)須明確地傳遍每一個(gè)組件,就能將值深入傳遞進(jìn)組件樹(shù)。
// 為當(dāng)前的 theme 創(chuàng)建一個(gè) context(“l(fā)ight”為默認(rèn)值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一個(gè) Provider 來(lái)將當(dāng)前的 theme 傳遞給以下的組件樹(shù)。
    // 無(wú)論多深,任何組件都能讀取這個(gè)值。
    // 在這個(gè)例子中,我們將 “dark” 作為當(dāng)前的值傳遞下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中間的組件再也不必指明往下傳遞 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 讀取當(dāng)前的 theme context。
  // React 會(huì)往上找到最近的 theme Provider,然后使用它的值。
  // 在這個(gè)例子中,當(dāng)前的 theme 值為 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

function Button({ theme }) {
  return <button style={{ backgroundColor: theme }}>Test Button</button>;
}

// 也可以使用 Consumer 獲取 context
// function ThemedButton () {
//   return (
//     <ThemeContext.Consumer>
//       {value => <Button theme={value} />}
//     </ThemeContext.Consumer>
//   )
// }

// 或者使用 hooks,useContext()

8.4 react-redux 中用到的 redux 的 api:bindActionCreators

bindActionCreators 在 connect 時(shí)傳 mapDispatchToProps 為對(duì)象時(shí)會(huì)用到,用于將對(duì)象中每個(gè) key 對(duì)應(yīng)的 value(action creator) 綁定一層 dispatch 調(diào)用。然后通過(guò) props 傳給組件,使組件中可以更方便的修改 redux 的 state,減少組件中寫(xiě)很多 dispatch(xxx)。
action creator 后面會(huì)講。
用法:bindActionCreators(actionCreators, dispatch)

bindActionCreators 把一個(gè) value 為不同 action creator 的對(duì)象,轉(zhuǎn)成擁有同名 key 的對(duì)象。同時(shí)使用 dispatch對(duì)每個(gè) action creator 進(jìn)行包裝,以便可以直接調(diào)用它們。

作用大概是下面這樣的效果:

{
  add: () => ({ type: 'ADD' }),
};
// 變成
{
    add: () => dispatch({ type: 'ADD' })
}

參數(shù):

  1. actionCreators (Function or Object): 一個(gè) action creator,或者一個(gè) value 是 action creator 的對(duì)象。
  2. dispatch (Function): 一個(gè)由 Store 實(shí)例提供的 dispatch 函數(shù)。

8.4.1 Action Creator

Action Creator 是指用來(lái)生成 action 的函數(shù)

例如:

function ADD_TODO(text) {
    return { type: 'ADD', text}
}
dispatch(ADD_TODO('待辦事項(xiàng)1')) // dispatch({ type: 'ADD', text: '待辦事項(xiàng)1'})

上例中 ADD_TODO 用來(lái)生成 Action { type: 'ADD', text}, ADD_TODO 就是一個(gè) action creator 函數(shù)

Action 是一個(gè)信息的負(fù)載,而 action creator 是一個(gè)創(chuàng)建 action 的工廠(chǎng)。

8.4.1 bindActionCreators 實(shí)現(xiàn)

作用:在 mapDispatchToProps 為對(duì)象時(shí),使對(duì)象 { add: () => ({type: 'ADD'})} 相當(dāng)于 add = () => dispatch({type: 'ADD'})

// bindActionCreator 會(huì)對(duì) action creator 進(jìn)行包裝,加上 dispatch 調(diào)用
function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args));
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  const boundActionCreators = {};
  for (let key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators;
}

8.4 react-redux 實(shí)現(xiàn)

src/react-redux/index.js

import Provider from './Provider';
import connect from './connect';
// 暴露 Provider,connect API
export { Provider, connect };

創(chuàng)建一個(gè) Context,用于數(shù)據(jù)通信

src/react-redux/Context.js

import React from 'react';

export const ReactReduxContext = React.createContext(null);

export default ReactReduxContext;

8.5 Provider 的實(shí)現(xiàn)

import React, { Component } from 'react';
import { ReactReduxContext } from './Context';

export default class Provider extends Component {
  render() {
    // 使用 Context.Provider 為組件提供 store
    return (
      <ReactReduxContext.Provider value={this.props.store}>
        {this.props.children}
      </ReactReduxContext.Provider>
    );
  }
}

8.6 connect 的實(shí)現(xiàn)

connect 接受 mapStateToProps 和 mapDispatchToProps 參數(shù),返回一個(gè)高階組件。

“連接” 需要使用 store 的組件,把 store 中的值傳遞給組件

import React, { useLayoutEffect, useReducer, useContext } from 'react';
// import { bindActionCreators } from 'redux';
import { bindActionCreators } from '../redux';
import { ReactReduxContext } from './Context';

const connect = (mapStateToProps = (state) => state, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {
  const store = useContext(ReactReduxContext);
  const { getState, dispatch, subscribe } = store;

  // mapStateToProps 是在使用 connect 時(shí)傳入的第一個(gè)參數(shù),
  // getState 獲取到 state 到值,然后傳遞給 mapStateToProps 使用
  // mapStateToProps 執(zhí)行完成后返回需要傳遞給組件的 stateProps
  const stateProps = mapStateToProps(getState());

  // connect 的第二個(gè)參數(shù) mapDispatchToProps 可以是對(duì)象或者函數(shù)
  let dispatchProps;
  if (typeof mapDispatchToProps === 'object') {
    // 如果 mapDispatchToProps 是一個(gè)對(duì)象,則使用 bindActionCreators 將該對(duì)象包裝成可以直接調(diào)用的函數(shù)對(duì)象
    dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
  } else if (typeof mapDispatchToProps === 'function') {
    // 如果 mapDispatchToProps 是一個(gè)函數(shù),則調(diào)用函數(shù)并傳入 dispatch
    dispatchProps = mapDispatchToProps(dispatch);
  } else {
    // 默認(rèn)傳遞 dispatch
    dispatchProps = { dispatch };
  }
    
  function storeStateUpdatesReducer(state, action) {
    return state + 1;
  }
  // 使用 useReducer 實(shí)現(xiàn)強(qiáng)制更新頁(yè)面
  // useReducer 返回的數(shù)組包含兩個(gè)項(xiàng) [state, dispatch],調(diào)用 dispatch 返回新的 state 時(shí),組件會(huì)重新渲染
  const [, forceComponentUpdateDispatch] = useReducer(
    storeStateUpdatesReducer,
    0
  );

  useLayoutEffect(() => {
    // 訂閱 store 中數(shù)據(jù)更新,強(qiáng)制刷新頁(yè)面
    const ubsubscribe = subscribe(() => {
      forceComponentUpdateDispatch({ type: 'STORE_UPDATED' });
    });
    return () => {
      // 卸載組件取消訂閱
      ubsubscribe && ubsubscribe();
    };
  }, [store]);

  // 將需要“連接”的組件返回,并傳遞給組件需要的數(shù)據(jù)
  return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
};

export default connect;

9. redux devtools

9.1 接入 redux

import { createStore, applyMiddleware } from 'redux';
// 引入 composeWithDevTools
import { composeWithDevTools } from 'redux-devtools-extension';

import thunk from 'redux-thunk';
import logger from 'redux-logger';

const reducer = function (state = 0, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    case 'MINUS':
      return state - 1;
    default:
      return state;
  }
};

const store = createStore(
  reducer,
  composeWithDevTools(applyMiddleware(logger, thunk))
);
export default store;

9.2 chrome 插件:redux devtools

redux-devtools-panel.png
  1. 打開(kāi) Chrome 調(diào)試工具,選擇 redux 進(jìn)入 redux devtools 調(diào)試界面。

  2. 選擇使用的功能:Log monitor、Inspector、Chart,默認(rèn)是 Inspector。

  3. 錄制的redux 執(zhí)行過(guò)程中執(zhí)行的 action 的列表,點(diǎn)擊每個(gè) action 可以進(jìn)入到該 action 下的詳細(xì)狀態(tài)

  4. Action 可以查看當(dāng)前 action 的詳細(xì)內(nèi)容

    redux-devtools-action.png
  1. State 可以查看當(dāng)前 state 數(shù)據(jù)的詳細(xì)內(nèi)容

    redux-devtools-state.png
  1. Diff 可以查看當(dāng)前 action 執(zhí)行后,state 發(fā)生了哪些變化

    redux-devtools-diff.png
  1. Trace 跟蹤當(dāng)前 action 執(zhí)行的代碼位置

    redux-devtools-trace.png
  1. Test 測(cè)試模版

    redux-devtools-test.png

    redux-devtools-toolbar.png
  1. Start recording/Pause recording 開(kāi)始/停止錄制,可以指定從什么時(shí)候開(kāi)始錄制到什么停止。

  2. Lock changes 鎖定當(dāng)前的錄制狀態(tài),再有 action 執(zhí)行也不會(huì)改動(dòng)當(dāng)前錄制的狀態(tài)。

  3. Dispatcher 用于派發(fā) action,Dispatcher 框內(nèi)可以編輯 action 的內(nèi)容。

  4. Slider 用于自動(dòng)播放整個(gè)錄制的過(guò)程。

  5. Import/ Export 導(dǎo)入/導(dǎo)出,導(dǎo)出當(dāng)前錄制的狀態(tài)的 JSON 文件,之后用于導(dǎo)入,導(dǎo)入后會(huì)還原到該文件保存到狀態(tài)。

  6. Settings 是 redux devtools 的配置菜單。

  7. 切換功能面板到 Log monitor 可以查看整個(gè)過(guò)程的日志

    redux-devtools-log-monitor.png
  8. 切換功能面板到 Chart 可以查看整個(gè) store 的狀態(tài)

    redux-devtools-chart.png

擴(kuò)展閱讀:

官網(wǎng):https://redux.js.org/

github: https://github.com/reduxjs/redux

中文文檔:https://www.redux.org.cn/

React Context:https://zh-hans.reactjs.org/docs/context.html

你可能不需要 redux:You Might Not Need Redux

?著作權(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)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • export const ActionTypes = {INIT:'@@redux/INIT'} // 生成一個(gè)s...
    jiandan5850閱讀 553評(píng)論 0 0
  • 概述 Redux 是 JavaScript 狀態(tài)容器,提供可預(yù)測(cè)化的狀態(tài)管理方案。其三大原則為: 單一數(shù)據(jù)源 =>...
    bowen_wu閱讀 613評(píng)論 0 0
  • Redux從入門(mén)到放棄 基本概念 前端應(yīng)用日漸復(fù)雜,傳統(tǒng)的JavaScipt+HTML+CSS三大件變得難以應(yīng)對(duì)近...
    明月本無(wú)心閱讀 697評(píng)論 0 0
  • 上周六參加了一個(gè)公司的面試,因?yàn)槭呛荛L(zhǎng)時(shí)間以來(lái)的第一次面試,發(fā)揮的并不是很好,有兩個(gè)下來(lái)一想就明白的問(wèn)題在當(dāng)時(shí)卻卡...
    夏爾先生閱讀 6,487評(píng)論 0 15
  • 寫(xiě)在開(kāi)頭 前置知識(shí)內(nèi)容,閉包,高階函數(shù),函數(shù)式編程思想,redux核心概念。 redux文檔:https://ww...
    前端開(kāi)發(fā)愛(ài)好者閱讀 802評(píng)論 0 1

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