Готовый «Поток данных React Hooks»

JavaScript React.js

1. Введение

React Hooks постепенно принимаются отечественными front-end командами, но схема потока данных на основе Hooks еще не отлажена, у нас есть «100» похожих вариантов, но каждый имеет свои преимущества и недостатки, что затрудняет выбор.

На этой неделе автор подробно расскажет о понимании потока данных Hooks.Я считаю, что после прочтения статьи вы сможете увидеть суть схемы потока данных Hooks, которая расцветает.

2 Интенсивное чтение

Говоря о потоке данных на основе React Hooks, начнем с базового решения, которое с наименьшей вероятностью вызовет разногласия.

Однокомпонентный поток данных

Простейший поток данных для одного компонента должен бытьuseState:

function App() {
  const [count, setCount] = useState();
}

useStateИспользование его внутри компонента бесспорно, поэтому следующей темой должно быть совместное использование потока данных между компонентами.

Совместное использование потока данных между компонентами

Самое простое решение для компонентов:useContext:

const CountContext = createContext();

function App() {
  const [count, setCount] = useState();
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <Child />
    </CountContext.Provider>
  );
}

function Child() {
  const { count } = useContext(CountContext);
}

Использование — это все официальные API, что, очевидно, не вызывает споров, но проблема в том, что данные и пользовательский интерфейс не разделены.unstated-nextРешение было найдено для вас.

Разделение потоков данных и компонентов

unstated-nextМожет помочь вам определить приведенный выше пример вAppДанные в нем разделены, чтобы сформировать пользовательский хук управления данными:

import { createContainer } from "unstated-next";

function useCounter() {
  const [count, setCount] = useState();
  return { count, setCount };
}

const Counter = createContainer(useCounter);

function App() {
  return (
    <Counter.Provider>
      <Child />
    </Counter.Provider>
  );
}

function Child() {
  const { count } = Counter.useContainer();
}

данные иAppРазвязанный, теперьCounterбольше никогдаAppграница,CounterМожно комбинировать с другими компонентами.

В это время медленно всплывали проблемы с производительностью, и первое, что нужно сделать, этоuseStateНе имея возможности объединить и обновить проблему, мы, естественно, подумали об использованииuseReducerрешить.

объединить обновление

useReducerМожно объединять и обновлять данные, что также является официальным API React, без каких-либо противоречий:

import { createContainer } from "unstated-next";

function useCounter() {
  const [state, dispath] = useReducer(
    (state, action) => {
      switch (action.type) {
        case "setCount":
          return {
            ...state,
            count: action.setCount(state.count),
          };
        case "setFoo":
          return {
            ...state,
            foo: action.setFoo(state.foo),
          };
        default:
          return state;
      }
      return state;
    },
    { count: 0, foo: 0 }
  );

  return { ...state, dispatch };
}

const Counter = createContainer(useCounter);

function App() {
  return (
    <Counter.Provider>
      <Child />
    </Counter.Provider>
  );
}

function Child() {
  const { count } = Counter.useContainer();
}

Даже если он обновляется одновременноcountа такжеfoo, мы также можем абстрагировать его вreducerспособ слияния обновлений.

Однако есть проблемы с производительностью:

function ChildCount() {
  const { count } = Counter.useContainer();
}

function ChildFoo() {
  const { foo } = Counter.useContainer();
}

возобновитьfooчас,ChildCountа такжеChildFooбудет выполняться одновременно, ноChildCountбесполезноfooА? Причина в том,Counter.useContainerПредоставленный поток данных является ссылочной сущностью, чьи дочерние узлыfooИзменение ссылки приведет к повторному выполнению всего хука, а затем будут повторно отображены все компоненты, которые на него ссылаются.

На данный момент мы обнаружили, что можем использовать ReduxuseSelectorВнедрение обновлений по запросу.

Обновление по требованию

Во-первых, мы используем Redux для преобразования потока данных:

import { createStore } from "redux";
import { Provider, useSelector } from "react-redux";

function reducer(state, action) {
  switch (action.type) {
    case "setCount":
      return {
        ...state,
        count: action.setCount(state.count),
      };
    case "setFoo":
      return {
        ...state,
        foo: action.setFoo(state.foo),
      };
    default:
      return state;
  }
  return state;
}

function App() {
  return (
    <Provider store={store}>
      <Child />
    </Provider>
  );
}

function Child() {
  const { count } = useSelector(
    (state) => ({ count: state.count }),
    shallowEqual
  );
}

useSelectorможет позволитьChildсуществуетcountобновлять, когда оно изменяется, иfooНе обновляться при изменении, что близко к идеальной цели производительности.

ноuseSelectorФункция предназначена только для предотвращения обновления компонента, когда результат вычисления не изменяется, но не гарантирует, что ссылка возвращаемого результата не изменится.

Предотвращение частых изменений ссылок на данные

Для приведенного выше сценария получитеcountСсылка неизменна,Но не обязательно для других сценариев.

Например:

function Child() {
  const user = useSelector((state) => ({ user: state.user }), shallowEqual);

  return <UserPage user={user} />;
}

ПредположениеuserСсылка на объект меняется каждый раз, когда обновляется поток данных.,ТакshallowEqualКонечно, это не работает, тогда мы заменяем его наdeepEqualКак насчет глубокого контраста? В результате ссылки все равно будут меняться, но повторные рендеры будут реже:

function Child() {
  const user = useSelector(
    (state) => ({ user: state.user }),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  // 但此处拿到的 user 引用还是会变化

  return <UserPage user={user} />;
}

ты чувствуешь этоdeepEqualПри действии не запускается повторный рендеринг,userСсылка не изменится? Ответ заключается в том, что он изменится, потому чтоuserОбъект меняется каждый раз при обновлении потока данных,useSelectorсуществуетdeepEqualПод действием действия не запускается повторный рендеринг, но поскольку глобальный редюсер скрывает собственный повторный рендеринг компонента, эта функция все равно будет выполняться повторно.userСсылки постоянно меняются.

следовательноuseSelector deepEqualдолжно быть сuseDeepMemoиспользуется в сочетании для обеспеченияuserСсылки меняются не часто:

function Child() {
  const user = useSelector(
    (state) => ({ user: state.user }),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />;
}

Конечно это крайний случай, пока вы видитеdeepEqualа такжеuseSelectorВ то же время необходимо спросить себя, не изменится ли неожиданно ссылка на возвращаемое значение.

функция запроса кэша

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

В приведенном выше примере функция запроса относительно проста, но если функция запроса очень сложна:

function Child() {
  const user = useSelector(
    (state) => ({ user: verySlowFunction(state.user) }),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />;
}

давайте предположимverySlowFunctionЧтобы пройти n 3 степени 1000 компонентов в холсте, время повторного рендеринга компонентов совершенно незначительно по сравнению со временем запроса, нам нужно рассмотреть функцию запроса кэша.

Один из способов - использоватьreselectКэширование на основе ссылок на параметры.

Представьте, еслиstate.userцитаты меняются редко, ноverySlowFunctionочень медленно, в идеалеstate.userВыполнить повторно после изменения ссылкиverySlowFunction, но в приведенном выше примереuseSelectorНе знаю, что можно так оптимизировать, могу только тупо повторять каждый рендер.verySlowFunction, даже еслиstate.userНичего не изменилось.

На данный момент мы хотим сказать ссылку,state.userЯвляется ли изменение ключом к повторному выполнению:

import { createSelector } from "reselect";

const userSelector = createSelector(
  (state) => state.user,
  (user) => verySlowFunction(user)
);

function Child() {
  const user = useSelector(
    (state) => userSelector(state),
    // 当 user 值变化时才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />;
}

В приведенном выше примере поcreateSelectorсозданныйuserSelectorБудет кэшироваться слой за слоем, когда вернется первый параметрstate.userПри обращении к тому же, он напрямую вернется к первому результатам производительности, пока их использование не будет продолжать реализовывать изменения.

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

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

Кэшированные запросы в сочетании с внешними переменными

Если пользователи, которых мы хотим запросить, из разных регионов, нам нужно передатьareaIdопределено, его можно разделить на две функции Selector:

import { createSelector } from "reselect";

const areaSelector = (state, props) => state.areas[props.areaId].user;

const userSelector = createSelector(areaSelector, (user) =>
  verySlowFunction(user)
);

function Child() {
  const user = useSelector(
    (state) => userSelector(state, { areaId: 1 }),
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />;
}

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

ноuserSelectorКэш будет недействительным, если он предоставляется нескольким компонентам, потому что мы создаем только один экземпляр Selector, поэтому эта функция также должна обернуть слой форм более высокого порядка:

import { createSelector } from "reselect";

const userSelector = () =>
  createSelector(areaSelector, (user) => verySlowFunction(user));

function Child() {
  const customSelector = useMemo(userSelector, []);

  const user = useSelector(
    (state) => customSelector(state, { areaId: 1 }),
    deepEqual
  );
}

Поэтому для связи объединения внешних переменных также необходимоuseMemoа такжеuseSelectorВ сочетании с,useMemoобрабатывать кеширование ссылок для зависимостей внешних переменных,useSelectorОбработка кэширования ссылок, связанных с хранилищем.

3 Резюме

Схема потока данных, основанная на хуках, не идеальна. Когда я писал эту статью, я чувствовал, что такая схема относится к «поверхностным и глубоким». Простые сцены легко понять. По мере того, как сцены становятся более сложными, схема становится все более и более сложным.

Однако эта идея управления неизменяемым потоком данных дает разработчикам очень широкие возможности управления кешем. Пока они полностью понимают вышеизложенные концепции, они могут разработать очень «ожидаемую» модель управления кешем данных. Пока они тщательно поддерживаются, все становится очень упорядоченно.

Адрес обсуждения:Интенсивное чтение «Потока данных React Hooks» · Выпуск № 242 · dt-fe/weekly

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

Сфокусируйся наАккаунт WeChat для интенсивного чтения в интерфейсе

Заявление об авторских правах: Бесплатная перепечатка - некоммерческая - не производная - сохранить авторство (Лицензия Creative Commons 3.0)