Запечено через React Hook

React.js

Основное блюдо дня

Давайте посмотрим на повседневное использование React Hooks в наши дни. Говоря о крючках, парень с барбекю использует их некоторое время, но он до сих пор не знает, как работают крючки.Когда есть ошибка, вы можете решить ее, только наполовину угадывая и наполовину пытаясь, что, очевидно, не так. возможно. Итак, давайте с сегодняшнего дня потихоньку запекать React Hook, чтобы, когда мы используем Hook для написания бага, мы могли «поразить душу», проанализировать проблему с точки зрения принципа и механизма работы и быстро решить проблему ( конечно, впринципе думается Пройдено, вероятность багов будет относительно меньше).

Статья слишком длинная, рекомендуется посмотреть после сбора

Что происходит за кулисами, когда вызывается React Hook?

Найдите исходный код Hook

Когда мы вызываем useXxx() в программе, что происходит за кулисами? Хотя немного неохотно, но если вы хотите узнать ответ, вы должны посмотреть исходный код.

Первое, что нужно сделать, это, конечно же, загрузить исходный код. Вводим в командной строке:

git clone https://github.com/facebook/react.git

Весь исходный код, который мы сегодня рассмотрим, находится в/packagesв этом каталоге.

Обычно, когда мы внедряем Hook в проект, мы обычно пишем это так:

import React, { useState } from 'react';

Назадfrom 'react'Это напоминает нам, что исходный код хука useState скрыт в/reactв этом каталоге. Следуя подсказкам, я быстро нашел место для экспорта хуков:/packages/react/src/React.js(Нажмите здесь, чтобы увидеть исходный код).

// /packages/react/src/React.js
...
import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  useResponder,
  useTransition,
  useDeferredValue,
} from './ReactHooks';
...

export {
  Children,
  createRef,
  Component,
  PureComponent,
  ...
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  ...
};

Из этого файла мы видим, что все хуки происходят из./ReactHook.jsС тех пор, как мы пришли/packages/react/src/ReactHook.js(Нажмите здесь, чтобы увидеть исходный код). Внутри мы постепенно начали видеть функции с именем useXxx, но это не настоящий исходный код хуков (как он может быть таким коротким). увидеть слова, которые часто встречаются в этом файлеdispatcher, мы, вероятно, можем знать, что этот файл в основном используется какрасписаниеРоль, когда пользователь вызывает useXxx, будет переданаresolveDispatcher()Этот метод генерирует планировщикdispatcher, а затем вызовите хук, который хочет пользователь, через планировщик. Если планирование не удается, в консоли будет напечатано сообщение об ошибке:

когда мы звонимresolveDispatcher()При получении планировщика он фактически вызывается./ReactCurrentDispatcher.js(Нажмите здесь, чтобы увидеть исходный код)изdispacther.useState():

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

В этом файле довольно странный синтаксис:import type, это на самом делеСинтаксис потока. Flow — это инструмент статической проверки типов для JavaScript, собственный проект Facebook с открытым исходным кодом, альтернатива TypeScript. Хотя TypeScript сейчас очень популярен, это большой проект по замене всего кода JavaScript на TypeScript (весь проект React в основном представляет собой код js), поэтому Flow предоставляет новый способ проверки типов данных, который начинается с нуля. инструмент проверки типов для.jsКод в файле проверен на тип данных, он совместим со всеми видами существующего кода JavaScript, и если вы не хотите использовать его в этот день, просто удалите разметку. началоimport typeРоль фактически состоит в том, чтобы импортировать типы данных из другого модуля, то есть импортировать/packages/react-reconciler/src/ReactFiberHooks.js(Нажмите здесь, чтобы увидеть исходный код)изDispatcherТипы.

тогда давайте двигаться дальшеReactFiberHooks.jsЧто есть в Канкане.

// /packages/react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {|
  readContext<T>(
    context: ReactContext<T>,
    observedBits: void | number | boolean,
  ): T,
  useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
  useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: (I) => S,
  ): [S, Dispatch<A>],
  useContext<T>(
    context: ReactContext<T>,
    observedBits: void | number | boolean,
  ): T,
  useRef<T>(initialValue: T): {|current: T|},
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  
  // 其他 Hook 的类型定义
  ...
};

приходитьReactFiberHooks.js, мы обнаружили, что это «монстр», содержащий более 2000 строк кода, и в этом файле размещены исходники всех официально предоставленных хуков. начало файлаtype DispatcherФактически, это определение типа каждого хука. Далее идет функция, которую конкретно реализует каждый хук.

Реализация хука

Прежде всего, вы должны заметить, что каждый хук имеет две связанные функции:mountXxx()а такжеupdateXxx(), это Hook на этапе монтирования (то есть монтирование компонента, или этап инициализации, или первое выполнение useXxx()) и этап обновления (то есть обновление компонента или повторное -фаза рендеринга компонента) логика. Чтобы облегчить управление и вызов, инженеры React сохраняют логику Hook на этапе монтирования в (HooksDispatcherOnMount), сохраните логику этапа обновления в (HooksDispatcherOnUpdate) и используйте собственное имя хука (useXxx) в качестве имени ключа:

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
};

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

Что делает Крюк в фазе маунта?

Каждый хук работает по-своему. Из-за ограниченного места здесь мы используемuseStateАнализ как пример. Логика использования состояния на этапе монтирования написана наmountState()В методе:

// react-reconciler/src/ReactFiberHooks.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {

  // 获取当前 Hook 节点,同时将当前 Hook 添加到 Hook 链表中
  const hook = mountWorkInProgressHook();
  
  // 初始化 Hook 的状态,即读取初始 state 值
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  
  // 创建一个新的链表作为更新队列,用来存放更新(setXxx())
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  
  // 创建一个 dispatch 方法(即 useState 返回的数组的第二个参数:setXxx()),
  // 该方法的作用是用来修改 state,并将此更新添加到更新队列中,另外还会将改更新和当前正在渲染的 fiber 绑定起来
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  
  // 返回当前 state 和 修改 state 的方法
  return [hook.memoizedState, dispatch];
}

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

  1. Получите текущий узел Hook и добавьте текущий Hook в связанный список Hook.
  2. Инициализировать состояние хука, то есть прочитать начальное значение состояния
  3. Создайте новый связанный список в качестве очереди обновления для хранения операций обновления (setXxx())
  4. Создайте метод отправки (то есть второй параметр массива, возвращаемый useState: setXxx()), цель этого метода — изменить состояние, добавить эту операцию обновления в очередь обновления, а также объединить обновление с current Волокна, подвергаемые рендерингу, связаны
  5. Метод, который возвращает текущее состояние и изменяет состояние (отправка)

Структура данных для хранения хуков - связанный список

надmountState()появляется в кодеconst hook = mountWorkInProgressHook();Такая строка кода, давайте посмотрим наmountWorkInProgressHook()Содержимое функции вы можете узнать по структуре данных хука:

// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  // 新建一个 Hook
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,  // next 指向下一个 Hook
  };

  // workInProgressHook 指向当前组件 的 Hook 链表
  if (workInProgressHook === null) {
    // 如果当前组件的 Hook 链表为空,那么就将刚刚新建的 Hook 作为 Hook 链表的第一个节点(头结点) 
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 如果当前组件的 Hook 链表不为空,那么就将刚刚新建的 Hook 添加到 Hook 链表的末尾(作为尾结点)
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

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

export type Hook = {
  memoizedState: any, // Hook 自身维护的状态
  ...
  queue: UpdateQueue<any, any> | null, // Hook 自身维护的更新队列
  next: Hook | null, // next 指向下一个 Hook
};

На этапе монтирования (инициализация компонента) при вызове useState() будет вызываться mountState()mountWorkInProgressHook()для создания нового узла Hook и добавления его в список Hook. Возьмем пример, если есть функциональный компонент, и следующий код выполняется в первый раз:

const [firstName, setFirstName] = useState('尼古拉斯');
const [lastName, setLastName] = useState('赵四');
useEffect(() => {});

Поскольку это первое выполнение, то есть на этапе монтирования, будет создан связанный список Hook, как показано на рисунке:

Где хранится весь связанный список Hook?

По теме вышеmountWorkInProgressHook()На самом деле в исходном коде уже есть общие подсказки:

// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  ...
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    ...
  }
  ...
}

Если нет связанного списка Hook, вновь созданный узел Hook будет использоваться как головной узел связанного списка Hook, а затем головной узел связанного списка Hook будет сохранен вcurrentlyRenderingFiber.memoizedStateв, то естьСвойство memoizedState текущего узла FiberNode.(Определения типов атрибутов для FiberNode написаны на/react-reconciler/src/ReactFiber.jsсередина). Такой простой оператор присваивания может связать текущий компонент с различными хуками в нем.

Как useState обрабатывает обновления состояния

Те, кто использовал useState, знают, что первый элемент массива, возвращаемого useState, — это значение текущего состояния, а второй элемент — это функция, используемая для установки (обновления) состояния (обычно эта функция будет называться setXxx).

Выше мы видимmountState()При использовании исходного кода будет создан новый список очереди обновлений.queue, функция этого связанного списка состоит в хранении операций обновления.Каждый узел в связанном списке представляет собой операцию обновления состояния (то есть однократный вызов setXxx()), чтобы можно было получить самое последнее состояние на последующей фазе обновления. . Посмотрим еще разсодержание этого списка:

const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
});
  • pending: ожидается самое последнее обновление
  • dispatch: метод обновления состояния (setXxx)
  • lastRenderedReducer: Редюсер, использованный при последнем рендеринге компонента (useState на самом деле является упрощенной версией useReducer, причина, по которой пользователям не нужно передавать редюсер при использовании useState, заключается в том, что useState по умолчанию использует редюсер, официально написанный React:basicStateReducer)
  • lastRenderedState: состояние последней визуализации компонента.

После создания очереди обновлений будет создан метод отправки (т. е. метод обновления состояния, setXxx).dispatchAction()Функция, через.bindСвяжите текущее волокно и очередь обновлений с этим методом отправки:

const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
): any));

Когда мы вызываем метод (setXxx) с новым состоянием, он вызывается немедленноdispatchAction(), задача этой функции: создать новыйобновить объект, добавленный в очередь обновлений (связанный список очереди), и на самом деле этоКруговой связанный список. Давайте взглянемdispatchAction()код::

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  ...
  // 为当前更新操作新建 update 对象
  const update: Update<S, A> = {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };
  ...
  // pending 指向的是最新的 update 对象
  // Append the update to the end of the list.
  const pending = queue.pending;
  if (pending === null) {
    // 如果更新队列为空,那么将当前的更新作为更新队列的第一个节点,并且让它的 next 属性指向自身,以此来保持为循环链表
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    // 如果更新队列为非空,那么就将当前的更新对象插入到列表的头部
    update.next = pending.next;
    // 链表的尾结点指向最新的头节点,以保持为一个循环链表
    pending.next = update;
  }
  // 让 queue.pending 指向最新的头节点
  queue.pending = update;
  
  ...
}

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

Предположим, теперь есть 3 операции, обновляющие состояние подряд:

setFirstName('Tom');
setFirstName('Allen');
setFirstName('Bill');

Итак, после выполнения трех вышеуказанных setXxx весь наш связанный список Hook становится таким:

Узел useState Hook в связанном списке Hook будет хранить исторические операции обновления в виде кругового связанного списка, как показано на рисунке выше.Из рисунка выше мы можем знать, что queue.pending всегда указывает на последнюю операцию обновления.

Когда инициировать обновление компонента (повторный рендеринг), чтобы компонент достиг фазы обновления

После добавления объекта обновления в список очереди обновлений хука,dispatchAction()Он также оценивает значение (действие), переданное текущим вызовом setXxx(action), и сравнивает его с последним отображаемым состоянием (состоянием, отображаемым на экране в это время), чтобы увидеть, есть ли какие-либо изменения. смена, звонокscheduleWork()Организовать работу по обновлению fiberNode (повторный рендеринг компонента), если нет изменений, пропустить его напрямую, не планируя обновление (повторный рендеринг компонента):

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  ...
  // 生成 update 对象,并将 update 对象添加到更新队列链表中
  ...
  
  // 获取上一次渲染的 state (也就是此时正显示在屏幕上的 state)
  const currentState: S = (queue.lastRenderedState: any);
  
  // 获取当前最新计算出来的 state(这个 state 还没有渲染,只是“迫切想要”渲染)
  const eagerState = lastRenderedReducer(currentState, action); // 如果是 useState,这一句相当于是:const eagerState = action;
  update.eagerReducer = lastRenderedReducer;
  update.eagerState = eagerState;
  
  // 判断 eagerState(当前最新计算出来的 state)和 currentState (上一次渲染时 state) 的值是否相同,如果相同则直接跳过,不再安排 fiberNode 的更新工作(取消组件的重新渲染)
  if (is(eagerState, currentState)) {
    return;
  }
  
  ...
  
  // 触发 fiberNode 安排更新工作(组件重新渲染)
  scheduleWork(fiber, expirationTime);
}

оscheduleWork()Проблема организации работы по обновлению здесь обсуждаться не будет, т.к. в ней больше логики и механизмов, таких как работа нескольких стадий файбера: стадия начала, стадия завершения, стадия фиксации, стадия раскрутки.... .. мы Изучаем на другой день. В общем, звонюscheduleWorkТакженетНемедленно обновите fiberNode для повторного рендеринга компонента.Это также включает в себя различную обработку определения приоритета обновления и слияние обновлений, например, useState здесь, чтобы дождаться всех текущихsetXxx()выполняются один за другим, если какой-либо из них вызываетscheduleWork, в конечном итоге сосредоточится наоднаждыОбновления (перерисовка компонентов).

Что делает Hook на этапе обновления?

Далее давайте посмотрим, что происходит на этапе обновления (повторный рендеринг компонента). Возьмите хук useState в качестве примера. В фазе Update, то есть при повторном рендеринге компонента, то есть когда useState выполняется второй, третий и N-й раз, он в это время не выполняется.mountState(), но выполнитьupdateState(). увидеть волнуupdateState()изисходный код:

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

Эй, ты видишь этоupdateState()который на самом деле вызываетupdateReducer(), что еще раз подтверждает, что useState на самом деле просто упрощенная версия useReducer . Поскольку мы не передаем редюсер при вызове useState, мы передаем его здесь по умолчанию.basicStateReducerЗайди как редуктор.

// 默认 reducer
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}

В использованииuseState(action), действие обычно будет значением, а не функцией. такbasicStateReducer()вернет действие напрямую.

Далее давайте посмотрим наupdateReducer()Сюда:

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取正在执行的处于更新阶段 Hook 节点
  const hook = updateWorkInProgressHook();
  // 获取更新队列链表
  const queue = hook.queue;
  ... 
  // 获取更新队列最初的 update 对象节点
  let first = baseQueue.next;
  // 初始化 newState
  let newState = current.baseState;
  ...
  let update = first;
  ... 
  do {
    ...
    // 循环遍历更新队列链表
    // 从最早的那个 update 对象开始遍历,每次遍历执行一次更新,去更新状态
    const action = update.action;
    newState = reducer(newState, action);
    update = update.next;
  } while (update !== null && update !== first);
  ...
  // 返回最新的状态和修改状态的方法
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

updateReducer()Он будет проходить по списку очереди обновлений, выполнять операцию обновления в каждом узле, получать последний статус и возвращать его, чтобы гарантировать, что мы можем получать последний статус каждый раз, когда мы обновляем компонент. Редуктор для useStatebaseStateReducer, поскольку входящее update.action является значением, update.aciton возвращается напрямую, а редуктор useReducer является определяемым пользователем редьюсером, поэтому он будет вычисляться шаг за шагом в соответствии с каждым входящим действием и newState, полученным каждым циклом. последний статус.

Подводя итог, когда выполняется фаза обновления, то есть второй, третий... N-й раз useState, делается следующее:

  1. Получить узел Hook на этапе обновления, который выполняется;
  2. Получить список очереди обновлений узла Hook;
  3. Начните обход с самого раннего узла объекта обновления в очереди обновлений и перейдите к последнему добавленному (последнему) узлу объекта обновления.При переходе к каждому узлу выполните операцию обновления узла и сохраните обновленное значение состояния в newState ;
  4. После обхода последнего узла объекта обновления последнее значение состояния в это время сохраняется в newState и, наконец, возвращается в newState, так что пользователь получает самое последнее состояние;

useState запускает процесс

Выше описано, что делает useState (useReducer) на этапе монтирования и обновления, а также когда компонент запускает обновление компонента.

Когда компонент впервые визуализируется (монтируется)

В это время useState выполняется в первый раз, что является фазой монтирования, поэтому выполняется mountState.

  1. Добавьте узел Hook для useState в список Hook.
  2. инициализировать значение состояния
  3. Возвращает состояние этого рендеринга и метод для изменения состояния

При вызове setXxx/dispatchAction

  1. Создайте объект обновления и добавьте объект обновления в список очереди обновлений узла Hook;
  2. Определите, совпадает ли входящее значение (действие) со значением состояния, отображаемым в данный момент на экране. Если оно совпадает, пропустите его. Если оно не совпадает, вызовите его.scheduleWorkРасписание повторного рендеринга компонентов;
  3. После того, как все текущие setXxx выполняются один за другим, если условия (2) могут быть выполнены, происходит вызовscheduleWorkЕсли это так, инициируйте обновление (повторный рендеринг компонента) и войдите в стадию обновления;

Когда компонент перерисовывается (обновляется)

Компонент перерисовывается и переходит в фазу Update, то есть выполняется 2-й, 3-й, ... n раз useState:

  1. Получить список очереди обновлений хука useState;
  2. Пройдите список очереди обновлений, перейдите от самого раннего объекта обновления к последнему добавленному объекту обновления и, наконец, получите последнее состояние и верните его как состояние рендеринга компонента на этот раз;
  3. Возвращает состояние этого рендеринга и метод для изменения состояния

Исходный код Zaikangkang useEffect

После приведенного выше анализа мы знаем, что хук выполняет разные операции при первом рендеринге (монтировании) и повторном рендеринге компонента. Он выполняется, когда первый рендерингmountXxx()метод, а при повторном рендеринге выполнениеupdateXxx()метод.

То же самое верно и для хука useEffect, который соответствуетmountEffectа такжеupdateEffect.

mountEffect

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  ...
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,  // HookPassive 一个 hook effect tag,表示如果这个 effect 需要被执行,那么它将会在组件 UI 渲染完后执行
    create,
    deps,
  );
}

mountEffectВ качестве точки входа действительно начинает работатьmountEffectImpl:

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  // 获取当前 Hook 节点,同时将当前 Hook 添加到 Hook 链表中
  const hook = mountWorkInProgressHook();
  
  // 获取依赖
  const nextDeps = deps === undefined ? null : deps;
  
  // pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,然后返回这个当前 effcet
  // 然后是把返回的当前 effect 保存到 Hook 节点的 memoizedState 属性中
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, // 这里用了位运算,HookHasEffect 也是一个 hook effect tag,表示这个 effect 需要执行。如果一个 effect 没有被打上 HookHasEffect 这个 tag,那么这个 effect 将会被跳过,不会执行
    create,
    undefined,
    nextDeps,
  );
}

существуетmountEffectImpl, текущий узел Hook и зависимости useEffect будут получены по очереди, а вызовpushEffectТекущий эффект добавления в FibernodeupdateQueueПоставьте в очередь и сохраните текущий эффект в текущем узле HookmemoizedStateв свойствах.

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  // 获取当前 FiberNode 的 updateQueue
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  
  if (componentUpdateQueue === null) {
    // 如果 updateQueue 为空,那就创建一个新的 updateQueue,其中 lastEffect 指向最新添加进来的 effect
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // 将当前 effect 添加到 updateQueue 中,并同样保持循环链表的结构
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      // 假如 lastEffect 指向 null,说明此时链表还不是循环链表的结构,那么就要控制最新的 effect 的 next 的指向,使其变为循环链表的结构 
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 将当前 effect 添加到 updateQueue 中
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // 令 lastEffect 始终指向最新添加进来的 effect
      componentUpdateQueue.lastEffect = effect;
    }
  }
  // 返回当前 effect
  return effect;
}

Подводя итог, основные вещи, которые делает useEffect на этапе монтирования:

  1. Получить текущий узел Hook и добавить его в список Hook;
  2. Получите зависимые зависимости этого эффекта;
  3. Добавьте эффект в updateQueue волокна. Свойство lastEffect объекта updateQueue всегда указывает на последний эффект, добавленный в очередь, а следующее свойство lastEffect всегда указывает на самый ранний добавленный эффект, который каждый раз будет формироваться заново.Круговой связанный списокСтруктура.

updateEffect

прочитай этоmountEffect, посмотри сноваupdateEffect:

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  ...
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

updateEffectпоток вызовов иmountEffectсходство,updateEffectЭто просто вход, что действительно работает, так этоupdateEffectImpl:

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  // 获取当前 Hook 节点,并把它添加到 Hook 链表中
  const hook = updateWorkInProgressHook();
  
  // 获取依赖
  const nextDeps = deps === undefined ? null : deps;
  
  // 初始化清除 effect 函数
  let destroy = undefined;

  if (currentHook !== null) {
    // 获取上一次渲染的 Hook 节点的 effect
    const prevEffect = currentHook.memoizedState;
    
    // 获取上一次渲染的 Hook 节点的 effect 的清除函数
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      // 获取上一次渲染的 Hook 节点的 effect 的依赖
      const prevDeps = prevEffect.deps;
      
      // 对比前后依赖的值是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果依赖的值相同,即依赖没有变化,那么只会给这个 effect 打上一个 HookPassive 一个 tag,然后在组件渲染完以后会跳过这个 effect 的执行
        pushEffect(hookEffectTag, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.effectTag |= fiberEffectTag;

  // pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,然后返回这个当前 effcet
  // 然后是把返回的当前 effect 保存到 Hook 节点的 memoizedState 属性中
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, // 给 effect 打上 HookHasEffect 和 HookPassive 两个 tag,表示在组件 UI 渲染完后需要执行这个 effect
    create,
    destroy,
    nextDeps,
  );
}

UseEffect на этапе обновления в основном похож на то, что делает этап монтирования.Единственное отличие состоит в том, что на этапе обновления будет учитываться, изменилась ли зависимость эффекта.Если изменений нет, то будет добавлен только тег HookPassive эффект, а на заключительном этапе фиксации (после завершения рендеринга представления компонента) выполнение эффекта будет пропущено; если есть изменение зависимости, эффект будет помечен HookPassive и HookHasEffect, что указывает на то, что эффект будет выполняться после завершения рендеринга представления компонента.

Подводя итог, useEffect на этапе обновления выполняет следующие действия:

  1. Получить текущий узел Hook и добавить его в список Hook;
  2. Получите зависимые зависимости этого эффекта;
  3. Сравните зависимости этого эффекта с зависимостями предыдущего рендеринга:
    • Если зависимостей и изменений нет, то только пометьте этот эффект с помощью HookPassive и пропустите выполнение этого эффекта на этапе фиксации (после рендеринга представления компонента);
    • Если зависимость изменится, эффект будет помечен HookPassive и HookHasEffect, и эффект будет выполнен на этапе фиксации (после рендеринга представления компонента);
  4. Добавьте этот эффект в updateQueue fiberNode и сохраните этот эффект в свойстве memoizedState текущего узла Hook.

useEffect запускает процесс

Функциональный компонент, использующий хук useEffect, работает следующим образом:

Начальный рендеринг компонента (монтаж):

  1. При выполнении useEffect добавьте хук useEffect в связанный список хуков, затем создайте updateQueue для fiberNode и добавьте этот эффект в updateQueue;
  2. визуализировать пользовательский интерфейс компонента;
  3. После завершения рендеринга пользовательского интерфейса выполните этот эффект;

Ререндеры компонентов (обновления):

  1. При выполнении useEffect добавьте хук useEffect в связанный список хуков, чтобы определить зависимости:
    • Если входящей зависимости нет (useEffect не проходит во втором параметре), то напрямую пометить эффект "необходимо выполнить" (HookHasEffect);
    • Если есть входящие зависимости и изменилось сравнение между текущей зависимостью и последней зависимостью рендеринга, то пометьте эффект тегом «необходимо выполнить» (HookHasEffect);
    • Если есть входящие зависимости, но зависимость не изменилась, тоНе будетДайте этому эффекту тег, который «должен быть выполнен»;
    • Если есть входящие зависимости, но в них передается пустой массив[], то такжеНе будетДайте этому эффекту тег, который «должен быть выполнен»;
  2. визуализировать пользовательский интерфейс компонента;
  3. Если есть функция очистки (возврат содержимого в действии), выполните функцию очистки последнего рендеринга; если зависимость[], не нужно сначала выполнять функцию очистки, а дождаться уничтожения компонента;
  4. Определите, есть ли у этого эффекта тег (HookHasEffect), который «нужно выполнить». Если да, то выполните этот эффект, если нет, то пропустите его напрямую.не выполнятьэтот эффект;

Когда компонент уничтожен:

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

Добавьте логику пометки эффекта

Во-первых, давайте посмотрим на эффективный крючок, что такое некоторые из тегов.

Тег эффекта хука определен в/packages/react-reconciler/src/ReactHookEffectTags.jsсередина:

export type HookEffectTag = number;

export const NoEffect = /*  */ 0b000;

// Represents whether effect should fire.
export const HasEffect = /* */ 0b001;

// Represents the phase in which the effect (not the clean-up) fires.
export const Layout = /*    */ 0b010;
export const Passive = /*   */ 0b100;

Значение каждого тега следующее:

  • NoEffect: его функция заключается в использовании в качестве условия для суждения, фактически он не будет использоваться при маркировке.
  • HasEffect: Если эффект помечен этим тегом, это означает, что эффект нужно выполнить
  • Passive: Если эффект отмечен этим тегом, это означает: если эффект необходимо выполнить, он будет выполнен после завершения рендеринга пользовательского интерфейса компонента.
  • Layout: если этот эффект попал в этот тег, то: если эффект необходимо выполнить, он будет выполнен до рендеринга пользовательского интерфейса компонента.

В двух словах,NoEffectФункция состоит в том, чтобы судить об условии, которое используется для определения наличия тега;HasEffectРоль заключается в том, чтобы определить, будет ли выполняться эффект; иPassiveа такжеLayoutРоль этих двух тегов состоит в том, чтобы определить, на какой фазе выполняется эффект.

Из исходного кода мы видим, что эти теги на самом деле являютсяДвоичная константа(в js0bНачало представляет собой двоичный код), потому что соответствующая логика этого тега эффекта проходит черезбитовая операцияПроводится эта хреновая операция. Давайте посмотрим на процесс тегирования. Операция пометки эффекта на самом деле выполняется вызовомpushEffect(), передайте первый параметр:

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  ...
  // hookEffectTag = HookPassive
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}

HookHasEffect | hookEffectTagЗдесь побитовое ИЛИ побитовых операций (|), когда результат выполнения0b101, указывающий, что эффект будет выполнен после завершения рендеринга пользовательского интерфейса компонента.

После пометки на этапе фиксации:

function schedulePassiveEffects(finishedWork: Fiber) {
  ...
      do {
        const {next, tag} = effect;
        // 判断 effect tag 有没有同时有 HookPassive 和 HookHasEffect 两个 tag,假如都有,那么这个 effect 将会在组件 UI 渲染完成后执行
        if (
          (tag & HookPassive) !== NoHookEffect &&
          (tag & HookHasEffect) !== NoHookEffect
        ) {
          enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
          enqueuePendingPassiveHookEffectMount(finishedWork, effect);
        }
        effect = next;
      } while (effect !== firstEffect);
    }
  }
}

Другой пример:

// hookEffectTag = HookPassive
if (areHookInputsEqual(nextDeps, prevDeps)) {
  pushEffect(hookEffectTag, create, destroy, nextDeps);
  return;
}

Приведенный выше пример прошел только вHookPassive, то тег этого эффекта всего лишьHookPassive. Указывает, что если этот эффект необходимо выполнить, он будет выполнен после завершения рендеринга пользовательского интерфейса компонента, но поскольку этот эффект не отмеченHookHasEffect, так что этот эффект на самом делене будет выполняться.

PS

  1. Когда это фаза фиксации волокна, она временно понимается как组件视图渲染完成后, когда именно и что вы делали? Из-за ограниченного места мы пока не будем обсуждать это здесь, если вам интересно, вы можете посмотреть исходный код.react-reconciler/src/ReactFiberCommitWork.jsэтот файл. Также есть несколько этапов:Начать этап,Полный этап,Расслабьтесь этап.

  2. Когда мы посмотрим на исходный код, мы увидимcurrentHookа такжеworkInProgressHookДве глобальные переменные, они фактически эквивалентны двум указателям, указывающим на один и тот же односвязный список Hook, но узлы, на которые они указывают, разные. Разница между ними заключается в том, что узел, на который указывает currentHook, представляет хук, который в данный момент отображается на экране, и на экране отображается некоторое состояние этого хука; этого хука Он все еще находится в фоновом процессе расчета, и это не то состояние, которое в конечном итоге отображается на экране. Конкретное описание между ними см. в исходном коде.react-reconciler/src/ReactUpdateQueue.jsПримечание в начале файла, автор подробно представит концепцию, функцию и разницу между ними в примечании.

конец

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

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