React-Redux 100行代碼簡易版探究原理。(面試熱點,React Hook + TypeScript實現)

前言

各位使用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

目標

  1. 本文目標是儘量簡短的實現react-reduxv7中的hook用法部分Provider, useSelector, useDispatch方法。(不實現connect方法)
  2. 可能會和官方版本的一些複雜實現不同,可是保證主要的流程一致。
  3. 用TypeScript實現,而且能得到完善的類型提示。

預覽

redux gif.gif

預覽地址: sl1673495.github.io/tiny-react-…

性能

說到性能這個點,自從React Hook推出之後,有了useContextuseReducer這些方便的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性能問題

想像這樣一個場景,在剛剛所描述的Context狀態管理模式下,咱們的全局狀態中有countmessage兩個狀態分別給經過StoreContext.Provider向下傳遞antd

  1. Counter計數器組件使用了count
  2. Chatroom聊天室組件使用了message

而在計數器組件經過Context中拿到的setState觸發了count改變的時候,閉包

因爲聊天室組件也利用useContext消費了用於狀態管理的StoreContext,因此聊天室組件也會被強制從新渲染,這就形成了性能浪費。

雖然這種狀況能夠用useMemo進行優化,可是手動優化和管理依賴必然會帶來必定程度的心智負擔,而在不手動優化的狀況下,確定沒法達到上面動圖中的重渲染優化。

那麼react-redux做爲社區知名的狀態管理庫,確定被不少大型項目所使用,大型項目裏的狀態可能分散在各個模塊下,它是怎麼解決上述的性能缺陷的呢?接着往下看吧。

缺陷示例

在我以前寫的類vuex語法的狀態管理庫react-vuex-hook中,就會有這樣的問題。由於它就是用了Context + useReducer的模式。

你能夠直接在 在線示例 這裏,在左側菜單欄選擇須要優化的場景,便可看到上述性能問題的重現,優化方案也已經寫在文檔底部。

這也是爲何我以爲Context + useReducer的模式更適合在小型模塊之間共享狀態,而不是在全局。

實現

介紹

本文的項目就上述性能場景提煉而成,由

  1. 聊天室組件,用了store中的count
  2. 計數器組件,用了store中的message
  3. 控制檯組件,用來監控組件的從新渲染。

用最簡短的方式實現代碼,探究react-redux爲何能在count發生改變的時候不讓使用了message的組件從新渲染。

redux的定義

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,拿到其中任意的返回值。

構建Context

利用官方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;
}
複製代碼

實現Provider

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>; }; 複製代碼

實現useDispatch

這裏就是簡單的把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>;
}
複製代碼

實現useSelector

這裏纔是重點,這個api有兩個參數。

  1. selector: 定義如何從state中取值,如state => state.count
  2. equalityFn: 定義如何判斷渲染之間值是否有改變。

在性能章節也提到過,大型應用中必須作到只有本身使用的狀態改變了,纔去從新渲染,那麼equalityFn就是判斷是否渲染的關鍵了。

關鍵流程(初始化):

  1. 根據傳入的selector從redux的store中取值。
  2. 定義一個latestSelectedState保存上一次selector返回的值。
  3. 定義一個checkForceUpdate方法用來控制當狀態發生改變的時候,讓當前組件的強制渲染。
  4. 利用store.subscribe訂閱一次redux的store,下次redux的store發生變化執行checkForceUpdate

關鍵流程(更新)

  1. 當用戶使用dispatch觸發了redux store的變更後,store會觸發checkForceUpdate方法。
  2. checkForceUpdate中,從latestSelectedState拿到上一次selector的返回值,再利用selector(store)拿到最新的值,二者利用equalityFn進行比較。
  3. 根據比較,判斷是否須要強制渲染組件。

有了這個思路,就來實現代碼吧:

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組件的使用,以及更多的優化以及邊界狀況。

從簡化版的實現入手,咱們能夠更清晰的獲得整個流程脈絡,若是你想進一步的學習源碼,也能夠考慮多花點時間去看官方源碼而且單步調試。

相關文章
相關標籤/搜索