Библиотека управления состоянием нового поколения React — отдача

внешний интерфейс React.js
Библиотека управления состоянием нового поколения React — отдача

введение

Что касается библиотеки управления состоянием реакции, вы можете быть знакомы с Redux, но, хотя Redux спроектирован так, чтобы быть относительно простым, у него есть некоторые проблемы, такие как необходимость написания большого количества кода шаблона; необходимо согласиться с тем, что новое состояние объект совершенно новый, если мы его не используем Совершенно новые объекты могут не обновляться.Это обычная проблема не обновляющегося статуса редукса, поэтому разработчикам необходимо убедиться, что они должны вводить библиотеки, такие как immer; кроме того, сам редукс это независимая от фреймворка библиотека, ее необходимо комбинировать с redux-react для использования в реакции. Приходится прибегать к таким библиотекам, как redux toolkit или rematch, которые имеют множество встроенных передовых практик и редизайн интерфейсов, но в то же время это также увеличивает стоимость обучения разработчиков. Таким образом, колеса управления состоянием реагирования появляются бесконечным потоком.Следующее познакомит вас с будущим дизайном библиотеки управления состоянием реагирования — recoil.

Введение

Слоган recoil очень прост: библиотека управления состоянием для React. Это не независимая от фреймворка государственная библиотека, она создана специально для реагирования.

Как и react, recoil также является библиотекой с открытым исходным кодом для facebook. Официально выделяют три основные функции:

  1. Minimal и Reactish: API в стиле Minimal и React.

  2. График потока данных: График потока данных. И производные данные, и асинхронные запросы — это чистые функции с эффективными подписками внутри.

  3. Наблюдение между приложениями: мониторинг между приложениями, позволяющий отслеживать общий статус.

основные идеи дизайна

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

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

recoil делит состояние на атомы, и дерево компонентов реакции будет подписываться только на нужное им состояние. В этом сценарии элементы слева и справа от дерева компонентов подписываются на разные атомы, и при изменении атома они будут обновлять только соответствующий подписанный узел.В то же время отдача также поддерживает «производное состояние», что означает, что существующие атомы объединяются в новое состояние (селектор), и новое состояние также может стать зависимостью других состояний.Поддерживает не только синхронные селекторы, но и recoil также поддерживает асинхронные селекторы.Единственным требованием recoil для селекторов является то, что они должны быть чистой функцией.Идея дизайна Recoil заключается в том, что мы помещаем состояние, разделяющее атом, а затем извлекаем больше из селектора состояния, последнего статуса подписки дерева компонентов React, который им нужен, когда есть обновления статуса, атом, атом и его нижестоящий узел изменяются только там, подписываются на их компоненты будут обновлены. Другими словами, отдача фактически создавалаНаправленный ациклический граф, этот граф ортогонален дереву компонентов реакции, и его состояние полностью совпадает с деревом компонентов реакции.разъединениеиз.

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

После стольких надуваний давайте взглянем на простое использование. В отличие от redux, которая является независимой от фреймворка библиотекой управления состоянием, поскольку Recoil — это библиотека управления состоянием, специально разработанная для React, ее API наполнен «стилем реакции». Recoil поддерживает только хуки API, которые, можно сказать, очень лаконичны в использовании. Посмотрите нижеDemo:

import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue
} from "recoil";

export default function App() {
  return (
    <RecoilRoot>
      <Demo />
    </RecoilRoot>
  );
}

const textState = atom({
  key: "textState", 
  default: "" 
});

const charCountState = selector({
  key:'charCountState',
  get: ({get}) => {
    // 要求是纯函数
    const text = get(textState)
    return text.length
  }
})

function Demo() {
  const [text, setText] = useRecoilState(textState);
  const count = useRecoilValue(charCountState)
  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <br />
      Echo: {text}
      <br />
      charCount: {count}
    </>
  );
}
  • Как и в React Redux, в recoil также есть Provider — RecoilRoot, который используется для глобального совместного использования некоторых методов и состояний.

  • Атом (атом) — наименьшая единица состояния в отдаче. Атом представляет собой значение, которое можно читать, записывать и на которое можно подписаться. Он должен иметь значение, отличное от других атомов.Уникальность и неизменностьключ. Данные могут быть определены через атом.

  • Селектор немного похож на селектор в React-Redux, он также используется для «производного» состояния, но отличается от React-Redux:

    • Селектор React-redux — это чистая функция: при изменении глобально уникального состояния он всегда будет запускаться и вычислять новое состояние из глобально уникального состояния.

    • В отдаче также требуется, чтобы get of options.селектора был чистой функцией, а переданный метод get использовался для получения других атомов.тогда и только тогда, когда зависим atom Изменено, и компонент подписывается на селектор, то он будет фактически пересчитан, а это значит, что вычисленное значение будет закэшировано.Когда зависимость не изменилась, оно будет фактически прочитано непосредственно из кеша и возвращено. Возвращаемый селектор также является атомом, что означает, что производное состояние на самом деле является атомом, и его также можно использовать в качестве зависимости от других селекторов.

Очевидно, что recoil собирает зависимости через входные параметры get в функции get, recoil поддерживает динамический сбор зависимостей, а это значит, что get можно вызывать в условиях:

const toggleState = atom({key: 'Toggle', default: false});

const mySelector = selector({
  key: 'MySelector',
  get: ({get}) => {
    const toggle = get(toggleState);
    if (toggle) {
      return get(selectorA);
    } else {
      return get(selectorB);
    }
  },
});

асинхронный

recoil естественно поддерживает асинхронность, использование очень простое, и нет необходимости настраивать какие-либо асинхронные плагины, см.Demo:

const asyncDataState = selector({
  key: "asyncData",
  get: async ({get}) => {
   // 要求是纯函数
    return await getAsyncData();
  }
});

function AsyncComp() {
  const asyncData = useRecoilValue(asyncDataState);
  return <>{asyncData}</>;
}
function Demo() {
  return (
    <React.Suspense fallback={<>loading...</>}>
      <AsyncComp />
    </React.Suspense>
  );
}

Из-за отдача свойств естественной поддержки реагируют напрямую, поэтому используйте время userecoilvalue доступом к данным, если асинхронное состояние ожидается, то по умолчанию будет выбросить обещание, требует использования наружного использования RACT.SUSPENSE, а затем реагировать в режиме реакции. Содержимое затупочкой; если сообщается, что ошибки могут также выбросить содержимое внутри, захватывают внешнюю ошибку диаграммы. Если вы не хотите использовать эту функцию, вы можете использовать UserEcoilValueloadable прямой доступ к асинхронному состоянию,demo:

function AsyncComp() {
  const asyncState = useRecoilValueLoadable(asyncDataState);
  if (asyncState.state === "loading") {
    return <>loading...</>;
  }
  if (asyncState.state === "hasError") {
    return <>has error....</>;
  }
  if (asyncState.state === "hasValue") {
    return <>{asyncState.contents}</>;
  }
  return null;
}

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

зависит от внешних переменных

Мы часто сталкиваемся с проблемой нечистого состояния, если состояние на самом деле зависит от внешних переменных, в recoil есть поддержка selectorFamily:

const getUserInfoState = selectorFamily({
  key: "userInfo",
  get: (userId) => ({ get }) => {
    return queryUserState({userId: id, xxx: get(xxx) });
  },
});

function MyComponent({ userID }) {
  
  const number = useRecoilValue(getUserInfoState(userID));
  //...
}

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

Анализ исходного кода

Если увидеть здесь, только чтобы понять, что простой пример выше, то мы могли бы сказать «На этом»? Не должно быть слишком сложно реализовать, вот простойреализованная версия, хотя функции похожи, архитектура совершенно другая.Исходный код recoil наследует прекрасные традиции исходного кода react, который очень трудно читать. . .

Его основные функции исходного кода разделены на несколько частей:

  • Логика, связанная с графом

  • Nodeatom и селектор абстрагируются внутри как узел

  • RecoilRoot — это в основном некоторый recoilRoot, используемый снаружи,

  • Тип RecoilValue, открытый для внешнего мира. То есть возвращаемое значение атома и селектора.

  • Крючки, относящиеся к используемым крючкам.

  • Моментальный снимок Моментальный снимок состояния, обеспечивающий регистрацию состояния и откат.

  • Какой-то другой код, который невозможно прочитать. . .

Давайте поговорим о моем поверхностном понимании чтения исходного кода в эти дни, приветствуем исправления от больших парней.

Поддержка параллельного режима

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

Cocurrent mode

Давайте поговорим о том, что является режимом Cocrerent Rection. Введение официального веб-сайта заключается в том, что диапазон новых функций помогают приложениям Ract остаются реагировать и и элегантным использованием возможностей пользовательского оборудования и скорость сети. Реагистрация миграции в волокну архитектуру предназначена для реализации одновременного режима, а реагирование имеет две фазы в новой архитектуре:

  • этап рендеринга (рендеринга)

  • этап фиксации

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

Проблема несоответствия между пользовательским интерфейсом и состоянием

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

раствор отдачи

общая структура данных

atom

atom на самом деле вызывает baseAtom, который имеет переменную замыкания defaultLoadable внутри baseAtom для записи текущего значения по умолчанию. Функции getAtom и setAtom объявляются и, наконец, передаются в registerNode для завершения регистрации.

function baseAtom(options){
   // 默认值
   let defaultLoadable = isPromise(options.default) ? xxxx : options.default
   
   function getAtom(store,state){
       if(state.atomValues.has(key)){
           // 如果当前state里有这个key的值,直接返回。
           return state.atomValues.get(key)
       }else if(state.novalidtedAtoms.has(key)){
          //.. 一些逻辑
       }else{
           return defaultLoadable;
       }
   }
   
   function setAtom(store, state, newValue){
      if (state.atomValues.has(key)) {
          const existing = nullthrows(state.atomValues.get(key));
          if (existing.state === 'hasValue' && newValue === existing.contents) {
              // 如果相等就返回空map
            return new Map();
          }
        }
      //...  
      // 返回的的是key --> 新的loadableValue的Map
      return new Map().set(key, loadableWithValue(newValue));      
   }
   
   function invalidateAtom(){
       //...
   }
   
   
   
  const node = registerNode(
    ({
      key,
      nodeType: 'atom',
      get: getAtom,
      set: setAtom,
      init: initAtom,
      invalidate: invalidateAtom,
      // 忽略其他配置。。。
    }),
  );
  return node; 
}

function registerNode(){
  if (nodes.has(node.key)) {
    //...
  }
  nodes.set(node.key, node);

  const recoilValue =
    node.set == null
      ? new RecoilValueClasses.RecoilValueReadOnly(node.key)
      : new RecoilValueClasses.RecoilState(node.key);

  recoilValues.set(node.key, recoilValue);
  return recoilValue;
}

selector

Поскольку селектор также может передавать элемент конфигурации set, здесь он анализироваться не будет.

function selector(options){
    const {key, get} = options
    const deps = new Set();
    function selectorGet(){
       // 检测是否有循环依赖
       return detectCircularDependencies(() =>
          getSelectorValAndUpdatedDeps(store, state),
      );
    }
    
    function getSelectorValAndUpdatedDeps(){
        const cachedVal = getValFromCacheAndUpdatedDownstreamDeps(store, state);
        if (cachedVal != null) {
          setExecutionInfo(cachedVal, store);
          // 如果有缓存值直接返回
          return cachedVal;
        }
        // 解析getter
         const [loadable, newDepValues] = evaluateSelectorGetter(
          store,
          state,
          newExecutionId,
        );
        // 缓存结果
        maybeSetCacheWithLoadable(
          state,
          depValuesToDepRoute(newDepValues),
          loadable,
        );
        //...
        return lodable
    }
   
    function evaluateSelectorGetter(){
        function getRecoilValue(recoilValue){
               const { key: depKey } = recoilValue
               dpes.add(key);
               // 存入graph
               setDepsInStore(store, state, deps, executionId);
               const depLoadable = getCachedNodeLoadable(store, state, depKey);
               if (depLoadable.state === 'hasValue') {
                    return depLoadable.contents;
              }
                throw depLoadable.contents;
        }
        const result = get({get: getRecoilValue});
        const lodable = getLodable(result);
        //...

        return [loadable, depValues];
    }

    return registerNode<T>({
          key,
          nodeType: 'selector',
          peek: selectorPeek,
          get: selectorGet,
          init: selectorInit,
          invalidate: invalidateSelector,
          //...
        });
      }
}

hooks

useRecoilValue && useRecoilValueLoadable

  • Нижний слой useRecoilValue на самом деле зависит от useRecoilValueLoadable.Если возвращаемое значение useRecoilValueLoadable является обещанием, выбросьте его.

  • useRecoilValueLoadable сначала подписывается на изменения RecoilValue в useEffect. Если обнаружится, что изменения отличаются, вызовите forceupdate для повторного рендеринга. Возвращаемое значение получается путем вызова метода get узла, чтобы получить значение типа lodable и вернуть его.

function useRecoilValue<T>(recoilValue: RecoilValue<T>): T {
  const storeRef = useStoreRef();
  const loadable = useRecoilValueLoadable(recoilValue);
  // 如果是promise就是throw出去。
  return handleLoadable(loadable, recoilValue, storeRef);
}

function useRecoilValueLoadable_LEGACY(recoilValue){
    const storeRef = useStoreRef();
    const [_, forceUpdate] = useState([]);
    
    const componentName = useComponentName();
    
    useEffect(() => {
        const store = storeRef.current;
        const storeState = store.getState();
        // 实际上就是在storeState.nodeToComponentSubscriptions里面建立 node --> 订阅函数的映射
        const subscription = subscribeToRecoilValue(
          store,
          recoilValue,
          _state => {
            // 在代码里通过gkx开启一些特性,方便单元测试和代码迭代。
            if (!gkx('recoil_suppress_rerender_in_callback')) {
              return forceUpdate([]);
            }
            const newLoadable = getRecoilValueAsLoadable(
              store,
              recoilValue,
              store.getState().currentTree,
            );
            // 小小的优化
            if (!prevLoadableRef.current?.is(newLoadable)) {
              forceUpdate(newLoadable);
            }
            prevLoadableRef.current = newLoadable;
          },
          componentName,
        );  
        //...
        // release
         return subscription.release;    
    })
    
    // 实际上就是调用node.get方法。然后做一些其他处理
    const loadable = getRecoilValueAsLoadable(storeRef.current, recoilValue);

    const prevLoadableRef = useRef(loadable);
    useEffect(() => {
        prevLoadableRef.current = loadable;
    });
    return loadable;
}

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

function useComponentName(): string {
  const nameRef = useRef();
  if (__DEV__) {
      if (nameRef.current === undefined) {
        const frames = stackTraceParser(new Error().stack);
        for (const {methodName} of frames) {
          if (!methodName.match(/\buse[^\b]+$/)) {
            return (nameRef.current = methodName);
          }
        }
        nameRef.current = null;
      }
      return nameRef.current ?? '<unable to determine component name>';
  }
  return '<component name not available>'; 
}

useRecoilValueLoadable_MUTABLESOURCE в основном такой же, за исключением того, что в функции подписки мы изменили вызов focusupdate вручную на вызов обратного вызова параметра.

function useRecoilValueLoadable_MUTABLESOURCE(){
    //...
    
    const getLoadable = useCallback(() => {
        const store = storeRef.current;
        const storeState = store.getState();
        //...
        const treeState = storeState.currentTree;
        return getRecoilValueAsLoadable(store, recoilValue, treeState);
    }, [storeRef, recoilValue]);
  
    const subscribe = useCallback(
    (_storeState, callback) => {
      const store = storeRef.current;
      const subscription = subscribeToRecoilValue(
        store,
        recoilValue,
        () => {
          if (!gkx('recoil_suppress_rerender_in_callback')) {
            return callback();
          }
          const newLoadable = getLoadable();
          if (!prevLoadableRef.current.is(newLoadable)) {
            callback();
          }
          prevLoadableRef.current = newLoadable;
        },
        componentName,
      );
      return subscription.release;
    },
    [storeRef, recoilValue, componentName, getLoadable],
  );
    const source = useRecoilMutableSource();
    const loadable = useMutableSource(source, getLoadableWithTesting, subscribe);
    const prevLoadableRef = useRef(loadable);
    useEffect(() => {
        prevLoadableRef.current = loadable;
    });
    return loadable;
}

useSetRecoilState & setRecoilValue

В итоге useSetRecoilState фактически вызывает queueOrPerformStateUpdate, помещает обновление в очередь обновлений и ждет возможности вызвать

function useSetRecoilState(recoilState){
  const storeRef = useStoreRef();
  return useCallback(
    (newValueOrUpdater) => {
      setRecoilValue(storeRef.current, recoilState, newValueOrUpdater);
    },
    [storeRef, recoilState],
  );
}

function setRecoilValue<T>(
  store,
  recoilValue,
  valueOrUpdater,
) {
  queueOrPerformStateUpdate(store, {
    type: 'set',
    recoilValue,
    valueOrUpdater,
  });
}

queueOrPerformStateUpdate, последующие операции здесь усложняются, чтобы упростить их на три шага, как показано ниже;

function queueOrPerformStateUpdate(){
    //...
    //atomValues中设置值
    state.atomValues.set(key, loadable);
    // dirtyAtoms 中添加key。
    state.dirtyAtoms.add(key);
    //通过storeRef拿到。 
    notifyBatcherOfChange.current()
}

Batcher

recoil внутренне реализует механизм пакетного обновления.

function Batcher({
  setNotifyBatcherOfChange,
}: {
  setNotifyBatcherOfChange: (() => void) => void,
}) {
  const storeRef = useStoreRef();

  const [_, setState] = useState([]);
  setNotifyBatcherOfChange(() => setState({}));

  useEffect(() => {
      endBatch(storeRef);
  });

  return null;
}


function endBatch(storeRef) {
    const storeState = storeRef.current.getState();    
    const {nextTree} = storeState;
    if (nextTree === null) {
      return;
    }
    // 树交换
    storeState.previousTree = storeState.currentTree;
    storeState.currentTree = nextTree;
    storeState.nextTree = null;
    
    sendEndOfBatchNotifications(storeRef.current);
}

function sendEndOfBatchNotifications(store: Store) {
  const storeState = store.getState();
  const treeState = storeState.currentTree;
  const dirtyAtoms = treeState.dirtyAtoms;
 // 拿到所有下游的节点。
 const dependentNodes = getDownstreamNodes(
    store,
    treeState,
    treeState.dirtyAtoms,
  );
  for (const key of dependentNodes) {
      const comps = storeState.nodeToComponentSubscriptions.get(key);

      if (comps) {
        for (const [_subID, [_debugName, callback]] of comps) {
          callback(treeState);
        }
      }
    }
  }
  //...
}

Суммировать

Хотя существует множество библиотек управления состоянием для React, некоторые идеи отдачи все еще очень продвинуты, и сообщество уделило много внимания этому новому колесу.В настоящее время githubstar14k. Поскольку recoil еще не является стабильной версией, объем загрузки npm невелик, и не всем рекомендуется использовать его в производственной среде. Однако я верю, что с выходом react18 recoil тоже будет обновлен до стабильной версии, и его использование будет все больше и больше, вы сможете попробовать его, когда придет время.

Справочная документация

Recoil

Интенсивное чтение «отдачи»

Анализ использования и принципа отдачи

Сравнение и принцип реализации управления состоянием React разных школ