Как использовать и внедрять хуки

React.js

Введение и обзор хуков?

Хуки — это особый тип функций в функциональных компонентах React (обычно начинающийся с «use», например «useState»), позволяющий разработчикам по-прежнему использовать состояние и жизненные циклы в функциональных компонентах, а также повторно использовать бизнес-логику с пользовательскими хуками.

Зачем внедрять хуки и какие проблемы решать

Общие проблемы, с которыми сталкиваются текущие реакции:

  1. Сложно повторно использовать логику (используйте только HOC или реквизиты рендеринга), что приведет к глубокой иерархии дерева компонентов.
  2. Большие компоненты трудно разбивать и рефакторить, а также трудно тестировать.
  3. Компоненты класса сложны для понимания, как методы требуютbind,thisПункт не ясен
  4. Бизнес-логика разбросана по различным методам компонента, что приводит к дублированию логики или связанной с ней логики.

Хуки позволяют нам лучше повторно использовать логику кода. Функциональные компоненты могут использоваться для логического повторного использования, но функциональные компоненты не имеют состояния и могут отображаться только как [чистые компоненты] и не могут обрабатывать локальное состояние. Хуки позволяют функциональным компонентам иметь локальное состояние и могут обрабатывать логику состояния.

Типы крючков

  • State hooks(использовать состояние в функциональном компоненте)
  • Effect hooks(использовать жизненный цикл и побочные эффекты в функциональном компоненте)
  • Custom hooks(Пользовательские хуки используются для повторного использования логики компонентов, решая проблему, описанную в первой мотивации выше)

State Hooks

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Крючки вернут кортеж, структура[value, setValue].

Эти два возвращаемых значения соответствуют предыдущим в реакции

  • this.state
  • this.setState

Мы также можем использовать несколько состояний в функции одновременно.

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

Перед обновлением значения состояния передайтеthis.setState({ fruit: 'orange' }), предыдущее состояние и обновленное состояние будут объединены.

С помощью хуков состояние будет разбито на значения, и после обновления новое значение будет заменено напрямую, а состояние не будет объединено. Структура [state, setState] также делает логику обновления значения более понятной.

Общие хуки, предоставляемые React по умолчанию

  • useState()
  • useContext()
  • useReducer()

useContext()

СотрудничатьReact.createContext({})использование, общее состояние между компонентами

Пример:

const AppContext = React.createContext({});

<AppContext.Provider value={{
  username: 'superawesome'
}}>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>

Затем вы можете использовать AppContext непосредственно в компоненте Navbar.

const Navbar = () => {
  const { username } = useContext(AppContext);
  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  );
}

useReducer()

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

Пример:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

Effect Hooks

EffectHooks позволяют нам использовать методы жизненного цикла внутри функциональных компонентов, где мы можем обновлять DOM, извлекать данные и т. д. с поведением «побочных эффектов». Хук эффекта будет выполняться после каждого рендера компонента, а функция возврата будет выполняться при выгрузке компонента.Если вы хотите, чтобы хук эффекта выполнялся только при первой загрузке компонента, вы можете передать в пустой массив в качестве второго параметра, или вы можете использовать массив Укажите зависимости в , effectHooks будет выполняться только при изменении зависимостей.

import { useState, useEffect } from 'react';

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}

Custom Hooks

Пользовательский хук — это функция javascript, которая начинается с «use» и может вызывать другие хуки для инкапсуляции логики и повторного использования кода. Например:

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

Можно использовать другие функциональные компоненты:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
*********************************
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

Хуки должны использоваться на верхнем уровне функций и не могут использоваться в условиях, циклах или вложенности. Хуки постепенно и полностью заменят компоненты класса и в настоящее время не могут поддерживать функции жизненных циклов getSnapshotBeforeUpdate и componentDidCatch.

Принцип реализации хуков

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

реагирующая инфраструктура

Инфраструктура React разделена на три части: базовый пакет react, react-reconciler, модуль рендеринга рендеринга.

базовый модуль реакции: React базовый API и классы компонентов, компоненты определяют методы render , setState и методы обратного вызова, связанные с жизненным циклом. Связанные API следующие:

const React = {
  Children: {},

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
  unstable_Profiler: REACT_PROFILER_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,
};

модуль рендеринга рендерера: Используйте разные методы рендеринга для разных сред хоста, такие как react-dom, react-webgl, react-native, react-art, полагайтесь на модуль react-reconciler, внедряйте соответствующие методы рендеринга в reconciler, а связанные с ними в react-dom API выглядит следующим образом:

const ReactDOM: Object = {
  createPortal,

  findDOMNode(
    componentOrElement: Element | ?React$Component<any, any>,
  ): null | Element | Text {},

  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {},

  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {},

  unstable_renderSubtreeIntoContainer() {},

  unmountComponentAtNode(container: DOMContainer) {},

  unstable_batchedUpdates: DOMRenderer.batchedUpdates,

  unstable_deferredUpdates: DOMRenderer.deferredUpdates,

  unstable_interactiveUpdates: DOMRenderer.interactiveUpdates,

  flushSync: DOMRenderer.flushSync,

  unstable_flushControlled: DOMRenderer.flushControlled,
}

основной модуль реактивного согласователя: Отвечает за алгоритм планирования и сравнение дерева волокон, подключение базового пакета реакции и модуля рендеринга, внедрение метода setState в экземпляр компонента, выполнение метода рендеринга в компоненте реакции на этапе сравнения, выполнение обратного вызова жизненного цикла в компоненте реакции. на этапе патча и вызывая инъекцию в рендерере. Соответствующий метод рендеринга реальной структуры представления.

Как работает setState

setStateОн определен в React.Component, но пакет React определяет только API и не реализует логику. Похожие такжеcreateContext()и т.д. Большинство функций реализовано в «рендерере». react-dom, react-dom/server, react-native, react-test-renderer, react-art — все это распространенные рендереры. Поэтому, когда мы используем новые функции реакции, необходимо обновить как реакцию, так и реакцию.

setStateОпределить средство обновления в React.Component

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

Средство обновления будет реализовано само по себе в конкретном рендерере:

// React DOM 内部
var classComponentUpdater = {
  isMounted: isMounted,
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst);
    var currentTime = requestCurrentTime();
    var expirationTime = computeExpirationForFiber(currentTime, fiber);

    var update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback$1(callback, 'setState');
      }
      update.callback = callback;
    }

    flushPassiveEffects();
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState: function (inst, payload, callback) {
    //注释了
  },
  enqueueForceUpdate: function (inst, callback) {
    //注释了
  }
};

HooksТот же дизайн также используется с использованием объекта «диспетчер» вместо «обновителя». мы называемuseState()пересылаются текущему диспетчеру. Используются как поле средства обновления, так и объект диспетчера.внедрение зависимостив виде общих принципов программирования. В обоих случаях средство визуализации «внедряет» реализацию таких функций, как setState, в общий пакет React, чтобы сделать компонент более декларативным.

Как работает useState

Как useState позволяет функциональным компонентам без сохранения состояния сохранять состояние, обновлять представления и в чем разница между обновлением this.setState?

В React есть базовый объект ReactElement, который создается с помощью React.createElement().

React.createElement(
  type,
  [props],
  [...children]
)

//举个例子
class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}
ReactDOM.render(
  <Hello toWhat="World" />,
  document.getElementById('root')
);

//完全等价于
class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);
  }
}
ReactDOM.render(
  React.createElement(Hello, {toWhat: 'World'}, null),
  document.getElementById('root')
);

const element = {
    $$typeof: REACT_ELEMENT_TYPE, // 是否是普通Element_Type
    
    // Built-in properties that belong on the element
    type: type, // 我们的组件,比如`class MyComponent`
    key: key,
    ref: ref,
    props: props,
    children: children,
    
    // Record the component responsible for creating this element.
    _owner: owner,
};

Это узел vdom.До React16 React генерировал настоящую структуру DOM на основе этого узла vdom. После React16 была официально введена структура Fiber, и базовая структура реакции стала более сложной. React будет соответствовать узлу vdom как узлу Fiber, структура узла Fiber:

function FiberNode(
    tag: WorkTag,
    pendingProps: mixed,
    key: null | string,
    mode: TypeOfMode,
    ) {
    // Instance
    this.tag = tag;
    this.key = key;
    this.elementType = null; // 就是ReactElement的`$$typeof`
    this.type = null; // 就是ReactElement的type
    this.stateNode = null;
    
    // Fiber
    this.return = null;
    this.child = null;
    this.sibling = null;
    
    this.memoizedState = null;
    this.updateQueue = null;
    
    this.index = 0;
    this.ref = null;
    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.firstContextDependency = null;
    
    // ...others
}

один из нихthis.updateQueueОчередь обновлений, используемая для хранения setState,this.memoizedStateДля хранения состояния в компоненте компонент класса используется для хранения объекта состояния, а хуки используются для хранения объекта хука.

//类组件中更新state的update对象
var update = {
    expirationTime: expirationTime,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null,
    nextEffect: null
};

//函数组件中的Hook对象
{
    baseState,
    next,
    baseUpdate,
    queue,
    memoizedState
};

//类组件中的updateQueue的结构
var queue = {
    baseState: baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
};

//每新增一个update就加入到队列中
function appendUpdateToQueue(queue, update) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

Механизм обновления memoizedState

Обновление хуков делится на два этапа, операция монтирования выполняется во время инициализации, а операция обновления выполняется во время обновления. Объекты HooksDispatcherOnMountInDEV и HooksDispatcherOnUpdateInDEV используются для хранения всех функций обновления хуков.

HooksDispatcherOnMountInDEV = {
    readContext: function (context, observedBits) {
    },
    useCallback: function (callback, deps) {
    },
    useContext: function (context, observedBits) {
    },
    useEffect: function (create, deps) {
    },
    useImperativeHandle: function (ref, create, deps) {
    },
    useLayoutEffect: function (create, deps) {
    },
    useMemo: function (create, deps) {
    },
    useReducer: function (reducer, initialArg, init) {
    },
    useRef: function (initialValue) {
    },
    useState: function (initialState) {
      var hook = mountWorkInProgressHook();
      if (typeof initialState === 'function') {
        initialState = initialState();
      }
      hook.memoizedState = hook.baseState = initialState;
      var queue = hook.queue = {
        last: null,
        dispatch: null,
        eagerReducer: basicStateReducer,
        eagerState: initialState
      };
      var dispatch = queue.dispatch = dispatchAction.bind(null,
      // Flow doesn't know this is non-null, but we do.
      currentlyRenderingFiber$1, queue);
      return [hook.memoizedState, dispatch];
    },
    useDebugValue: function (value, formatterFn) {
    }  
};

//其中的dispatch即为我们调用的‘setState’函数,核心代码为:
function dispatchAction(fiber, queue, action) {
    //注释了*******
    var update = {
        expirationTime: renderExpirationTime,
        action: action,
        eagerReducer: null,
        eagerState: null,
        next: null
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    renderPhaseUpdates.set(queue, update);
}

  HooksDispatcherOnUpdateInDEV = {
    //注释了**********
    useState: function (initialState) {
      currentHookNameInDev = 'useState';
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
      try {
        return updateState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
  };
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer(reducer, initialArg, init) {
    //  注释了**********
    var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        var newState = hook.memoizedState;
        var update = firstRenderPhaseUpdate;
        do {
          // Process this render phase update. We don't have to check the
          // priority because it will always be the same as the current
          // render's.
          var _action = update.action;
          newState = reducer(newState, _action);
          update = update.next;
        } while (update !== null);
}

Действие в объекте обновления заключается в использовании параметра setState. Обновление будет добавлено в очередь обновлений. После того, как все «обновления» будут собраны, будет запущено обновление реакции. При обновлении выполните useState в функциональном компоненте, затем получите объект Hook, выньте объект очереди, обновите его по очереди, получите новое состояние и сохраните его в memoizedState, а затем вернитесь, чтобы обновить представление.

вmemoizedStateиспользуется для записи этогоuseStateдолжен вернуть результат, аnextуказывая на следующий разuseStateСоответствующий объект `Hook.

пример:

function FunctionalComponent () {
const [state1, setState1] = useState(1)
const [state2, setState2] = useState(2)
const [state3, setState3] = useState(3)
}

Порядок выполнения такой:

hook1 => Fiber.memoizedState
state1 === hoo1.memoizedState
hook1.next => hook2
state2 === hook2.memoizedState
hook2.next => hook3
state3 === hook2.memoizedState

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

Имитационные крючки

Разберем характеристики хуков:

  1. Вызовите useState() и верните Tuple([value,setValue])
  2. useState(), который можно использовать только в области верхнего уровня, зависит от порядка, в котором он был создан.
  3. Может использоваться только в функциональных компонентах, а не в компонентах класса.

Реализация может быть смоделирована с использованием структуры массива:

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}

export function useState(initVal) {
  if (firstRun) {
    state.push(initVal);
    setters.push(createSetter(cursor));
    firstRun = false;
  }

  const setter = setters[cursor];
  const value = state[cursor];

  cursor++;
  return [value, setter];
}

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
  const [lastName, setLastName] = useState("Yardley"); // cursor: 1

  return (
    <div>
      <Button onClick={() => setFirstName("Richard")}>Richard</Button>
      <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    </div>
  );
}

function MyComponent() {
  cursor = 0; // resetting the cursor
  return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']

// click the 'Fred' button

console.log(state); // After-click: ['Fred', 'Yardley']