各位使用react技術棧的小夥伴都不可避免的接觸過redux
+ react-redux
的這套組合,衆所周知redux是一個很是精簡的庫,它和react是沒有作任何結合的,甚至能夠在vue項目中使用。css
redux的核心狀態管理實現其實就幾行代碼vue
function createStore(reducer) {
let currentState
let subscribers = []
function dispatch(action) {
currentState = reducer(currentState, action);
subscribers.forEach(s => s())
}
function getState() {
return currentState;
}
function subscribe(subscriber) {
subscribers.push(subscriber)
return function unsubscribe() {
...
}
}
dispatch({ type: 'INIT' });
return {
dispatch,
getState,
};
}
複製代碼
它就是利用閉包管理了state等變量,而後在dispatch的時候經過用戶定義reducer拿到新狀態賦值給state,再把外部經過subscribe的訂閱給觸發一下。react
那redux的實現簡單了,react-redux的實現確定就須要相對複雜,它須要考慮如何和react的渲染結合起來,如何優化性能。git
react-redux
v7中的hook用法部分Provider
, useSelector
, useDispatch
方法。(不實現connect
方法)說到性能這個點,自從React Hook推出之後,有了useContext
和useReducer
這些方便的api,新的狀態管理庫如同雨後春筍版的冒了出來,其中的不少就是利用了Context
作狀態的向下傳遞。github
舉一個最簡單的狀態管理的例子vuex
export const StoreContext = React.createContext();
function App({ children }) {
const [state, setState] = useState({});
return <StoreContext.Provider value={{ state, setState }}>{children}</StoreContext.Provider>; } function Son() { const { state } = useContext(StoreContext); return <div>state是{state.xxx}</div>; } 複製代碼
利用useState或者useContext,能夠很輕鬆的在全部組件之間經過Context共享狀態。redux
可是這種模式的缺點在於Context會帶來必定的性能問題,下面是React官方文檔中的描述:api
想像這樣一個場景,在剛剛所描述的Context狀態管理模式下,咱們的全局狀態中有count
和message
兩個狀態分別給經過StoreContext.Provider
向下傳遞antd
Counter
計數器組件使用了count
Chatroom
聊天室組件使用了message
而在計數器組件經過Context中拿到的setState觸發了count
改變的時候,閉包
因爲聊天室組件也利用useContext
消費了用於狀態管理的StoreContext,因此聊天室組件也會被強制從新渲染,這就形成了性能浪費。
雖然這種狀況能夠用useMemo
進行優化,可是手動優化和管理依賴必然會帶來必定程度的心智負擔,而在不手動優化的狀況下,確定沒法達到上面動圖中的重渲染優化。
那麼react-redux
做爲社區知名的狀態管理庫,確定被不少大型項目所使用,大型項目裏的狀態可能分散在各個模塊下,它是怎麼解決上述的性能缺陷的呢?接着往下看吧。
在我以前寫的類vuex語法的狀態管理庫react-vuex-hook中,就會有這樣的問題。由於它就是用了Context
+ useReducer
的模式。
你能夠直接在 在線示例 這裏,在左側菜單欄選擇須要優化的場景
,便可看到上述性能問題的重現,優化方案也已經寫在文檔底部。
這也是爲何我以爲Context
+ useReducer
的模式更適合在小型模塊之間共享狀態,而不是在全局。
本文的項目就上述性能場景提煉而成,由
聊天室
組件,用了store中的count
計數器
組件,用了store中的message
控制檯
組件,用來監控組件的從新渲染。用最簡短的方式實現代碼,探究react-redux爲何能在count
發生改變的時候不讓使用了message
的組件從新渲染。
redux的使用很傳統,跟着官方文檔對於TypeScript的指導走起來,而且把類型定義和store都export出去。
import { createStore } from 'redux';
type AddAction = {
type: 'add';
};
type ChatAction = {
type: 'chat';
payload: string;
};
type LogAction = {
type: 'log';
payload: string;
};
const initState = {
message: 'Hello',
logs: [] as string[],
};
export type ActionType = AddAction | ChatAction | LogAction;
export type State = typeof initState;
function reducer(state: State, action: ActionType): State {
switch (action.type) {
case 'add':
return {
...state,
count: state.count + 1,
};
case 'chat':
return {
...state,
message: action.payload,
};
case 'log':
return {
...state,
logs: [action.payload, ...state.logs],
};
default:
return initState;
}
}
export const store = createStore(reducer);
複製代碼
import React, { useState, useCallback } from 'react';
import { Card, Button, Input } from 'antd';
import { Provider, useSelector, useDispatch } from '../src';
import { store, State, ActionType } from './store';
import './index.css';
import 'antd/dist/antd.css';
function Count() {
const count = useSelector((state: State) => state.count);
const dispatch = useDispatch<ActionType>();
// 同步的add
const add = useCallback(() => dispatch({ type: 'add' }), []);
dispatch({
type: 'log',
payload: '計數器組件從新渲染🚀',
});
return (
<Card hoverable style={{ marginBottom: 24 }}> <h1>計數器</h1> <div className="chunk"> <div className="chunk">store中的count如今是 {count}</div> <Button onClick={add}>add</Button> </div> </Card>
);
}
export default () => {
return (
<Provider store={store}> <Count /> </Provider>
);
};
複製代碼
能夠看到,咱們用Provider
組件裏包裹了Count
組件,而且把redux的store傳遞了下去
在子組件裏,經過useDispatch
能夠拿到redux的dispatch, 經過useSelector
能夠訪問到store,拿到其中任意的返回值。
利用官方api構建context,而且提供一個自定義hook: useReduxContext
去訪問這個context,對於忘了用Provider包裹的狀況進行一些錯誤提示:
對於不熟悉自定義hook的小夥伴,能夠看我以前寫的這篇文章:
使用React Hooks + 自定義Hook封裝一步一步打造一個完善的小型應用。
import React, { useContext } from 'react';
import { Store } from 'redux';
interface ContextType {
store: Store;
}
export const Context = React.createContext<ContextType | null>(null);
export function useReduxContext() {
const contextValue = useContext(Context);
if (!contextValue) {
throw new Error(
'could not find react-redux context value; please ensure the component is wrapped in a <Provider>',
);
}
return contextValue;
}
複製代碼
import React, { FC } from 'react';
import { Store } from 'redux';
import { Context } from './Context';
interface ProviderProps {
store: Store;
}
export const Provider: FC<ProviderProps> = ({ store, children }) => {
return <Context.Provider value={{ store }}>{children}</Context.Provider>; }; 複製代碼
這裏就是簡單的把dispatch返回出去,經過泛型傳遞讓外部使用的時候能夠得到類型提示。
泛型推導不熟悉的小夥伴能夠看一下以前這篇:
進階實現智能類型推導的簡化版Vuex
import { useReduxContext } from './Context';
import { Dispatch, Action } from 'redux';
export function useDispatch<A extends Action>() {
const { store } = useReduxContext();
return store.dispatch as Dispatch<A>;
}
複製代碼
這裏纔是重點,這個api有兩個參數。
selector
: 定義如何從state中取值,如state => state.count
equalityFn
: 定義如何判斷渲染之間值是否有改變。在性能章節也提到過,大型應用中必須作到只有本身使用的狀態改變了,纔去從新渲染,那麼equalityFn
就是判斷是否渲染的關鍵了。
關鍵流程(初始化):
latestSelectedState
保存上一次selector返回的值。checkForceUpdate
方法用來控制當狀態發生改變的時候,讓當前組件的強制渲染。store.subscribe
訂閱一次redux的store,下次redux的store發生變化執行checkForceUpdate
。關鍵流程(更新)
dispatch
觸發了redux store的變更後,store會觸發checkForceUpdate
方法。checkForceUpdate
中,從latestSelectedState
拿到上一次selector的返回值,再利用selector(store)拿到最新的值,二者利用equalityFn
進行比較。有了這個思路,就來實現代碼吧:
import { useReducer, useRef, useEffect } from 'react';
import { useReduxContext } from './Context';
type Selector<State, Selected> = (state: State) => Selected;
type EqualityFn<Selected> = (a: Selected, b: Selected) => boolean;
// 默認比較的方法
const defaultEqualityFn = <T>(a: T, b: T) => a === b;
export function useSelector<State, Selected>(
selector: Selector<State, Selected>,
equalityFn: EqualityFn<Selected> = defaultEqualityFn,
) {
const { store } = useReduxContext();
// 強制讓當前組件渲染的方法。
const [, forceRender] = useReducer(s => s + 1, 0);
// 存儲上一次selector的返回值。
const latestSelectedState = useRef<Selected>();
// 根據用戶傳入的selector,從store中拿到用戶想要的值。
const selectedState = selector(store.getState());
// 檢查是否須要強制更新
function checkForUpdates() {
// 從store中拿到最新的值
const newSelectedState = selector(store.getState());
// 若是比較相等,就啥也不作
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return;
}
// 不然更新ref中保存的上一次渲染的值
// 而後強制渲染
latestSelectedState.current = newSelectedState;
forceRender();
}
// 組件第一次渲染後 執行訂閱store的邏輯
useEffect(() => {
// 🚀重點,去訂閱redux store的變化
// 在用戶調用了dispatch後,執行checkForUpdates
const unsubscribe = store.subscribe(checkForUpdates);
// 組件被銷燬後 須要調用unsubscribe中止訂閱
return unsubscribe;
}, []);
return selectedState;
}
複製代碼
本文涉及到的源碼地址:
github.com/sl1673495/t…
原版的react-redux的實現確定比這裏的簡化版要複雜的多,它要考慮class組件的使用,以及更多的優化以及邊界狀況。
從簡化版的實現入手,咱們能夠更清晰的獲得整個流程脈絡,若是你想進一步的學習源碼,也能夠考慮多花點時間去看官方源碼而且單步調試。