Эта статья является стажером в той же группе, что и ByteDance.@idealism идиотПрочтите краткий документ об исходном коде React Hooks во время стажировки и поделитесь им следующим образом после моей модификации:
Большинство проектов, с которыми я столкнулся во время стажировки, были проектами, связанными с React.Я часто сталкивался с некоторыми проблемами в процессе использования хуков API.Иногда это было связано с пониманием API, а иногда я был смущен тем, почему это Так случилось, поэтому я потратил несколько дней, чтобы проверить некоторую информацию и прочитать исходный код, и я сделал здесь резюме, которое можно рассматривать как некоторый личный обмен опытом понимания. Исходный код хуков в каждой версии может немного отличаться.Нам не нужно заботиться о конкретных именах переменных, а только общий процесс и структура данных.Текстовая часть следующая:
Проблема, которую решают хуки
Появление любой новой технологии заключается в решении существующих проблем. Прежде чем официально ввести исходный код, давайте посмотрим, какие проблемы пытаются решить хуки:
-
существует
Class Component
Повторное использование логики состояния между компонентами класса затруднено. Идея предыдущего решения React проблемы повторного использования такова:render propsИ компоненты более высокого порядка, недостаток в том, что их трудно понять, и слишком много вложенности, чтобы образовать «гнездовой ад». -
сложный
Class Component
Становится трудно понять: функции жизненного цикла компонентов класса будут наполнены различной логикой состояний и побочными эффектами, которые сложно переиспользовать и разбросать, как вcomponentDidMount
а такжеcomponentDidUpdate
получить данные, но вcomponentDidMount
В нем также может быть много другой логики, из-за чего компоненты становятся все более и более раздутыми, и логика явно нагромождена в различных функциях жизненного цикла, что делает разработку React «программированием, ориентированным на жизненный цикл». -
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
Не исключение, соответствующее двум методам:
mountState
updateState
mountState
давайте сначала посмотримmountState
Что сделано в методе, а потом посмотрите исходный код, станет понятнее.
- генерировать
hook
объект и смонтировать его наfiber
объектmemoizedState
в связанном списке, на который указывает атрибут - генерировать
hook
объектmemoizedState
Свойства используются для записи обновленных значений;hook
объектqueue
Атрибут, который представляет собой связанный список инициализированного обновления. - генерировать
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
свойства выше.
Вызывается каждый раз, когда пользователь инициализируетhook
api, создаст уникальный и волшебный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. .
// 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;
}