React-Redux 100 строк кода упрощенной версии для изучения принципа.

React.js опрос

предисловие

Каждый, кто использует стек технологий реагирования, неизбежно соприкасается сredux + react-reduxЭтот набор комбинаций, как мы все знаем, redux — это очень обтекаемая библиотека, она не комбинируется с react, и ее даже можно использовать в проектах vue.

Основная реализация Redux для управления состоянием на самом деле состоит всего из нескольких строк кода.

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,
 };
}

Он использует замыкание для управления переменными, такими как состояние, а затем присваивает новое состояние состоянию через определяемый пользователем редьюсер при диспетчеризации, а затем запускает внешнюю подписку посредством подписки.

Реализация redux проста, а реализация react-redux должна быть относительно сложной, нужно учитывать, как совместить с рендерингом реакции и как оптимизировать производительность.

Цель

  1. Цель этой статьи — сделать реализацию как можно короче.react-reduxраздел использования крюка v7Provider, useSelector, useDispatchметод. (не реализованаconnectметод)
  2. Он может отличаться от некоторых сложных реализаций официальной версии, но основной процесс гарантированно непротиворечив.
  3. Реализовано на TypeScript с полными подсказками типов.

предварительный просмотр

redux gif.gif

Адрес предварительного просмотра:Забудьте об этом 1673495.GitHub.IO/tiny-react-…

представление

Говоря о производительности, с момента появления React HookuseContextа такжеuseReducerЭти удобные API, новые библиотеки управления состоянием выросли как грибы после дождя, и многие из них используютContextСделайте нисходящий проход состояния.

Приведите простой пример управления состоянием

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.

Но недостаток этой модели заключается в том, что контекст принесет некоторые проблемы с производительностью, следующее является описанием Официальный документ React:

Context性能问题

Представьте себе сценарий, в котором в соответствии с только что описанным шаблоном управления состоянием контекста наше глобальное состояниеcountа такжеmessageДва состояния задаютсяStoreContext.Providerпройти вниз

  1. CounterКомпонент счетчика используетcount
  2. ChatroomКомпонент комнаты чата используетmessage

И счетчик счетчика срабатывает SetState, полученный в контекстеcountпри смене,

Поскольку компонент чата также используетuseContextStoreContext, используемый для управления состоянием, потребляется, поэтому компонент комнаты чата также будет вынужден повторно отображать, что приводит к потере производительности.

Хотя этот случай можно использоватьuseMemoОптимизировать, но ручная оптимизация и управление зависимостями неизбежно принесут определенную степень умственной нагрузки, и без ручной оптимизации оптимизация повторного рендеринга в приведенной выше анимации точно не будет достигнута.

Такreact-reduxКак хорошо известная в сообществе библиотека управления состоянием, она должна использоваться многими крупными проектами.Состояние в крупных проектах может быть разбросано по различным модулям.Как это решает вышеупомянутые дефекты производительности? Затем посмотрите вниз.

Пример дефекта

Библиотека управления состоянием для vuex-подобного синтаксиса, который я написал ранее.react-vuex-hook, будет такая проблема. потому что он используетсяContext + useReducerрежим.

вы можете напрямуюОнлайн-примерЗдесь в левой панели меню выберите需要优化的场景, вы можете увидеть повторение вышеуказанных проблем с производительностью, и план оптимизации также был написан внизу документа.

Вот почему я думаюContext + useReducerШаблон больше подходит для совместного использования состояния между небольшими модулями, а не глобально.

использовать

Проект в этой статье извлечен из приведенных выше сценариев производительности и состоит из

  1. 聊天室Компоненты, используемые в магазинеcount
  2. 计数器Компоненты, используемые в магазинеmessage
  3. 控制台Компонент, используемый для контроля повторного рендеринга компонента.

Определение редукции

Использование редукции очень традиционно, следуйте инструкциям официальной документации по TypeScript и экспортируйте как определение типа, так и хранилище.

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компонент и передать хранилище избыточности вниз

В дочернем компоненте черезuseDispatchВы можете получить отправку редукса черезuseSelectorВы можете получить доступ к хранилищу и получить любое возвращаемое значение.

выполнить

Реализуйте код кратчайшим образом и узнайте, почему react-redux можно использовать вcountНе используйте его, когда он меняетсяmessageКомпонент перерисовывается.

Реализовать контекст

Используйте официальный API для создания контекста и предоставления пользовательского хука:useReduxContextПерейдите к этому контексту и дайте несколько сообщений об ошибках, если вы забыли обернуть провайдера:

Для тех, кто не знаком с пользовательскими хуками, вы можете прочитать эту статью, которую я написал ранее:
Используйте 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>;
};

Реализировать ueformath.

Здесь просто вернуть диспетчер и передать дженерики, чтобы получить подсказки типа при внешнем использовании.

Для тех, кто не знаком с обобщенным выводом, вы можете прочитать предыдущую статью:
Упрощенная версия 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>;
}

Реализовать использование селектора

В том-то и дело, что у этого апи есть два параметра.

  1. selector: определить, как получить значения из состояния, напримерstate => state.count
  2. equalityFn: определяет, как определить, изменилось ли значение между рендерами.

В главе о производительности также упоминалось, что в крупномасштабных приложениях его необходимо повторно отображать только тогда, когда изменяется используемое им состояние.equalityFnЭто ключ к решению, следует ли рендерить.

Ключевой процесс (инициализация):

  1. Получите значение из хранилища избыточности в соответствии с переданным селектором.
  2. определитьlatestSelectedStateСохраните последнее значение, возвращенное селектором.
  3. определитьcheckForceUpdateМетод используется для управления принудительной отрисовкой текущего компонента при изменении состояния.
  4. использоватьstore.subscribeПодпишитесь на хранилище избыточности один раз и выполните в следующий раз, когда хранилище избыточности изменится.checkForceUpdate.

Ключевые процессы (обновлено)

  1. когда пользователь используетdispatchПосле того, как изменения хранилища избыточности будут запущены, хранилище сработаетcheckForceUpdateметод.
  2. checkForceUpdateв отlatestSelectedStateПолучите возвращаемое значение последнего селектора, а затем используйте 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/forget 1673495/he…

Реализация оригинального react-redux определенно сложнее, чем упрощенная версия здесь, она учитывает использование компонентов класса, а также больше оптимизаций и пограничных случаев.

Начиная с реализации упрощенной версии, мы можем более четко получить весь контекст процесса.Если вы хотите глубже изучить исходный код, вы также можете подумать о том, чтобы потратить больше времени на просмотр официального исходного кода и пошаговую отладку.