咱們在使用react
進行開發時,一般會搭配react-redux
進行狀態管理,react-redux
實際上是基於redux
封裝的,使開發者更方便的使用redux
管理數據,因此要明確redux
徹底可使用。咱們要學習react-redux
首先要先學習redux
。javascript
redux簡單實現demohtml
咱們先來看一下redux
的基本使用,下面的代碼經過createStore
來建立一個store
,建立成功後會返回三個API(subscribe
、dispatch
、getState
)。咱們經過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
經過上面的例子,咱們首先要實現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
實現就超簡單了,由於內部變量currentState
始終保持最新,咱們只要將這個變量返回就行了,一行代碼搞定數組
// 獲取最新state
function getState() {
return currentState;
}
複製代碼
咱們定義了內部變量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
接受一個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源碼
咱們定義好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函數傳入倆個參數,分別是mapStateToProps
和mapDispatchToProps
,做用跟名字相同,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
,咱們須要在組件渲染前獲取到所需的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
組件都須要的,因此咱們應該抽離出公共部分。
在類組件咱們想要複用邏輯只能經過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變化就會從新渲染,當咱們所依賴的值沒有改變時,咱們無需從新渲染。
咱們先解決上面的說的第一個問題,想支持動態的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()
來獲取到當前store
,useEffect
第二個參數依賴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組件中的Provider
、connect
替換掉,代碼是能夠正常的使用。完整demo
上面實現了connect
用於共享邏輯,雖然函數組件也能夠經過它進行包裹使用,但React Hook
的出現讓咱們對於邏輯複用有了更好的辦法,那就是本身寫一個Hook
。useSelector
是react-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;
}
複製代碼
注意:這裏須要使用lastSelectorFn
Ref存儲選擇器
,不然useEffect依賴selectorFn
會形成死循環。
實現useDispatch
就超簡單了,就是直接返回store.dispatch
就好
import { useContext } from "react";
import storeContext from "./storeContext";
export default function useDispatch(seletorFn) {
const store = useContext(storeContext);
return store.dispatch;
}
複製代碼
本文中實現的redux
、react-redux
都只是實現了一小部分API,而且沒有處理異常狀況。但與源碼的核心大致相同。但願閱讀完的小夥伴有所收穫,若是不過癮還能夠去閱讀下源碼哦。