手寫傻瓜式 React 全家桶之 React-Redux

文章系列

手寫傻瓜式 React 全家桶之 Reduxjavascript

手寫傻瓜式 React 全家桶之 React-Reduxhtml

本文代碼前端

上一篇手寫了 Redux 源碼,同時也說明了 Redux 裏頭是沒有 React 相關的 API,這篇我們來寫下 React-Redux,那麼 React,Redux 以及 React-Redux 關係是:java

  • Redux: Redux 是一個應用狀態管理js庫,它自己和 React 是沒有關係的,換句話說,Redux 能夠應用於其餘框架構建的前端應用。
  • React-Redux:React-Redux 是鏈接 React 應用和 Redux 狀態管理的橋樑。React-redux 主要專一兩件事,一是如何向 React 應用中注入 redux 中的 Store ,二是如何根據 Store 的改變,把消息派發給應用中須要狀態的每個組件。
  • React:用於構建用戶界面的庫

1、爲何要使用 React-Redux

上一篇使用 Redux 開發了個加減器的功能,可是暴露了幾個問題:react

  1. store 須要手動引入,而且在組件初始化以及銷燬時,手動進行 subscribe 與 unsubscribe
import store from "../store";
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }
複製代碼
  1. 狀態的更改,會致使全部的組件都從新渲染,好比 A 組件只依賴 a 狀態,而 b 狀態更改時,也會致使 A 組件的從新渲染

爲了解決這些問題,react-redux 就應運而生了git

2、什麼是 React-Redux

React-Redux 是鏈接 React 應用和 Redux 狀態管理的橋樑。其中既有 React 的 API,也會依賴 Redux 的相關 API。其實 React-Redux 主要提供了兩個 api:github

  1. Provider 爲後代組件提供store
  2. connect 爲組件提供數據和變動⽅法

Provider

將根組件嵌套在 <Provider> 中,這樣子孫組件就能經過 connect 獲取到 stateweb

例子:redux

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";

ReactDOM.render(
  <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>,
  document.getElementById("root")
);

複製代碼

其中的 store 參數就是指 Redux 的 createStore 生成的 storeapi

connect

connect 是個高階組件,通過它包裝後的組件將獲取以下功能:

  1. 默認 props 裏會帶有 dispatch 函數
  2. 若是給 connect 傳遞了第一個參數,那麼會將 store 裏的 state 數據,映射到當前組件的 props
  3. 若是給 connect 傳遞了第二個參數,那麼會將相關方法,映射到當前組件的 props
  4. 組件依賴的 state 更改時,會通知當前組件更新,從新渲染視圖

語法:

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?) 複製代碼

默認 create-react-app 腳手架是不支持 @裝飾器的,能夠經過 react-app-rewired 優雅配製

接下來分別講解下這四個參數

mapStateToProps:

const mapStateToProps = state => ({ count: state.count })
複製代碼

該函數必須返回一個純對象,這個對象會與組件的 props 合併。若是定義該參數,組件將會監聽 Redux store 的變化,不然不監聽。

mapDispatchToProps:

若是省略這個 mapDispatchToProps 參數,默認狀況下,dispatch 會注⼊到你的組件 props 中。 該參數存在兩種格式:

  • 對象格式:
const mapDispatchToProps = {
  add: () => ({ type: "ADD" }),
  minus: () => ({ type: "MINUS" }),
};
複製代碼

對象裏的方法名會被合併到組件的 props 裏,經過該方法名就能夠觸發相應的 action

對象的形式,沒辦法往 props 裏注入 dispatch,只能是具體的 action 操做

  • 函數形式:

該函數將接收 dispatch 參數,而後返回任何要注入到 props 裏的對象

const mapDispatchToProps = (dispatch) => ({
  add: () => dispatch({ type: "ADD" }),
  minus: () => dispatch({ type: "MINUS" }),
});
複製代碼

上面這種寫法有些複雜,能夠採用 redux 提供的 bindActionCreators 簡化下

const mapDispatchToProps = (dispatch) => {
  let creators = {
    add: () => ({ type: "ADD" }),
    minus: () => ({ type: "MINUS" }),
  };
  creators = bindActionCreators(creators, dispatch);
  return {
    ...creators,
    dispatch,
  };
};
複製代碼

mergeProps:

mergeProps(stateProps, dispatchProps, ownProps)
複製代碼

若是省略這個 mergeProps 參數,默認狀況下,會返回 Object.assign({}, ownProps, stateProps, dispatchProps)

若是定義了這個參數,mapStateToProps()mapDispatchToProps() 的執⾏結果和組件⾃身的 props 將傳⼊到這個回調函數中。

該回調函數返回的對象將做爲 props 傳遞到被包裹的組件中。

options:

{
  context?: Object,   // 自定義上下文
  pure?: boolean, // 默認爲 true , 當爲 true 的時候 ,除了 mapStateToProps 和 props ,其餘輸入或者state 改變,均不會更新組件。
  areStatesEqual?: Function, // 當pure true , 比較引進store 中state值 是否和以前相等。 (next: Object, prev: Object) => boolean
  areOwnPropsEqual?: Function, // 當pure true , 比較 props 值, 是否和以前相等。 (next: Object, prev: Object) => boolean
  areStatePropsEqual?: Function, // 當pure true , 比較 mapStateToProps 後的值 是否和以前相等。 (next: Object, prev: Object) => boolean
  areMergedPropsEqual?: Function, // 當 pure 爲 true 時, 比較 通過 mergeProps 合併後的值 , 是否與以前等 (next: Object, prev: Object) => boolean
  forwardRef?: boolean, //當爲true 時候,能夠經過ref 獲取被connect包裹的組件實例。
}
複製代碼

mergePropsoptions 比較少用到,重點關注前兩個參數

示例代碼:

import React, { Component } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
const mapStateToProps = (state) => ({ count: state.count });
// const mapDispatchToProps = {
// add: () => ({ type: "ADD" }),
// minus: () => ({ type: "MINUS" }),
// };
// const mapDispatchToProps = (dispatch) => ({
// add: () => dispatch({ type: "ADD" }),
// minus: () => dispatch({ type: "MINUS" }),
// });

const mapDispatchToProps = (dispatch) => {
  let creators = {
    add: () => ({ type: "ADD" }),
    minus: () => ({ type: "MINUS" }),
  };
  creators = bindActionCreators(creators, dispatch);
  return {
    ...creators,
    dispatch,
  };
};

@connect(mapStateToProps, mapDispatchToProps)
class Counter extends Component {
  render() {
    const { count, add, minus } = this.props;
    return (
      <div className="border"> <h3>加減器</h3> <button onClick={add}>add</button> <span style={{ marginLeft: "10px", marginRight: "10px" }}>{count}</span> <button onClick={minus}>minus</button> </div>
    );
  }
}
export default Counter;

複製代碼

useSelector and useDispatch

在函數組件裏,除了使用 connect 方式接收傳遞的 state 與 dispatch 信息以外,React-Redux 還提供了兩個 hook: useSelectoruseDispatch

useSelector:

const result: any = useSelector(selector: Function, equalityFn?: Function)
複製代碼

平時用的更多的是第一個參數,是個函數,參數爲 storestate

const state = useSelector(({ count }) => ({ count }));
複製代碼

返回個對象,keycount,內容就是 store state 裏的 count。 這樣經過 state.count 就能夠獲取到

useDispatch:

const dispatch = useDispatch()
複製代碼

執行下 useDispatch 就獲取到了 dispatch,經過 dispatch 就能夠更改狀態

useStore:

const store = useStore()
複製代碼

返回 store 對象的引用。儘可能不要使用該 hookuseSelector 纔是首選

示例代碼:

import { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
export default function ReactReduxHookPage() {
  const state = useSelector(({ count }) => count);
  const dispatch = useDispatch();
  const add = useCallback(() => {
    dispatch({ type: "ADD" });
  }, []);
  return (
    <div> <h3>ReactReduxHookPage</h3> <p>{state}</p> <button onClick={add}>add</button> </div>
  );
}
複製代碼

3、手寫 Provide

provide 作的事情就是爲後代組件提供 store ,這不正是 React context api 乾的事 首先建一個 context 文件,導出須要用的 context :

import React from "react";

const ReactReduxContext = React.createContext();

export default ReactReduxContext;

複製代碼

將 context 應用到 Provider 組件裏

import ReactReduxContext from "./context";
export function Provider({ children, store }) {
  return (
    <ReactReduxContext.Provider value={store}> {children} </ReactReduxContext.Provider>
  );
}

複製代碼

能夠看出 Provider 組件代碼不難,無非就是將傳進來的 store 做爲 contextvalue 值,而後直接渲染 children 便可

4、手寫 connect

基本功能

上面也講到 connect 是個函數,而且返回個高階組件,因此它的基本結構爲:

function connect() {
  return function (WrappedComponent) {
    return function (props) {
      return <WrappedComponent {...props} />;
    };
  };
}
export default connect;
複製代碼

羅列個 connect 組件要實現的功能:

  1. 接收傳遞下來的 store
  2. 若是傳遞了 mapStateToProps 參數,則傳入 state 執行
  3. 默認將 dispatch 注入組件的 props
  4. 若是傳遞了 mapDispatchToProps 參數而且參數是個函數類型,則傳入 dispatch 執行
  5. 若是傳遞了 mapDispatchToProps 參數而且參數是個對象類型,則就要將參數看成 creators action 處理
  6. 將處理好的 statePropsdispatchProps,以及組件自身的 props 一併傳入組件
import { useContext } from "react";
import ReactReduxContext from "./context";
function connect(mapStateToProps, mapDispatchToProps) {
  return function (WrappedComponent) {
    return function (props) {
      let stateProps = {};
      let dispatchProps = {};
      // 1. 接收傳遞下來的 store
      const store = useContext(ReactReduxContext);
      const { getState, dispatch } = store;
      // 2. 若是傳遞了 mapStateToProps 參數,則傳入 state 執行
      if (
        mapStateToProps !== "undefined" &&
        typeof mapStateToProps === "function"
      ) {
        stateProps = mapStateToProps(getState());
      }
      // 3. 默認將 dispatch 注入組件的 props 裏
      dispatchProps = { dispatch };
      // 4. 若是傳遞了 mapDispatchToProps 參數而且參數是個函數類型,則傳入 dispatch 執行
      // 5. 若是傳遞了 mapDispatchToProps 參數而且參數是個對象類型,則就要將參數看成 creators action 處理
      if (mapDispatchToProps !== "undefined") {
        if (typeof mapDispatchToProps === "function") {
          dispatchProps = mapDispatchToProps(dispatch);
        } else if (typeof mapDispatchToProps === "object") {
          dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
        }
      }
      return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
    };
  };
}

// 手寫 redux 裏的 bindActionCreators
function bindActionCreators(creators, dispatch) {
  let obj = {};
  // 遍歷對象
  for (let key in creators) {
    obj[key] = bindActionCreator(creators[key], dispatch);
  }
  return obj;
}

// 將 () => ({ type:'ADD' }) creator 轉成成 () => dispatch({ type:'ADD' })
function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args));
}
export default connect;

複製代碼

觸發組件更新

將官方的 React-Redux 替換爲手寫的 provider 與 connect,能夠正常顯示出頁面,但會發現點擊按鈕,頁面上的值並無發生改變 頁面沒有更新 在上一篇 Redux 裏講過,能夠用 store.subscribe 來監聽 state 的變化並執行回調。

store.subscribe(() => {
	this.forceUpdate()
})
複製代碼

因爲 connect 是個函數組件,那麼在函數裏是否有相似 forceUpdate 的東西呢? 目前官方並未提供,因此只能經過模擬實現:⽤⼀個增⻓的計數器來強制從新渲染

const [, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
	forceUpdate();
}
複製代碼

connect 函數里加上以下代碼:

const [, forceUpdate] = useReducer(x => x+1, 0)
  // 之因此用 useLayoutEffect 是爲了在頁面渲染以前就執行,防止操做過快時,採用 useEffect 會有缺失的狀況
  const unsubscribe = useLayoutEffect(() => {
    subscribe(()=> {
      forceUpdate()
    })
    return () => {
      if(unsubscribe) {
        unsubscribe()
      }
    }
  }, [store])
複製代碼

再次驗證: 功能大致正常 能夠看到點擊按鈕,頁面已經能夠即時響應了,那是否已經足夠完善呢?不是的,還存在些問題,下面咱們邊分析邊改進

檢查 props 變化

再添加個 user.js 組件:

import React, { Component } from "react";
import { connect } from "../kReactRedux";

@connect(({ user }) => ({
  user,
}))
class User extends Component {
  render() {
    console.info(222); // 方便查看是否會從新渲染
    const { user } = this.props;
    return (
      <div className="border"> <h3>用戶信息</h3> {user.name} </div>
    );
  }
}
export default User;
複製代碼

該組件只依賴 store 裏的 user 信息,但訪問該頁面,會發現點擊 counter 組件裏的 add 按鈕,會致使 user 組件一併從新渲染 從新渲染 這也不難理解,由於現有的代碼是採用 subscribe ,一旦 store 狀態更改就會觸發回調,而回調裏作的事情就是強制刷新,而 user 組件又是採用 connect 包裝的,天然也就會從新渲染。因此應該要在觸發回調時,判斷下當前組件的 props 值是否更改,若是更改了才強制刷新。

要檢查先後 props 的更改,就須要將上次渲染的 props 與本次渲染的 props 進行比較。而要存儲上次渲染的 props ,就得采用 useRef 將上次渲染的 props 存儲下來

// 6.組裝最終的props
const actualProps = Object.assign({}, props, stateProps, dispatchProps);
// 7.記錄上次渲染參數
const lastProps = useRef();
useLayoutEffect(() => {
  lastProps.current = actualProps;
}, []);
複製代碼

檢測 props 是否變化是須要從新計算的,因此將獲取最終 props 的邏輯抽離出來

function getProps(store, wrapperProps) {
    const { getState, dispatch } = store;
    let stateProps = {};
    let dispatchProps = {};

    // 2. 若是傳遞了 mapStateToProps 參數,則傳入 state 執行
    if (
      mapStateToProps !== "undefined" &&
      typeof mapStateToProps === "function"
    ) {
      stateProps = mapStateToProps(getState());
    }
    console.info(stateProps, "stateProps");
    // 3. 默認將 dispatch 注入組件的 props 裏
    dispatchProps = { dispatch };
    // 4. 若是傳遞了 mapDispatchToProps 參數而且參數是個函數類型,則傳入 dispatch 執行
    // 5. 若是傳遞了 mapDispatchToProps 參數而且參數是個對象類型,則就要將參數看成 creators action 處理
    if (mapDispatchToProps !== "undefined") {
      if (typeof mapDispatchToProps === "function") {
        dispatchProps = mapDispatchToProps(dispatch);
      } else if (typeof mapDispatchToProps === "object") {
        dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
      }
    }

    // 6.組裝最終的props
    const actualProps = Object.assign(
      {},
      wrapperProps,
      stateProps,
      dispatchProps
    );

    return actualProps;
  }
複製代碼

那麼要如何比較先後兩個 props 是否更改呢? React-Redux 裏面是採用的 shallowEqual ,也就是淺比較

// shallowEqual.js 
function is(x, y) {
  if (x === y) {
    // 處理 +0 === -0 // true 的狀況
    // 當是 +0 與 -0 時,要返回 false
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    // 處理 NaN !== NaN // true 的狀況
    // 當 x 與 y 是 NaN 時,要返回 true
    return x !== x && y !== y;
  }
}

export default function shallowEqual(objA, objB) {
  // 首先對基本數據類型的比較
  // !! 如果同引用便會返回 true
  if (is(objA, objB)) return true;
  // 因爲 is() 已經對基本數據類型作一個精確的比較,因此若是不等
  // 那就是object,因此在判斷兩個數據有一個不是 object 或者 null 以後,就能夠返回false了
  if (
    typeof objA !== "object" ||
    objA === null ||
    typeof objB !== "object" ||
    objB === null
  ) {
    return false;
  }

  // 過濾掉基本數據類型以後,就是對對象的比較了
  // 首先拿出 key 值,對 key 的長度進行對比
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  // 長度不等直接返回false
  if (keysA.length !== keysB.length) return false;
  // 長度相等的狀況下,進行循環比較
  for (let i = 0; i < keysA.length; i++) {
    // 調用 Object.prototype.hasOwnProperty 方法,判斷 objB 裏是否有 objA 中全部的 key
    // 若是有那就判斷兩個 key 值所對應的 value 是否相等(採用 is 函數)
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}
複製代碼

subscribe 回調裏先獲取最新的 props,並與上一次的 props 進行比較,若是不同才進行更新,對應的組件就會從新渲染,而若是同樣就不調用強制刷新函數,組件也就不會從新渲染。

subscribe(() => {
  const newProps = getProps(store, props);
   if (!shallowEqual(lastProps.current, newProps)) {
     lastProps.current = actualProps;
     forceUpdate();
   }
});
複製代碼

connect 完整代碼:

import { useContext, useReducer, useLayoutEffect, useRef } from "react";
import ReactReduxContext from "./context";
import shallowEqual from "./shallowEqual";
function connect(mapStateToProps, mapDispatchToProps) {
  function getProps(store, wrapperProps) {
    const { getState, dispatch } = store;
    let stateProps = {};
    let dispatchProps = {};

    // 2. 若是傳遞了 mapStateToProps 參數,則傳入 state 執行
    if (
      mapStateToProps !== "undefined" &&
      typeof mapStateToProps === "function"
    ) {
      stateProps = mapStateToProps(getState());
    }
    // 3. 默認將 dispatch 注入組件的 props 裏
    dispatchProps = { dispatch };
    // 4. 若是傳遞了 mapDispatchToProps 參數而且參數是個函數類型,則傳入 dispatch 執行
    // 5. 若是傳遞了 mapDispatchToProps 參數而且參數是個對象類型,則就要將參數看成 creators action 處理
    if (mapDispatchToProps !== "undefined") {
      if (typeof mapDispatchToProps === "function") {
        dispatchProps = mapDispatchToProps(dispatch);
      } else if (typeof mapDispatchToProps === "object") {
        dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
      }
    }

    // 6.組裝最終的props
    const actualProps = Object.assign(
      {},
      wrapperProps,
      stateProps,
      dispatchProps
    );

    return actualProps;
  }
  return function (WrappedComponent) {
    return function (props) {
      // 1. 接收傳遞下來的 store
      const store = useContext(ReactReduxContext);
      const { subscribe } = store;
      const actualProps = getProps(store, props);
      // 7.記錄上次渲染參數
      const lastProps = useRef();
      const [, forceUpdate] = useReducer((x) => x + 1, 0);
      const unsubscribe = useLayoutEffect(() => {
        subscribe(() => {
          const newProps = getProps(store, props);
          if (!shallowEqual(lastProps.current, newProps)) {
            lastProps.current = actualProps;
            forceUpdate();
          }
        });
        lastProps.current = actualProps;
        return () => {
          if (unsubscribe) {
            unsubscribe();
          }
        };
      }, [store]);
      return <WrappedComponent {...actualProps} />;
    };
  };
}

// 手寫 redux 裏的 bindActionCreators
function bindActionCreators(creators, dispatch) {
  let obj = {};
  // 遍歷對象
  for (let key in creators) {
    obj[key] = bindActionCreator(creators[key], dispatch);
  }
  return obj;
}

// 將 () => ({ type:'ADD' }) creator 轉成成 () => dispatch({ type:'ADD' })
function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args));
}

export default connect;


複製代碼

驗證下: 驗證 點擊 counter 裏的 add 按鈕,更改的是 count 值,因爲 counter 組件裏的 mapStateToProps 函數是跟 count 有關的,因此執行完 getProps 獲取到的 props 跟原先的是不同的;

而 user 組件裏 mapStateToPropsmapDispatchToProps、原有的 props 三者都與 count 無關,執行完 getProps 獲取到的 props 是跟原先同樣的,因此 user 組件不會從新渲染。

5、手寫 useSelector 與 useDispatch

useSelector: 接收個函數參數,傳入 state 並執行返回便可。當 state 更改時,強制從新執行

import ReactReduxContext from "./context";
import { useContext, useReducer, useLayoutEffect } from "reat";
export default function useSelector(selector) {
  const store = useContext(ReactReduxContext);
  const { getState, subscribe } = store;
  const [, forceUpdate] = useReducer((x) => x + 1, 0);
  const unsubscribe = useLayoutEffect(() => {
    subscribe(() => {
      forceUpdate();
    });
    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, []);
  return selector(getState());
}

複製代碼

useDispatch:

返回 dispatch 便可

import ReactReduxContext from "./context";
import { useContext } from "reat";
export default function useDispatch() {
  const store = useContext(ReactReduxContext);
  const { dispatch } = store;
  return dispatch;
}

複製代碼

6、總結

  1. React-Redux 是鏈接 ReactRedux 的庫,同時使用了 ReactRedux 的API。
  2. React-Redux 提供的兩個主要 api 是 Providerconnect
  3. Provider 的做用是接收 store 並將它放到 contextValue 上傳遞下去。
  4. connect 的做用是從 store 中選取須要的屬性(包括 statedispatch )傳遞給包裹的組件。
  5. connect 會本身判斷是否須要更新,判斷的依據是依賴的 store state 是否已經變化了。
相關文章
相關標籤/搜索