Анализ общего исходного кода API React Hooks, написанного нашими стажерами

React.js

Эта статья является стажером в той же группе, что и ByteDance.@idealism идиотПрочтите краткий документ об исходном коде React Hooks во время стажировки и поделитесь им следующим образом после моей модификации:

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

Проблема, которую решают хуки

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

  1. существуетClass ComponentПовторное использование логики состояния между компонентами класса затруднено. Идея предыдущего решения React проблемы повторного использования такова:render propsИ компоненты более высокого порядка, недостаток в том, что их трудно понять, и слишком много вложенности, чтобы образовать «гнездовой ад».

  2. сложныйClass ComponentСтановится трудно понять: функции жизненного цикла компонентов класса будут наполнены различной логикой состояний и побочными эффектами, которые сложно переиспользовать и разбросать, как вcomponentDidMountа такжеcomponentDidUpdate получить данные, но вcomponentDidMountВ нем также может быть много другой логики, из-за чего компоненты становятся все более и более раздутыми, и логика явно нагромождена в различных функциях жизненного цикла, что делает разработку React «программированием, ориентированным на жизненный цикл».

  3. Class ComponentСложные вопросы, например:

    • эта проблема с указателем
    • Технология прекомпиляции компонентов столкнется со случаем сбоя оптимизации в классе
    • класс плохо сжимается
    • Классы нестабильны при горячей перезагрузке

Команда React надеется, что хуки, запущенные в v16, смогут решить вышеуказанные проблемы, но есть и некоторые трудности, которые необходимо решить при реализации хуков:

  • Class Componentможет хранить состояние экземпляра постоянно, в то время какFunctional ComponentНо не может, потому что каждый раз функция перевыполняетсяstateбудет переназначен на 0

  • КаждыйClass componentЭкземпляры имеют функцию-членthis.setStateизменить свое состояние иFunction componentэто просто функция и не может иметьthis.setStateЭто использование может быть достигнуто только с помощью глобального метода setState или других методов.

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

    setAge(18)
    setAge(19)
    setAge(20)  // 只关心这一次计算出来的值
    

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

  • ...

Чтобы решить вышеуказанные трудности, команда React разработала основную логику архитектуры Hook: с помощью замыканий два связанных списка (один компонентhookсвязанный список вызовов, каждыйhookЕсть много объектовupdateСписок цепочекqueue) и сквознойdispatchИсходный код будет подробно рассмотрен ниже.

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

Структура объекта хука

type Hook = {
  memoizedState: any,   // 上次更新之后的最终状态值
  queue: UpdateQueue, //更新队列
  next, // 下一个 hook 对象
};

надnextДля чего нужны указатели?

вFunctional Componentможно вызывать несколько раз вhook, следующим образом:

let [name, setName] = useState('')
let [age, setAge] = useState(0)

Каждый раз, когда вызывается метод ловушки,hookобъект. Приведенный выше код вызывается 2 разаuseStateбудет генерировать 2hookобъект, эти 2hook черезnext Указатели объединяются в связанный список, как показано на следующем рисунке:

Структура данных объекта обновления

type Update = {
  expirationTime: ExpirationTime,//过期时间
  action: A,//修改动作
  eagerReducer: ((S, A) => S) | null,//下一个reducer
  eagerState: S | null,//下一次的state
  next: Update; | null,//下一个update
};

Объект обновления записывает информацию об изменении данных. обнаружит, что естьnext указатель, несколькоupdateОбъекты также объединяются в структуру связанного списка.

Структура данных объекта очереди

Вышеупомянутое обновление сформирует связанный список, записанный в объекте очереди.lastВ свойствах имена переменных разных версий реакции немного отличаются.Новая версия реакцииpending Атрибуты. Здесь следует отметить, что, поскольку нам нужно вставить объект обновления, а затем пройти по связанному списку для вычисления в конце, поэтомуupdateОн состоит из кругового связанного списка,lastуказать на последнийupdate, следующее обновление соответствует первому обновлению

type queue = {
  last: Update| null,   // 记录了第一个 update 对象
  dispatch,							// 记录了解构给用户的 dispatch 方法
  lastRenderedReducer,
  lastRenderedState,
};

В исходном коде реакции три вышеупомянутые основные структуры данных:hook,queue,updateСуществует отношение эталонного ряда, как показано на следующем рисунке:

хорошо, понялhook,queue,updateДля понимания трех основных структур данных мы можем взглянуть на исходный код.

useState

Мы начинаем с основногоuseStateИсходник выглядит.

Большинство хуков в React делятся на две фазы: При инициализации в первый разmountэтап и время обновленияupdateсцена.

useStateНе исключение, соответствующее двум методам:

  1. mountState
  2. updateState

mountState

давайте сначала посмотримmountStateЧто сделано в методе, а потом посмотрите исходный код, станет понятнее.

  1. генерироватьhookобъект и смонтировать его наfiberобъектmemoizedStateв связанном списке, на который указывает атрибут
  2. генерироватьhookобъектmemoizedStateСвойства используются для записи обновленных значений;hookобъектqueueАтрибут, который представляет собой связанный список инициализированного обновления.
  3. генерироватьdispatch метод возвращается пользователю.dispatchвторой параметр полученной деструктуризации

Подробный анализ исходного кода выглядит следующим образом:

// mountState() 会在 mount 阶段被调用
function mountState(initialState) {
  // 1. 生成一个 hook 对象
  var hook = mountWorkInProgressHook();
  // hook.memoizedState 作用是记录上一次更新的最新值。下一次更新是拿最新值开始计算,而不是最初的值。在 mount 阶段先设置为初始值
  hook.memoizedState = hook.baseState = initialState;
  // 2. 一个 hook 有可能会产生多个 update,通过 queue 对象来记录 update 链表, queue.pending 指向了 update 链表中的第一个, queue.dispatch 记录解构给用户的dispatch
  hook.queue = {
    pending: null,
    dispatch: null,   
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  // fiber 和 hook 是一对多的关系,这种关系通过 bind 记录在 dispatch  方法中
  // 3. 调用 .bind() 预注入fiber(当前fiber)和 hook.queue。
  hook.queue.dispatch = dispatchAction.bind(null, fiber, hook.queue);

  return [hook.memoizedState, hook.queue.dispatch];
}

В приведенном выше коде следует отметить, чтоdispatch метод черезdispatchAction.bind()генерируется, а затем монтируется вhookобъектqueueсвойства выше.

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

Почему ты говоришь, что это потрясающе? потому чтоbind()Метод потрясающий.bind()В дополнение к изменению точки этого, вы также можете предварительно ввести параметры.

Эта строка кода выше работаетdispatchAction.bind(null, fiber, hook.queue), фактический эффект заключается не только в привязке this к null, но и в создании нового метода, который предварительно внедряется заранееfiber(текущий объект волокна) и hook.queue. Когда мы звоним в бизнес-кодsetName('aaa')Когда на самом деле реальный вызов и входные параметрыdispatch(fiber, hook.queue, 'aaa').

ЭтоfunctionalComponentМожно вызывать несколько разhookметод, чтобы соответствующиеfiberобъект иhookМежду объектами существует отношение «один ко многим», которое определяетсяbind()Способ предварительного ввода параметров записывается во вновь сгенерированной функции, которую также можно назвать闭包, потому что вновь сгенерированная функция сохраняетfiber,hookСсылки на эти две переменные не будут уничтожены сборщиком мусора движка js.hookИнформация об обновлении записывается на объект, так чтоFunctional ComponentОн реализует постоянство данных.

Приведенный выше код, в самом начале, позвонивmountWorkInProgressHook()Метод недавно создал объект ловушки, давайте посмотримmountWorkInProgressHookЧто делает метод.

mountWorkInProgressHook

function mountWorkInProgressHook() {
  // 1. 新建了一个 hook 对象
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };

  // 2. 把新建的 hook 对象挂载到链表上
  if (workInProgressHook === null) {
    // 如果当前没有 hook 链表,则 memoizedState 属性指向 hook
    fiber.memoizedState = workInProgressHook = hook;
  } else {
    // 如果已经存在 hook 链表了,则通过 next 指针串联
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

в коде вышеfiber изmemoizedStateНекоторых студентов свойства могут сбить с толку, потому что объект memoizedState хорошо знаком в исходном коде реакции.hookОбъекты также имеют свойство memoizedState, которое не имеет к ним никакого отношения.

hookобъектmemoizedStateСвойство записывает значение после последнего обновления и является конкретным значением.

fiber объектmemoizedStateСвойства имеют разное значение в разных типах компонентов, вFunctional Componentсередина,fiber изmemoizedStateатрибут для записиhookсвязанный список, указывающий на первыйhookобъект, который находится вышеhookобъект. существуетClass Componentсередина, memoizedStateИспользуется для записи данных, соответствующих компонентам классаstate.

Приведенный выше кодmountUpdateвся логика. В конце кода мы возвращаем пользователю деструктурированныйdispatchметод используется для обновления данных, когда пользователь вызываетsetNameи так далее, введитеupdateState этап, дальше посмотримupdateStateЧто происходит за кулисами.

updateState

Каждый раз, когда мы выполняемdispatchметод, создастupdateобъект,updateОбъект записывает информацию об обновлении, структура следующая:

type Update = {
  expirationTime: ExpirationTime,//过期时间
  suspenseConfig: null | SuspenseConfig,
  action: A,//修改动作
  eagerReducer: ((S, A) => S) | null,	//下一个reducer
  eagerState: S | null,	//下一次的state
  next: Update<S, A> | null,//下一个update
}

созданныйupdateОбъект будет подключен к списку обновленийqueueначальство.

Обновить связанный списокqueueЧто это такое?

Устанавливается на соответствующийhookобъектqueueВ атрибуте React принимает структуру данных связанного списка при каждом обновлении.nextуказатель, сгенерированный тот же хукupdateОбъекты объединены. при вставке второгоupdateпредмет, второйupdateобъектnextуказывает на головной узел.

Так,updateStateЧто именно он сделал? Ниже приведеныdispatchActionметод:

// 参数fiber和参数queue 是通过 bind 预传入的两个参数
// action 是用户真正穿过来的值
function dispatchAction(fiber,queue,action) {    
   	// 生成一个 update 对象
  	const update = {   
      action,
      next: null,    
    };    
  	// 将 update 对象添加到循环链表中    
    const last = queue.last;    
    if (last === null) {      
        // 链表为空,将当前更新作为第一个,并保持循环      
        update.next = update;    
    } else {      
        const first = last.next;      
        if (first !== null) {        
        // 在最新的update对象后面插入新的update对象        
            update.next = first;      
        }      
        last.next = update;    
    }    
    // 将表头保持在最新的update对象上    
    queue.last = update;   
    // 进行调度工作    
    scheduleWork(); 
}

Приведенный выше код не делает ничего, кроме трех вещей:

  • создать объект обновления
  • Добавьте объект обновления в круговой связанный список
  • Вызов scheduleWork() для планирования работы

scheduleWork()После этого он входит в процесс обновления алгоритма планирования реагирования, который находится за пределами объема этой статьи.

useEffect

useEffectИспользование также делится наmountа такжеupdate,mountСцена в основном предназначена для создания эффекта, и ее нужно повесить в двух местах, одно из нихhooksцепь, другой сквознойpushEffectПучокuseEffectвсе собраноupdateQueueв этом связанном списке, а затем выполнить после завершения обновленияupdateQueueФункция. Фаза обновления в основном такая же, за исключением того, что добавляется решение deps.Если deps не изменится, он будет помечен тегом, который не нужно обновлять, и тогда функция не будет выполняться во время процесса updateQueue. .img

// react-reconciler/src/ReactFiberHooks.js
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps,
  );
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.effectTag |= fiberEffectTag;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    undefined,
    nextDeps,
  );
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

useMemo и useCallback

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

// 比较相关函数
function is(x: any, y: any) {
  return (
  (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) 
  );
}

for (let i = 0; i<prevDeps.length &&i < nextDeps.length; i++) {
  if (is(nextDeps[i], prevDeps[i])) {
      continue;
  }
  return false;
}
return true

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

// useMemo相关 
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}


// useCallback相关
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

Например, пример с emmmm может быть неуместен.Основная идея состоит в том, чтобы выразить, что после переключения контекста функция вызывается в предыдущем контексте, переменная внутри функции по-прежнему указывает на предыдущую ссылку на переменную, а переменная с то же имя не будет перезаписано.

let obj = {} 
function area() {
  let b = 666
  const test = () => {
    console.log(b)
  }
  obj.test = test // 在外层缓存 模拟FiberNode上的Hook的memories
}
area()
function area2() {
  let b = 999
  obj.test() // 这里调用area里的test,b指向 666 
}
area2() // 666 

useRef

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

function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

Общая структурная схема

Присоединяйтесь к нам