предисловие
Каждый, кто использует стек технологий реагирования, неизбежно соприкасается с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 должна быть относительно сложной, нужно учитывать, как совместить с рендерингом реакции и как оптимизировать производительность.
Цель
- Цель этой статьи — сделать реализацию как можно короче.
react-reduxраздел использования крюка v7Provider,useSelector,useDispatchметод. (не реализованаconnectметод) - Он может отличаться от некоторых сложных реализаций официальной версии, но основной процесс гарантированно непротиворечив.
- Реализовано на TypeScript с полными подсказками типов.
предварительный просмотр
Адрес предварительного просмотра:Забудьте об этом 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:
Представьте себе сценарий, в котором в соответствии с только что описанным шаблоном управления состоянием контекста наше глобальное состояниеcountа такжеmessageДва состояния задаютсяStoreContext.Providerпройти вниз
-
CounterКомпонент счетчика используетcount -
ChatroomКомпонент комнаты чата используетmessage
И счетчик счетчика срабатывает SetState, полученный в контекстеcountпри смене,
Поскольку компонент чата также используетuseContextStoreContext, используемый для управления состоянием, потребляется, поэтому компонент комнаты чата также будет вынужден повторно отображать, что приводит к потере производительности.
Хотя этот случай можно использоватьuseMemoОптимизировать, но ручная оптимизация и управление зависимостями неизбежно принесут определенную степень умственной нагрузки, и без ручной оптимизации оптимизация повторного рендеринга в приведенной выше анимации точно не будет достигнута.
Такreact-reduxКак хорошо известная в сообществе библиотека управления состоянием, она должна использоваться многими крупными проектами.Состояние в крупных проектах может быть разбросано по различным модулям.Как это решает вышеупомянутые дефекты производительности? Затем посмотрите вниз.
Пример дефекта
Библиотека управления состоянием для vuex-подобного синтаксиса, который я написал ранее.react-vuex-hook, будет такая проблема. потому что он используетсяContext + useReducerрежим.
вы можете напрямуюОнлайн-примерЗдесь в левой панели меню выберите需要优化的场景, вы можете увидеть повторение вышеуказанных проблем с производительностью, и план оптимизации также был написан внизу документа.
Вот почему я думаюContext + useReducerШаблон больше подходит для совместного использования состояния между небольшими модулями, а не глобально.
использовать
Проект в этой статье извлечен из приведенных выше сценариев производительности и состоит из
-
聊天室Компоненты, используемые в магазинеcount -
计数器Компоненты, используемые в магазинеmessage -
控制台Компонент, используемый для контроля повторного рендеринга компонента.
Определение редукции
Использование редукции очень традиционно, следуйте инструкциям официальной документации по 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>;
}
Реализовать использование селектора
В том-то и дело, что у этого апи есть два параметра.
-
selector: определить, как получить значения из состояния, напримерstate => state.count -
equalityFn: определяет, как определить, изменилось ли значение между рендерами.
В главе о производительности также упоминалось, что в крупномасштабных приложениях его необходимо повторно отображать только тогда, когда изменяется используемое им состояние.equalityFnЭто ключ к решению, следует ли рендерить.
Ключевой процесс (инициализация):
- Получите значение из хранилища избыточности в соответствии с переданным селектором.
- определить
latestSelectedStateСохраните последнее значение, возвращенное селектором. - определить
checkForceUpdateМетод используется для управления принудительной отрисовкой текущего компонента при изменении состояния. - использовать
store.subscribeПодпишитесь на хранилище избыточности один раз и выполните в следующий раз, когда хранилище избыточности изменится.checkForceUpdate.
Ключевые процессы (обновлено)
- когда пользователь использует
dispatchПосле того, как изменения хранилища избыточности будут запущены, хранилище сработаетcheckForceUpdateметод. -
checkForceUpdateв отlatestSelectedStateПолучите возвращаемое значение последнего селектора, а затем используйте 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/forget 1673495/he…
Реализация оригинального react-redux определенно сложнее, чем упрощенная версия здесь, она учитывает использование компонентов класса, а также больше оптимизаций и пограничных случаев.
Начиная с реализации упрощенной версии, мы можем более четко получить весь контекст процесса.Если вы хотите глубже изучить исходный код, вы также можете подумать о том, чтобы потратить больше времени на просмотр официального исходного кода и пошаговую отладку.