手寫傻瓜式 React 全家桶之 Reduxjavascript
手寫傻瓜式 React 全家桶之 React-Reduxhtml
本文代碼前端
上一篇手寫了 Redux 源碼,同時也說明了 Redux 裏頭是沒有 React 相關的 API,這篇我們來寫下 React-Redux,那麼 React,Redux 以及 React-Redux 關係是:java
上一篇使用 Redux 開發了個加減器的功能,可是暴露了幾個問題:react
import store from "../store";
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
複製代碼
爲了解決這些問題,react-redux 就應運而生了git
React-Redux 是鏈接 React 應用和 Redux 狀態管理的橋樑。其中既有 React 的 API,也會依賴 Redux 的相關 API。其實 React-Redux 主要提供了兩個 api:github
將根組件嵌套在 <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
是個高階組件,通過它包裝後的組件將獲取以下功能:
props
裏會帶有 dispatch
函數connect
傳遞了第一個參數,那麼會將 store
裏的 state
數據,映射到當前組件的 props
裏connect
傳遞了第二個參數,那麼會將相關方法,映射到當前組件的 props
裏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包裹的組件實例。
}
複製代碼
mergeProps
與 options
比較少用到,重點關注前兩個參數
示例代碼:
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;
複製代碼
在函數組件裏,除了使用 connect
方式接收傳遞的 state 與 dispatch 信息以外,React-Redux 還提供了兩個 hook: useSelector
與 useDispatch
useSelector:
const result: any = useSelector(selector: Function, equalityFn?: Function)
複製代碼
平時用的更多的是第一個參數,是個函數,參數爲 store
的 state
const state = useSelector(({ count }) => ({ count }));
複製代碼
返回個對象,key
爲 count
,內容就是 store state
裏的 count
。 這樣經過 state.count
就能夠獲取到
useDispatch:
const dispatch = useDispatch()
複製代碼
執行下 useDispatch
就獲取到了 dispatch
,經過 dispatch
就能夠更改狀態
useStore:
const store = useStore()
複製代碼
返回 store
對象的引用。儘可能不要使用該 hook
,useSelector
纔是首選
示例代碼:
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>
);
}
複製代碼
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
做爲 context
的 value
值,而後直接渲染 children
便可
上面也講到 connect
是個函數,而且返回個高階組件,因此它的基本結構爲:
function connect() {
return function (WrappedComponent) {
return function (props) {
return <WrappedComponent {...props} />;
};
};
}
export default connect;
複製代碼
羅列個 connect 組件要實現的功能:
store
mapStateToProps
參數,則傳入 state
執行dispatch
注入組件的 props
裏mapDispatchToProps
參數而且參數是個函數類型,則傳入 dispatch
執行mapDispatchToProps
參數而且參數是個對象類型,則就要將參數看成 creators action
處理stateProps
,dispatchProps
,以及組件自身的 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])
複製代碼
再次驗證: 能夠看到點擊按鈕,頁面已經能夠即時響應了,那是否已經足夠完善呢?不是的,還存在些問題,下面咱們邊分析邊改進
再添加個 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 組件裏 mapStateToProps
、mapDispatchToProps
、原有的 props
三者都與 count
無關,執行完 getProps
獲取到的 props
是跟原先同樣的,因此 user 組件不會從新渲染。
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;
}
複製代碼
React-Redux
是鏈接 React
和 Redux
的庫,同時使用了 React
和 Redux
的API。React-Redux
提供的兩個主要 api 是 Provider
與 connect
Provider
的做用是接收 store
並將它放到 contextValue
上傳遞下去。connect
的做用是從 store
中選取須要的屬性(包括 state
與 dispatch
)傳遞給包裹的組件。connect
會本身判斷是否須要更新,判斷的依據是依賴的 store state
是否已經變化了。