Полное руководство для начинающих по React Hooks

Node.js React.js
Полное руководство для начинающих по React Hooks

автор:RichLab Яньлян

Примечание редактора: Это глубокая статья о React Hooks. На прочтение и понимание требуется определенное время. Можно сначала собрать, а потом медленно читать. Больше захватывающих взаимодействийЗнай почти.

1 Why Hook?

1.1 Начиная с теории проектирования компонентов React

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

  • React считает, что представления пользовательского интерфейса — это визуальное отображение данных, т.е.UI = F(DATA),здесьFнужно быть ответственнымОбрабатывать входные данные и реагировать на изменения в данных
  • в формулеFАбстрагированный на компоненты в React, ReactОркестрация приложений с детализацией на основе компонентовДа, компоненты — это наименьшая единица повторного использования кода.
  • По дизайну React принимаетpropsсвойства для получения внешних данных, используйтеstateСвойства для управления данными (состоянием), генерируемыми самим компонентом, и для реализации (во время выполнения) реакции на изменения данных, ReactПринять дизайн компонентов на основе классов (Class)!
  • Кроме того, React считает, чтоКомпоненты имеют жизненный циклТаким образом, концепция жизненного цикла впервые используется в проектировании компонентов, предоставляя разработчикам ряд API-интерфейсов, которые они могут использовать от создания компонента до его уничтожения.

Это теоретическая основа проектирования компонентов React.Компоненты React, с которыми мы наиболее знакомы, обычно выглядят так:

// React基于Class设计组件
class MyConponent extends React.Component {
  // 组件自身产生的数据
  state = {
    counts: 0
  }

  // 响应数据变更
  clickHandle = () => {
    this.setState({ counts: this.state.counts++ });
    if (this.props.onClick) this.props.onClick();
  }

  // lifecycle API
  componentWillUnmount() {
    console.log('Will mouned!');
  }

    // lifecycle API
  componentDidMount() {
    console.log('Did mouned!');
  }

  // 接收外来数据(或加工处理),并编排数据在视觉上的呈现
  render(props) {
    return (
      <>
        <div>Input content: {props.content}, btn click counts: {this.state.counts}</div>
        <button onClick={this.clickHandle}>Add</button>
      </>
    );
  }
}

1.2 Проблемы с компонентом класса

1.2.1 Дилемма повторного использования компонентов

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

  • Для обмена данными между компонентами React официально использует односторонний поток данных (Flux) для решения проблемы.
  • Для повторного использования (с сохранением состояния) компонентов команда React предложила множество решений.В первые дни использовался CreateClass + Mixins, и он был разработан после замены CreateClass на Class Component.Render Propsа такжеHigher Order Component, вплоть до более позднего дизайна Function Component + Hooks, исследование повторного использования компонентов командой React не прекращалось.

Проблемы с использованием HOC (клише):

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

Рендер реквизит:

  • Поток данных стал более интуитивным, и дочерние компоненты могут четко видеть источник данных.
  • Но по сути Render Props реализованы на основе замыканий, и повторное использование большого количества компонентов неизбежно приведет к проблеме callback hell.
  • Контекст компонента теряется, поэтому нетthis.propsСвойства, недоступные, как HOCthis.props.children

1.2.2 Дефекты класса Javascript

1,thisуказывая на (дефект языка)

class People extends Component {
  state = {
    name: 'dm',
    age: 18,
  }

  handleClick(e) {
    // 报错!
    console.log(this.state);
  }

  render() {
    const { name, age } = this.state;
    return (<div onClick={this.handleClick}>My name is {name}, i am {age} years old.</div>);
  }
}

createClass не должен иметь дело с указанием на это.Когда компонент класса немного непреднамерен, причина появится.thisуказывая на ошибку.

2. Проблемы с размером компиляции (и производительностью):

// Class Component
class App extends Component {
  state = {
    count: 0
  }

  componentDidMount() {
    console.log('Did mount!');
  }

  increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  decreaseCount = () => {
    this.setState({ count: this.state.count - 1 });
  }

  render() {
    return (
      <>
        <h1>Counter</h1>
        <div>Current count: {this.state.count}</div>
        <p>
          <button onClick={this.increaseCount}>Increase</button>
          <button onClick={this.decreaseCount}>Decrease</button>
        </p>
      </>
    );
  }
}

// Function Component
function App() {
  const [ count, setCount ] = useState(0);
  const increaseCount = () => setCount(count + 1);
  const decreaseCount = () => setCount(count - 1);

  useEffect(() => {
    console.log('Did mount!');
  }, []);

  return (
    <>
      <h1>Counter</h1>
      <div>Current count: {count}</div>
      <p>
        <button onClick={increaseCount}>Increase</button>
        <button onClick={decreaseCount}>Decrease</button>
      </p>
    </>
  );
}

Результат компиляции компонента класса (Webpack):

var App_App = function (_Component) {
  Object(inherits["a"])(App, _Component);

  function App() {
    var _getPrototypeOf2;
    var _this;
    Object(classCallCheck["a"])(this, App);
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    _this = Object(possibleConstructorReturn["a"])(this, (_getPrototypeOf2 = Object(getPrototypeOf["a"])(App)).call.apply(_getPrototypeOf2, [this].concat(args)));
    _this.state = {
      count: 0
    };
    _this.increaseCount = function () {
      _this.setState({
        count: _this.state.count + 1
      });
    };
    _this.decreaseCount = function () {
      _this.setState({
        count: _this.state.count - 1
      });
    };
    return _this;
  }
  Object(createClass["a"])(App, [{
    key: "componentDidMount",
    value: function componentDidMount() {
      console.log('Did mount!');
    }
  }, {
    key: "render",
    value: function render() {
      return react_default.a.createElement(/*...*/);
    }
  }]);
  return App;
}(react["Component"]);

Результат компиляции функционального компонента (Webpack):

function App() {
  var _useState = Object(react["useState"])(0),
    _useState2 = Object(slicedToArray["a" /* default */ ])(_useState, 2),
    count = _useState2[0],
    setCount = _useState2[1];
  var increaseCount = function increaseCount() {
    return setCount(count + 1);
  };
  var decreaseCount = function decreaseCount() {
    return setCount(count - 1);
  };
  Object(react["useEffect"])(function () {
    console.log('Did mount!');
  }, []);
  return react_default.a.createElement();
}
  • Сам класс, реализованный с помощью Javascript, относительно безвкусен, отсутствует концепция множественного наследования, как в Java/C++, а логическое повторное использование классов представляет собой проблему.
  • Компонент класса используется как Javascript внутри ReactFunction类иметь дело с
  • После компиляции функционального компонента это обычная функция, дружественная к движку js.
🤔 Вопрос: Как React распознает чистые функциональные и классовые компоненты?

1.3 Отсутствующие функции функционального компонента

Не все компоненты должны иметь дело с жизненным циклом.Функциональный компонент был разработан в начале выпуска React, чтобы упростить написание компонента класса, когда есть только рендеринг.

  • Функциональный компонент — это чистая функция, которая способствует повторному использованию и тестированию компонентов.
  • Проблема с функциональным компонентом заключается в том, что он просто получает свойства, связывает события и возвращает jsx, который сам по себе являетсяКомпоненты без состояния полагаются на дескриптор, переданный реквизитами, для реагирования на изменения данных (состояния), поэтому функциональный компонент не может существовать без комментария класса!
function Child(props) {
  const handleClick = () => {
    this.props.setCounts(this.props.counts);
  };

  // UI的变更只能通过Parent Component更新props来做到!!
  return (
    <>
      <div>{this.props.counts}</div>
      <button onClick={handleClick}>increase counts</button>
    </>
  );
}

class Parent extends Component() {
  // 状态管理还是得依赖Class Component
  counts = 0

  render () {
    const counts = this.state.counts;
    return (
      <>
        <div>sth...</div>
        <Child counts={counts} setCounts={(x) => this.setState({counts: counts++})} />
      </>
    );
  }
}

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

Появление хуков компенсирует отсутствие функционального компонента по сравнению с классовым компонентом, позволяя функциональному компоненту заменить классовый компонент.

1.4 Комбинация функциональных компонентов и хуков

1. Функции, которые относительно независимы и не имеют ничего общего с рендерингом, могут быть напрямую извлечены для реализации хука, например, библиотека запросов, состояние входа в систему, основное тело пользователя, скрытая точка и т. д. Теоретически вместо этого декораторы могут быть реализованы с помощью хука. (Такие какreact-use, предоставляет большое количество реализаций хуков из пользовательского интерфейса, анимации, событий и других общих функций).

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

🤔: Пожалуйста, решите, как использовать компонент класса для достижения этой цели.
function useWinSize() {
  const html = document.documentElement;
  const [ size, setSize ] = useState({ width: html.clientWidth, height: html.clientHeight });

  useEffect(() => {
    const onSize = e => {
      setSize({ width: html.clientWidth, height: html.clientHeight });
    };

    window.addEventListener('resize', onSize);

    return () => {
      window.removeEventListener('resize', onSize);
    };
  }, [ html ]);

  return size;
}

// 依赖win宽度,适配图片布局
function Article(props) {
  const { width } = useWinSize();
  const cls = `layout-${width >= 540 ? 'muti' : 'single'}`;

  return (
    <>
      <article>{props.content}<article>
      <div className={cls}>recommended thumb list</div>
    </>
  );
}

// 弹层宽度根据win宽高做适配
function Popup(props) {
  const { width, height } = useWinSize();
  const style = {
    width: width - 200,
    height: height - 300,
  };
  return (<div style={style}>{props.content}</div>);
}

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

кейс: проверка формы

function App() {
  const { waiting, errText, name, onChange } = useName();
  const handleSubmit = e => {
    console.log(`current name: ${name}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <>
        Name: <input onChange={onChange} />
        <span>{waiting ? "waiting..." : errText || ""}</span>
      </>
      <p>
        <button>submit</button>
      </p>
    </form>
  );
}

2 Реализация и использование хуков

2.1 useState

useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>]

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

Мысль 🤔: почему useState возвращает не объект, а кортеж?
Объявление функции useState
  • Функциональный компонент, использующий Hooks API, возвращаемый сеттер может изменить состояние компонента и вызвать повторную визуализацию компонента.
  • В отличие от хуков в общем смысле, хуки здесь могут вызываться многократно и производить разные эффекты, а хуки рождаются и убиваются вместе с Fiber Node.

2.1.1 Почему в функциональном компоненте можно вызывать только Hooks API?

Реализация Hooks API по умолчанию:

function throwInvalidHookError() {
  invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.');
}

var ContextOnlyDispatcher = {
    ...
  useEffect: throwInvalidHookError,
  useState: throwInvalidHookError,
  ...
};

При вызове хука в функциональном компоненте:

function renderWithHooks(current, workInProgress, Component, props, refOrContext, nextRenderExpirationTime) {
  currentlyRenderingFiber$1 = workInProgress; // 指针指向当前正在render的fiber节点
  ....
  if (nextCurrentHook !== null) {
    // 数据更新
    ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
  } else {
    // 首次render
    ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
  }
}

/// hook api的实现
HooksDispatcherOnMountInDEV = {
    ...
  useState: function (initialState) {
    currentHookNameInDev = 'useState';
    ...
    return mountState(initialState);
  },
};

2.1.2 Почему я должен вызывать Hooks API в верхней области функционального компонента?

В компонентах класса состояние — это объект, соответствующий узлу FiberNode.memoizedStateсвойство при вызове в компоненте классаsetState()обновлять время от времениmemoizedStateВот и все. Но в функциональном компонентеmemoizedStateОн выполнен в виде связанного списка (объект Hook):

// Hook类型定义
type Hook = {
  memoizedState: any, // 存储最新的state
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null, // 更新队列
  next: Hook | null, // 下一个hook
}

// 定义一次更新
type Update<S, A> = {
  ...
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null, // 待更新状态值
  next: Update<S, A> | null,
  ...
};

// 待更新队列定义
type UpdateQueue<S, A> = {
  last: Update<S, A> | null, // 最后一次更新操作
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null, // 最新处理处理state的reducer
  lastRenderedState: S | null, // 最新渲染后状态
};

Пример:

function App() {
  const [ n1, setN1 ] = useState(1);
  const [ n2, setN2 ] = useState(2);

  // if (sth) {
  //    const [ n4, setN4 ] = useState(4);
  // } else {
  //    const [ n5, setN5 ] = useState(5);
  // }

  const [ n3, setN3 ] = useState(3);
}

Структура хранилища хуков (связный список):

Структура связанного списка Fiber (Hook)
  • Вызов Hook API сгенерирует соответствующий экземпляр Hook (и добавит его в цепочку Hooks), но компоненту будут возвращены состояние и соответствующий сеттер.При повторном рендеринге фреймворк не знает, какому экземпляру Hooks соответствует сеттер (если для его хранения не используется HashMap) хуки, но это требует передачи соответствующего ключа в React при вызове, что увеличит сложность использования хуков).
  • Re-render повторно выполнит весь компонент из первой строки кода., то есть вся цепочка Hooks будет выполняться по порядку, если sth не будет удовлетворена при повторном рендере, она будет выполненаuseState(5)Branch, напротив, useState(4) не будет выполняться, что приведет кuseState(5)Возвращаемое значение на самом деле равно 4, потому чтоПосле первого рендеринга memoizedState, соответствующий хуку, может быть изменен только с помощью отправки, возвращаемой useState., поэтому необходимоСохраняйте порядок хуков без изменений, поэтому хуки не могут быть вызваны в ветке, только вызов верхнего уровня может гарантировать порядок выполнения каждого хука!

2.1.3 Как хук useState обновляет данные?

useState() этап монтирования (часть) реализации исходного кода:

// useState() 首次render时执行mountState
function mountState(initialState) {
  // 从当前Fiber生成一个新的hook对象,将此hook挂载到Fiber的hook链尾,并返回这个hook
  var hook = mountWorkInProgressHook();

  hook.memoizedState = hook.baseState = initialState;

  var queue = hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: (state, action) => isFn(state) ? action(state) : action,
    lastRenderedState: initialState
  };
  // currentlyRenderingFiber$1保存当前正在渲染的Fiber节点
  // 将返回的dispatch和调用hook的节点建立起了连接,同时在dispatch里边可以访问queue对象
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

//// 功能相当于setState!
function dispatchAction(fiber, queue, action) {
  ...
  var update = {
    action, // 接受普通值,也可以是函数
    next: null,
  };
  var last = queue.last;

  if (last === null) {
    update.next = update;
  } else {
    last.next = update;
  }

  // 略去计算update的state过程
  queue.last = update;
  ...
  // 触发React的更新调度,scheduleWork是schedule阶段的起点
  scheduleWork(fiber, expirationTime);
}
  • dispatchActionФункция является ключом к обновлению состояния, она генерируетupdateСмонтируйте его в очередь Hooks и отправьте график обновления React, а последующая работа согласуется с компонентом класса.
  • Теоретически, диспетчеризацию можно вызывать несколько раз одновременно, но только последняя из них вступит в силу (последний указатель очереди указывает на состояние последнего обновления).
  • УведомлениеuseStateобновить данные иsetStateОтличие в том, что прежнее сольется со старым состоянием, нам нужно только передать измененную часть, ноuseStateЭто прямо прикрыто!
Фаза расписания находится между фазами согласования и фиксации, а метод начальной точки расписания — scheduleWork. ReactDOM.render, setState, forceUpdate, dispatchAction хуков React проходят через scheduleWork. Ссылка:zhuanlan.zhihu.com/p/54042084

Фаза обновления (изменения состояния, повторный рендеринг родительского компонента и т. д. приведет к обновлению состояния компонента) useState() обновляет состояние:

function updateState(initialState) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  var newState;
  var update;

  if (numberOfReRenders > 0) {
    // 组件自己re-render
    newState = hook.memoizedState;
    // renderPhaseUpdates是一个全局变量,是一个的HashMap结构:HashMap<(Queue: Update)>
    update = renderPhaseUpdates.get(queue);
  } else {
    // update
    newState = hook.baseState;
    update = hook.baseUpdate || queue.last;
  }

  do {
    newState = update.action; // action可能是函数,这里略去了细节
    update = update.next;
  } while(update !== null)

  hook.memoizedState = newState;
  return [hook.memoizedState, queue.dispatch];
}
  • React выполнит всю очередь обновлений для объекта ловушки по очереди, чтобы получить последнее состояние, поэтому tuple[0], возвращаемый функцией useState(), всегда будет последним состоянием!
  • Как видите, на этапе обновления initialState вообще не используется!

2.1.4 Процесс обновления хука useState

function App() {
  const [n1, setN1] = useState(1);
  const [n2, setN2] = useState(2);
  const [n3, setN3] = useState(3);

  useEffect(() => {
    setN1(10);
    setN1(100);
  }, []);

  return (<button onClick={() => setN2(20)}>click</button>);
}

Процесс обновления диаграммы:

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

2.2 useEffect

useEffect(effect: React.EffectCallback, deps?: ReadonlyArray<any> | undefined)

Роль: обработка побочных эффектов в функциональных компонентах, таких как асинхронные операции, отложенные операции и т. д., может заменить компонент класса.componentDidMount,componentDidUpdate,componentWillUnmountи другой жизненный цикл.

2.2.1 Анализ реализации useEffect

HooksDispatcherOnMountInDEV = {
    useEffect: function() {
    currentHookNameInDev = 'useEffect';
    ...
    return mountEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);
  },
};

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  return hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag,
    create: create, // 存储useEffect传入的callback
    destroy: destroy, // 存储useEffect传入的callback的返回函数,用于effect清理
    deps: deps,
    next: null
  };
  .....
  componentUpdateQueue = createFunctionComponentUpdateQueue();
  componentUpdateQueue.lastEffect = effect.next = effect;
  ....
  return effect;
}

function renderWithHooks() {
    ....
  currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
  ....
}
  • В отличие от useState, который передается в определенном состоянии, useEffect передает функцию обратного вызова.Самое большое отличие от useState — время выполнения.После того, как компонент визуализируется как настоящий DOMвыполнить (поэтому может использоваться для манипулирования DOM)
  • Вызов useEffect также добавит хук в цепочку хуков текущего узла Fiber и вернет его, а его memoizedState сохранит хукeffectобъект,effectВ конечном итоге объект будет подключен к узлу Fiber.updateQueueОчередь (когда узлы Fiber отобразятся на странице, начнется выполнение узла FiberupdateQueueфункция хранится в)

2.2.2 Параметр deps очень важен

Следующий код очень распространен, 🤔 В чем проблема? бегатьdemo

// 用Hook写
function App() {
  const [data, setData] = useState('');

  useEffect(() => {
    setTimeout(() => {
      setData(`current data: ${Date.now()}`);
    }, 3000);
  });

  return <div>{data}</div>;
}
// 等价代码
class App extends Component {
  state = {data = ''}

  componentDidMount() {
    setTimeout(() => {
      this.setState({ data: `current data: ${Date.now()}` });
    }, 3000);
  }

  render() {
    return <div>{this.state.data}</div>;
  }
}
  • При повторном рендеринге компонента функциональный компонент повторно выполняет всю функцию, включая все «зарегистрированные» хуки.По умолчанию обратный вызов useEffect также будет выполняться повторно!
  • useEffect может принимать второй параметрdeps, который используется для определения необходимости повторного выполнения обратного вызова при повторном рендеринге, поэтому deps должны передаваться в соответствии с фактическими зависимостями, а не меньше или больше!
  • Элементы массива deps должны быть изменяемыми, например, useRef, dispatch и т. д. не могут и не должны передаваться в
  • Сравнение deps на самом деле поверхностное сравнение (см.исходный код), бессмысленно передавать объекты и функции
  • В качестве наилучшей практики, пожалуйста, передайте как можно больше отложений при использовании useEffect (я не нашел сцены без отложений)

2.2.3 Устранение побочных эффектов

Хук Принимает использование useeffect для возврата функции, которая будет выполнять эту функцию на этапе очистки волокна, тем самым достигая эффекта очистки Эффект:

function App() {
  useEffect(() => {
    const timer = setTimeout(() => {
        console.log('print log after 1s!');
    }, 1000);
    window.addEventListener('load', loadHandle);

    return () => window.removeEventListener('load', loadHandle); // 执行清理
  }, []);
}

// 同等实现
class App extends Component {
  componentDidMount() {
    const timer = setTimeout(() => {
        console.log('print log after 1s!');
    }, 1000);
    window.addEventListener('load', loadHandle);
  }

  componentDidUnmount() {
    window.removeEventListener('load', loadHandle);
  }
}

2.3 useContext

Для совместного использования состояния между компонентами API-интерфейсы, связанные с контекстом, официально предоставляются в компонентах класса:

  • использоватьReact.createContextAPI создает контекст, и, поскольку он поддерживает вызов вне компонента, он может обеспечить совместное использование состояния.
  • использоватьContext.ProviderСтатус монтирования API в компонентах верхнего уровня
  • использоватьContext.ConsumerAPI предоставляет состояние для определенных компонентов или черезcontextTypeАтрибут указывает ссылку компонента на контекст

Должен использоваться при использовании состояния, предоставленного контекстомcontextTypeАтрибуты определяют контекстные ссылки или используют<Context.Consumer>компоненты обертки, которые неудобны в использовании (см.Официальный пример React Context).

Команда React предоставляет функциональные компонентыuseContextHook API, который используется для получения состояния, хранящегося в Context внутри функционального компонента:

useContext<T>(Context: ReactContext<T>, unstable_observedBits: void | number | boolean): T

Реализация useContext относительно проста, достаточно прочитать значение _currentValue, смонтированное в объекте контекста, и вернуть:

function useContext(content, observedBits) {
  // 处理observedBits,暂时
  // 只有在React Native里边isPrimaryRenderer才会是false
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
Чтобы понять реализацию useContext, вы должны сначала понять реализацию контекста в исходном коде.Серия исходных кодов React | Подробное объяснение React Context

useContext значительно упрощает процесс потребления контекста и предоставляет возможность совместного использования состояния между компонентами.На самом деле, большая часть текущих решений сообщества по управлению состоянием на основе хуков реализована на основе useContext (другой — useState). Схема управления состоянием будет представлена ​​в следующей статье.

2.4 useReducer

useReducer<S, I, A>(reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>]

Роль: используется для управления сложными структурами данных (useStateОбычно он используется для управления состоянием плоских структур), который в основном реализует основные функции редукции. На самом деле, хук useReducer можно легко реализовать на основе Hooks Api:

const useReducer = (reducer, initialArg, init) => {
  const [state, setState] = useState(
    init ? () => init(initialArg) : initialArg,
  );
  const dispatch = useCallback(
    action => setState(prev => reducer(prev, action)),
    [reducer],
  );
  return useMemo(() => [state, dispatch], [state, dispatch]);
};

Редуктор предоставляет возможность изменять состояние вне компонента, а useReducer возвращаетdispatchОбъект снова "безопасная работа", можно безопасно передавать непосредственно в подкомпоненты, не вызывая повторного рендеринга подкомпонентов.

function reducer(state, action) {
  // 这里能够拿到组件的全部state!!
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + state.step,
      };
    ...
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {count: initialCount, step: 10});

  return (
    <>
      <div>{state.count}</div>
      // redux like diaptch
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <ChildComponent dispatch={dispatch} />
    </>
  );
}

2.5 Оптимизация производительности (мемоизация), связанная с Hooks API

2.5.1 useCallback

useCallback<T>(callback: T, deps: Array<mixed> | void | null): T

Из-за особенностей функций javascript, когда сигнатура функции передается в useEffect как deps, это все равно вызовет повторный рендеринг (даже если тело функции не изменилось), это явление также существует в компонентах класса:

// 当Parent组件re-render时,Child组件也会re-render
class Parent extends Component {
  render() {
    const someFn = () => {}; // re-render时,someFn函数会重新实例化

    return (
      <>
        <Child someFn={someFn} />
        <Other />
      </>
    );
  }
}

class Child extends Component {
  componentShouldUpdate(prevProps, nextProps) {
    return prevProps.someFn !== nextProps.someFn; // 函数比较将永远返回false
  }
}

Функциональный компонент (см.demo):

function App() {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([]);
  const fetchData = async () => {
    setTimeout(() => {
      setList(initList);
    }, 3000);
  };

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return (
    <>
      <div>click {count} times</div>
      <button onClick={() => setCount(count + 1)}>Add count</button>
      <List list={list} />
    </>
  );
}

решение:

  • Вынести функцию за пределы компонента (недостаток в том, что нельзя прочитать состояние компонента)
  • Если возможно, переместите тело функции вuseEffectвнутренний
  • Если вызов функции больше, чемuseEffectВнутри (если необходимо перейти к дочерним компонентам) вы можете использоватьuseCallbackфункции-оболочки API,useCallbackСуть в том, чтобы выполнить анализ зависимостей функции и повторно выполнить ее при изменении зависимости.

2.5.2 useMemo & memo

useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T

useMemo используется для кэширования некоторых трудоемких результатов расчета и повторного выполнения расчета только при изменении зависимых параметров:

function App(props) {
  const start = props.start;
  const list = props.list;
  const fibValue = useMemo(() => fibonacci(start), [start]); // 缓存耗时操作
  const MemoList = useMemo(() => <List list={list} />, [list]);

  return (
    <>
      <div>Do some expensive calculation: {fibValue}</div>
      {MemoList}
      <Other />
    </>
  );
}
Простое понимание:useCallback(fn, deps) === useMemo(() => fn, deps)

В функциональных компонентах React предоставляет компонент и класс, которыйPureComponentAPI для той же функциональностиReact.memo, когда он перерисовывает себя, для каждогоpropsЭлементы сравниваются неглубоко, и повторный рендеринг не запускается, если ссылка не изменилась.

// 只有列表项改变时组件才会re-render
const MemoList = React.memo(({ list }) => {
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>{item.content}</li>
      ))}
    </ul>
  );
});

в сравнении сReact.memo,useMemoВызываемый внутри компонента, вы можете получить доступ к свойствам и состоянию компонента, поэтому он имеет более детальный контроль над зависимостями.

2.6 useRef

Собственно об использованииRefофициальная документацияБыло сказано очень подробно, используйтеRef HookВозвращает изменяемую ссылку на объект ref, но useRef более универсален, чем ref, он может хранить произвольные значения javascript, а не только ссылки на DOM.

Реализация useRef относительно проста:

// mount阶段
function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var ref = { current: initialValue };
  {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

// update阶段
function updateRef(initialValue) {
  var hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

useRef особенный:

  • useRef — единственный из всех API-интерфейсов Hooks, который возвращает изменяемые данные.
  • Единственный способ изменить значение useRef — изменить его текущее значение, и изменение значения не приведет к повторному рендерингу.
  • UseRef возвращает фиксированное значение каждый раз при рендеринге компонента без следующегоCapture Valuesхарактеристика

2.7 API других хуков

  • useLayoutEffect: используется так же, как и useEffect, отличие от useEffect заключается во времени выполнения.useLayoutEffect выполняется до того, как браузер отрисует узел(то же время, что и для componentDidMount и componentDidUpdate)
  • useDebugValue: используется для отладки инструментов разработчика.
  • useImperativeHandle: используется с forwardRef для настройки значения, предоставляемого родительскому компоненту через ref.

2.8 Функция захвата значений

1,useStateСо значениями захвата см.demo

2,useEffectсо значениями захвата

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
    // 连续点击三次button,页面的title将依次改为1、2、3,而不是3、3、3
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

3. Дескриптор события имеет значения захвата, см.demo

4. . . Все Hooks API имеют функцию захвата значений, кромеuseRef,Проверитьdemo(setTimeout всегда может получить последнее значение состояния), состояние неизменно, а ссылка может изменяться.

function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var ref = { current: initialValue }; // ref就是一个普通object的引用,没有闭包
  {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

API-интерфейсы хуков, не связанные с useRef, по сути образуют замыкания, а замыкания имеют свои собственные независимые состояния, что является сутью Capture Values..

2.9 Пользовательские компоненты: смоделируйте некоторые распространенные жизненные циклы

  • componentDidMount: когда deps пуст, обратный вызов больше не выполняется во время повторного рендеринга.
// mount结束,已经更新到DOM
const onMount = function useDidMount(effect) => {
    useEffect(effect, []);
};
  • componentDidUpdate
// layout结束,render DOM之前(会block rendering)
const onUpdate = function useUpdate(effect) => {
  useLayoutEffect(effect, []);
};
  • componentWillUnMount
const unMount = function useWillUnMount(effect, deps = []) => {
  useEffect(() => effect, deps);
};
  • shouldComponentUpdate (или React.PureComponent)
// 使用React.memo包裹组件
const MyComponent = React.memo(() => {
  return <Child prop={prop} />
}, [prop]);

// or
function A({ a, b }) {
  const B = useMemo(() => <B1 a={a} />, [a]);
  const C = useMemo(() => <C1 b={b} />, [b]);
  return (
    <>
      {B}
      {C}
    </>
  );
}

3 проблемы с хуками

1. Хуки могут решить проблему повторного использования функций компонента, но плохо решают проблему повторного использования JSX, например (1.4) случай проверки формы:

function App() {
  const { waiting, errText, name, onChange } = useName();
  // ...

  return (
    <form>
      <div>{name}</div>
      <input onChange={onChange} />
      {waiting && <div>waiting<div>}
      {errText && <div>{errText}<div>}
    </form>
  );
}

Хотя он может инкапсулировать пользовательский ввод, проверку и другую логику в хук useName, часть DOM по-прежнему связана, что не способствует повторному использованию компонентов.Я надеюсь, что команда React найдет эффективное решение.

2. React Hooks размывает (или отказывается) от концепции жизненного цикла, но также обеспечивает более высокий порог обучаемости (например, понимание жизненного цикла Hooks, понимание правил Hooks, оценку зависимостей useEffect и т. д.) по сравнению с грядущие хуки Vue3.0, он имеет более высокий порог для использования.

3. У классов более богатые выразительные возможности (ООП), чем у функций. Использование в React хуков + функциональный компонент (функциональный) на самом деле беспомощный выбор. Представьте себе компонент класса с более чем дюжиной смонтированных методов или свойств. Использование функционального компонента для написания того, как организовать код, чтобы сделать логику ясной? На самом деле за этим стоит компромисс между функциональным программированием и объектно-ориентированным программированием.

4 Ref


Наконец, спасибо, что внимательно прочитали такую ​​длинную статью~

"Команда фронтенда Ant RichLab"Обязуемся делиться с вами высококачественными техническими статьями

Добро пожаловать в нашу колонку, делитесь статьей с друзьями и развивайтесь вместе :-)

Наша команда срочно набирает: интерактивные графические технологии, front-end/full-stack разработка, front-end архитектура, алгоритмы, разработка больших данных и другие направления опциональны, а уровень ожидания P6+~P7, у команды хорошая техническая атмосфера, и есть много возможностей для совершенствования.Вы можете отправить свое резюме прямо мне Hashudai.lyy@alibaba-inc.com