redux和react-redux從實現到理解

前言

咱們在使用react進行開發時,一般會搭配react-redux進行狀態管理,react-redux實際上是基於redux封裝的,使開發者更方便的使用redux管理數據,因此要明確redux徹底可使用。咱們要學習react-redux首先要先學習reduxjavascript

redux簡單實現demohtml

react-redux簡單實現demojava

Redux基本使用

咱們先來看一下redux的基本使用,下面的代碼經過createStore來建立一個store,建立成功後會返回三個API(subscribedispatchgetState)。咱們經過subscribe來訂閱store中數據的變化,當有變化時會執行回調函數,經過getState獲取最新數據輸出,最後咱們經過dispatch傳入action來觸發數據改變。react

// src/store/index.js
import { createStore } from 'redux'

// 定義reducer
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 建立store,返回API { subscribe, dispatch, getState }
let store = createStore(counter)

// 訂閱store變化試,派發通知
store.subscribe(() => console.log(store.getState()))

// 經過dispatch觸發action,作到store中數據變化
store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1
複製代碼

咱們引入這個文件,在控制檯中能夠看到依次輸出一、二、1。能夠看出來redux用法很簡單,其實它只是規定了改變數據的方法,當咱們遵循這個規則時,咱們的數據源就是惟一的,數據也變得可控起來。接下來咱們本身來實現一個簡易版的redux來知足基本使用。git

實現簡易版Redux

經過上面的例子,咱們首先要實現createStore,該函數會返回三個經常使用的API,而且能夠操做state。下面是函數的骨架。github

// src/mock/redux.js
function createStore(reducer) {

  let currentState; // 始終保持最新的state
  const listeners = []; // 用於存儲訂閱者

  // 訂閱store
  function subscribe(fn) {}

  // 獲取最新state
  function getState() {}

  // 改變數據的惟一方法(約定)
  function dispatch() {}

  return { subscribe, getState, dispatch };
}

export default createStore;
複製代碼

下面咱們逐一實現這三個API。redux

getState

getState實現就超簡單了,由於內部變量currentState始終保持最新,咱們只要將這個變量返回就行了,一行代碼搞定數組

// 獲取最新state
 function getState() {
   return currentState;
 }
複製代碼

subscribe

咱們定義了內部變量listeners,因此只要將傳入的訂閱者存儲到listeners中就能夠。注意:訂閱者必定是函數,這樣state變化時,去執行listeners中的函數就能夠了。咱們還要返回一個函數用於取消訂閱。閉包

// 訂閱store
function subscribe(fn) {
  if (typeof fn !== "function") {
  	throw new Error("期待訂閱者是個函數類型");
  }
  listeners.push(fn);
  // 用於取消訂閱
  return function describe() {
  	const idx = listeners.indexOf(fn);
    listeners.splice(idx, 1);
  };
}
複製代碼

dispatch

dispatch接受一個action對象,該action對象會傳入到reducer中,reducer是咱們在建立store傳入的。reducer約定會經過action的type來返回新的state,那其實dispatch的原理也就很簡單了。咱們只要把傳入的action傳入到reducer函數中,返回新的state賦值給currentState就能夠了。看代碼:app

// 改變數據的惟一方法(約定)
function dispatch(action) {
  currentState = reducer(currentState, action);
  // 別忘了,數據改變後,要通知全部的訂閱者。
  listeners.forEach(fn => fn());
}
複製代碼

是否是超Easy?拋去redux的概念,其實咱們就是經過閉包的概念,來操做內部的數據,從而實現狀態管理。

- import { createStore } from 'redux'
+ import createStore from "../mock/redux";
複製代碼

咱們將src/store/index.js文件中createStore替換成咱們的,再次執行看下,效果是一致的。demo源碼

React-Redux的基本使用

咱們定義好store,而後經過react-redux提供的Provider向下注入依賴store

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

// 忽略無關代碼

ReactDOM.render(
  <Provider store={store}> <APP /> </Provider>,
  rootElment
);
複製代碼

咱們在須要依賴state的組件文件中使用react-redux提供的connect對組件進行高階包裹。其中咱們向傳connect函數傳入倆個參數,分別是mapStateToPropsmapDispatchToProps,做用跟名字相同,react-redux會把倆個函數執行,將返回值都以props的形式傳入到組件中。

import { connect } from "react-redux";

// 忽略無關代碼

function mapStateToProps(state) {
  return {
    count: state
  };
}

function mapDispatchToProps(dispatch) {
  return {
    increment() {
      dispatch({
        type: "INCREMENT"
      });
    },
    decrement() {
      dispatch({
        type: "DECREMENT"
      });
    }
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App); // App組件接受到的props中 包括 count、increment、decrement
複製代碼

咱們只要在App組件從props中解構出值`進行使用。

function App(props) {
  const { count, increment, decrement } = props;
  return (
    <div className="App"> <p>當前count: {count}</p> <button onClick={increment}>增長1</button> <button onClick={decrement}>減小1</button> </div>
  );
}
複製代碼

乍一看代碼量不少,但解決了組件嵌套的問題,當嵌套組件須要依賴state時候,咱們只須要用connect進行包裹,傳入mapStateToProps就能夠。並且不須要咱們手動訂閱store的變化,從而觸發組件的渲染。那它是如何工做的呢?咱們接下來分析一波,並動手實現一個簡易的react-redux

實現簡易版React-Redux

首先咱們忘記react-redux的存在,嘗試直接在react組件中使用redux,咱們須要在組件渲染前獲取到所需的state。而且訂閱store,當其state變化後,咱們要從新渲染該組件從而獲取到最新的state。代碼以下:

class App extends React.Component {
  componentDidMount() {
    // 訂閱
    this.describe = store.subscribe(() => {
      // 強制渲染
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    // 取消訂閱
    this.describe();
  }

  increment = () => {
    store.dispatch({
      type: "INCREMENT"
    });
  };

  decrement = () => {
    store.dispatch({
      type: "DECREMENT"
    });
  };

  render() {
    // 獲取當前狀態並賦值
    const count = store.getState();
    return (
      <div className="App"> <p>當前count: {count}</p> <button onClick={this.increment}>增長1</button> <button onClick={this.decrement}>減小1</button> </div>
    );
  }
}
複製代碼

咱們能夠發現,獲取所需state訂閱store從新渲染組件是每個須要依賴redux組件都須要的,因此咱們應該抽離出公共部分。

connect

在類組件咱們想要複用邏輯只能經過HOC高階組件來實現,connect函數其實就是生成高階組件。下面咱們先寫個最基本的connect函數:

/** * 經過傳入mapStateToProps/mapDispatchToProps生成高階組件 * 並把所需state經過props傳入組件 * @param {function} mapStateToProps * @param {function} mapDispatchToProps */
function connect(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(WrapperComponent) {
    return function ConnectFunction(props) {
      // 獲取到所需state,觸發dispatch的函數
      const stateProps = mapStateToProps(store.getState());
      const dispatchProps = mapDispatchToProps(store.dispatch);
      // 執行強制渲染
      const [, forceRender] = useReducer(s => s + 1, 0);
      // 訂閱store變化
      useEffect(() => {
        const describe = store.subscribe(forceRender);
        return describe;
      }, []);

      return <WrapperComponent {...props} {...stateProps} {...dispatchProps} />; }; }; } 複製代碼

:由於函數組件沒有this.forceUpdate方法,因此經過useReducer自增實現一樣的效果。

上述代碼把獲取所需state訂閱store從新渲染組件倆部分都抽離了出來,使咱們能夠在須要使用store中數據時,直接經過connect(mapStateToProps)(Comp)對組件進行包裹便可。

但如今還有倆個問題須要優化。1.是咱們如今的store是直接引入的,沒法支持動態的store ,2.是目前爲止,咱們store變化就會從新渲染,當咱們所依賴的值沒有改變時,咱們無需從新渲染。

Provider

咱們先解決上面的說的第一個問題,想支持動態的store,咱們就須要實現react-redux中的Provider組件,看名字你們應該知道它是基於react context實現的,沒錯,要實現動態store,咱們須要使Provider向下注入依賴,而後在connect包裹組件的時候,經過context來獲取最新store。

import storeContext from "./storeContext";
// storeContext就是經過React.createContext()生成context

const Provider = ({ store, children }) => {
  return (
    <storeContext.Provider value={store}>{children}</storeContext.Provider> ); }; export default Provider; 複製代碼

Provider組件就這麼簡單,接下來咱們須要修改connect函數

import storeContext from "./storeContext";
// 忽略無關代碼...
const store = useContext(storeContext);
// 獲取到所需state,觸發dispatch的函數
const stateProps = mapStateToProps(store.getState());
const dispatchProps = mapDispatchToProps(store.dispatch);
// 訂閱store變化
useEffect(() => {
  const describe = store.subscribe(forceRender);
  return describe;
}, [store]);

// 忽略無關代碼...
複製代碼

經過react提供的useContext()來獲取到當前storeuseEffect第二個參數依賴store,當store自己變化時,也會從新訂閱。這樣咱們第一個問題算是解決了。用法與react-redux也大致相同。

再解決第二個問題:咱們如今訂閱store中state變化,仍是很暴力的(直接強制從新渲染)。要解決這個問題也很簡單,咱們只要訂閱的回調函數中,加入新老值的比較,當不相同時,咱們才執行forceRender

// src/react-redux/connect.js
import shallowEqual from "shallowequal";

function connect(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(WrapperComponent) {
    return function ConnectFunction(props) {
      const store = useContext(storeContext);
      const lastStateProps = useRef({}); // 保存最新的state
      const lastDispatchProps = useRef({});

      // 執行強制渲染
      const [, forceRender] = useReducer(s => s + 1, 0);

      // 訂閱store變化
      useEffect(() => {
        lastStateProps.current = mapStateToProps(store.getState());
        lastDispatchProps.current = mapDispatchToProps(store.dispatch);
      }, [store]);

      // 訂閱store變化
      useEffect(() => {
        forceRender();
        function checkForUpdates() {
          const newStateProps = mapStateToProps(store.getState());
          // 執行淺比較
          if (!shallowEqual(lastStateProps.current, newStateProps)) {
            console.log('render')
            // 賦值最新的state
            lastStateProps.current = newStateProps;
            forceRender();
          }
        }
        const describe = store.subscribe(checkForUpdates);
        return describe;
      }, [store]);

      return (
        <WrapperComponent {...props} {...lastStateProps.current} {...lastDispatchProps.current} /> ); }; }; } 複製代碼

咱們引入shallowequal對新老state進行淺比較,當不相等時,才進行forceRender

- import { Provider } from "react-redux";
+ import { connect, Provider } from "./react-redux";
複製代碼

如今,咱們將App組件中的Providerconnect替換掉,代碼是能夠正常的使用。完整demo

useSelector

上面實現了connect用於共享邏輯,雖然函數組件也能夠經過它進行包裹使用,但React Hook的出現讓咱們對於邏輯複用有了更好的辦法,那就是本身寫一個HookuseSelectorreact-redux官方已經實現了的。具體的使用以下:

const count = useSelector(state => state.count)
複製代碼

經過傳入一個選取函數返回所須要的state,其實這裏的選取函數至關因而mapStateToProps。咱們來動手實現如下。

import storeContext from "./storeContext";

export default function useSelector(seletorFn) {
  const store = useContext(storeContext);
  return seletorFn(store.getState());
}
複製代碼

如今咱們能夠執行useSeletor獲取到所須要的state,接下來咱們要作的就是訂閱store從新渲染,其實就是咱們實現connect中函數組件的代碼,咱們直接copy過來改一下

import storeContext from "./storeContext";
import shallowEqual from "shallowequal";

export default function useSelector(selectorFn) {
  const store = useContext(storeContext);

  const lastStateProps = useRef();
  const lastSelectorFn = useRef();

  // 執行強制渲染
  const [, forceRender] = useReducer(s => s + 1, 0);

  // 賦值state
  useEffect(() => {
    lastSelectorFn.current = selectorFn;
    lastStateProps.current = selectorFn(store.getState());
  });
  
  // 訂閱store變化
  useEffect(() => {
    function checkForUpdates() {
      const newStateProps = lastSelectorFn.current(store.getState());
      if (!shallowEqual(lastStateProps.current, newStateProps)) {
        console.log("render");
        lastStateProps.current = newStateProps;
        forceRender();
      }
    }
    const describe = store.subscribe(checkForUpdates);
    forceRender();
    return describe;
  }, [store]);

  return lastStateProps.current;
}
複製代碼

注意:這裏須要使用lastSelectorFnRef存儲選擇器,不然useEffect依賴selectorFn會形成死循環。

useDispatch

實現useDispatch就超簡單了,就是直接返回store.dispatch就好

import { useContext } from "react";
import storeContext from "./storeContext";

export default function useDispatch(seletorFn) {
  const store = useContext(storeContext);
  return store.dispatch;
}
複製代碼

總結

本文中實現的reduxreact-redux都只是實現了一小部分API,而且沒有處理異常狀況。但與源碼的核心大致相同。但願閱讀完的小夥伴有所收穫,若是不過癮還能夠去閱讀下源碼哦。

推薦

redux好文章

完美擁抱 React Hooks 的狀態管理器hox

相關文章
相關標籤/搜索