- Исходная версия реакции: v16.11.0
- Примечания к исходному коду:airingursb/react
Перед написанием этой статьи я заранее прочитал некоторые статьи, и анализ исходного кода хуков либо слишком мелкий, либо не дотошный, поэтому эта статья не забывает разгадывать исходный код, с небольшой глубины, стремиться к строке кода. В этой серии объясняется, что первые хуки - это USSTATE, мы начнем с USSTATE, а затем объясним правила, объясним принципы, а затем просто реализуем и, наконец, разрешим. Кроме того, в этой статье, дополняющей обзор HOOKов, первые два ограничиваются проблемой не написанного ни одного.
Примечание: Прошло два месяца с момента последней статьи.Бизнес занят в эти два месяца, поэтому нет времени обновлять эту серию статей, но реакт обновился с 16.9 до 16.11 за последние два месяца, и я Я просматривал эти обновления несколько раз Ни один из хуков не задействован, поэтому я также напрямую обновил примечание к исходному коду до версии 16.11.
1. Обзор хуков React
Хуки появились в React 16.8 и позволяют использовать состояние и другие функции React без написания классов. По сути, это особый вид функции, который согласуется сuseВначале вы можете внедрить некоторые функции в функциональный компонент, предоставив функциональному компоненту некоторые возможности, которыми обладает компонент класса.
Например, первоначально мы сказали, что функциональный компонент не может сохранять состояние, поэтому мы часто говорим функциональный компонент без сохранения состояния, но теперь мы можем использовать хук useState, чтобы сделать функциональный компонент таким же сохраняющим состояние, как и классовый компонент. Некоторое время назад @types/react также изменил SFC на FC.
1.1 Мотивация
на официальном сайте ReactВведение в хукиПричины запуска Hook перечислены в:
- Повторное использование логики состояния между компонентами сложно
- Сложные компоненты становятся трудными для понимания
- непонятный класс
Во-первых, сложно повторно использовать логику состояния между компонентами. Это вопрос, который обсуждался во второй части нашей серии и не будет повторяться здесь.
Во-вторых, сложные компоненты становятся трудными для понимания, то есть логика компонентов усложняется. В основном для Class Component нам часто приходится писать код в различных жизненных циклах компонента, таких как получение данных в componentDidMount и componentDidUpdate, но componentDidMount также может включать много другой логики, что делает компонент более раздутым по мере его разработки. И логика, очевидно, сгруппирована в различных функциях жизненного цикла, что делает разработку React «программированием, ориентированным на жизненный цикл». Появление хуков превратило этот вид «программирования, ориентированного на жизненный цикл», в «программирование, ориентированное на бизнес-логику», так что разработчикам больше не нужно заботиться о жизненном цикле, о котором они не должны заботиться.
В-третьих, непонятный класс, который показывает, что функциональное программирование проще, чем ООП. Итак, если копнуть немного глубже и рассмотреть производительность, будут ли хуки замедляться из-за создания функций во время рендеринга? Ответ — нет, первоначальная производительность замыканий и классов в современных браузерах существенно отличается только в экстремальных сценариях. Вместо этого мы можем утверждать, что дизайн Хука был бы более эффективным в некоторых отношениях:
- Хуки позволяют избежать дополнительных накладных расходов, которые требуются классам, таких как стоимость создания экземпляров класса и привязки обработчиков событий в конструкторе.
- Идиоматический код не требует глубокой вложенности дерева компонентов при использовании хуков. Это явление очень распространено в кодовых базах, которые используют компоненты более высокого порядка, реквизиты рендеринга и контексты. Дерево компонентов меньше, а объем работы в React уменьшен.
На самом деле преимущества React Hooks заключаются не только в большей функциональности, более тонкой детализации обновлений и более четком коде, но и в следующих трех преимуществах:
- Множественные состояния не будут вложены друг в друга, а метод записи по-прежнему мозаичен: как и async/await для ада обратного вызова, хуки также решают проблему вложенного ада компонентов более высокого порядка. Хотя renderProps также может решить эту проблему с помощью компоновки, он немного более громоздкий в использовании и увеличивает количество сущностей за счет принудительной инкапсуляции нового объекта.
- Хуки могут ссылаться на другие хуки, а пользовательские хуки более гибкие.
- Проще отделить пользовательский интерфейс компонента от состояния.
1.2 Hooks API
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
- useResponder
Вышеупомянутый API хуков будет объяснен один за другим в будущем и не будет повторяться здесь. В этой статье сначала объясняется useState.
1.3 Пользовательские хуки
С помощью пользовательских хуков логика компонентов может быть извлечена в многократно используемые функции. Вот сайт Амвэй:usehooks.com/, который собирает практичные пользовательские хуки, которые можно легко подключить к проекту, что полностью отражает возможность повторного использования и простоту хуков.
2. Использование и правила useState
import React, { useState } from 'react'
const App: React.FC = () => {
const [count, setCount] = useState<number>(0)
const [name, setName] = useState<string>('airing')
const [age, setAge] = useState<number>(18)
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => {
setCount(count + 1)
setAge(age + 1)
}}>
Click me
</button>
</>
)
}
export default App
Если вы использовали редукцию, эта сцена должна быть вам очень знакома. Учитывая начальное состояние, отправьте действие, измените состояние с помощью редуктора и вернитесь в новое состояние, запустив компонент для повторного рендеринга.
Он эквивалентен следующему компоненту класса:
import React from 'react'
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
age: 18,
name: 'airing'
};
}
render() {
return (
<>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({
count: this.state.count + 1,
age: this.state.age + 1
})}>
Click me
</button>
</>
);
}
}
Видно, что Function Component более лаконичен, чем Class Component, и использование useState также очень простое. Но следует отметить, что использование хуков должно соответствовать этому правилу:Убедитесь, что хуки вызываются в одном и том же порядке при каждом рендеринге.. Поэтому лучше каждый раз использовать хуки только на верхнем уровне и не вызывать хуки в циклах, условиях и вложенных функциях, иначе легко наделать ошибок.
Так почему мы должны соблюдать это правило? Далее, давайте взглянем на принцип реализации useState и сами реализуем useState, чтобы увидеть с первого взгляда.
3. Принцип и простая реализация useState
3.1 Demo 1: dispatch
Во втором разделе мы обнаружили, что использование useState очень похоже на Redux, поэтому мы сами реализуем useState на основе идеи Redux:
function useState(initialValue) {
let state = initialValue
function dispatch(newState) {
state = newState
render(<App />, document.getElementById('root'))
}
return [state, dispatch]
}
Мы заменим useState из React нашей собственной реализацией:
import React from 'react'
import { render } from 'react-dom'
function useState(initialValue: any) {
let state = initialValue
function dispatch(newState: any) {
state = newState
render(<App />, document.getElementById('root'))
}
return [state, dispatch]
}
const App: React.FC = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('airing')
const [age, setAge] = useState(18)
return (
<>
<p>You clicked {count} times</p>
<p>Your age is {age}</p>
<p>Your name is {name}</p>
<button onClick={() => {
setCount(count + 1)
setAge(age + 1)
}}>
Click me
</button>
</>
)
}
export default App
На этот раз мы обнаружили, что нажатие кнопки не будет иметь никакого ответа, и ни количество, ни возраст не изменились. Поскольку реализованный нами useState не имеет функции хранения, последнее состояние сбрасывается при каждом повторном рендеринге. Я думаю, что вы можете использовать переменную для ее внешнего хранения.
3.2 Демонстрация 2: Состояние памяти
Основываясь на этом, давайте оптимизируем useState, который мы только что реализовали:
let _state: any
function useState(initialValue: any) {
_state = _state | initialValue
function setState(newState: any) {
_state = newState
render(<App />, document.getElementById('root'))
}
return [_state, setState]
}
Хотя нажатие кнопки изменилось, эффект не совсем правильный. Если мы удалим два useStates, возраст и имя, мы обнаружим, что эффект нормальный. Это связано с тем, что мы используем только одну переменную для хранения, поэтому, естественно, может быть сохранено только одно значение useState. Тогда мы подумали, что можно использовать memorandum, то есть массив, для хранения всего состояния, но при этом нужно вести индекс массива.
3.3 Демонстрация 3: Памятка
Основываясь на этом, давайте еще раз оптимизируем только что реализованный useState:
let memoizedState: any[] = [] // hooks 的值存放在这个数组里
let cursor = 0 // 当前 memoizedState 的索引
function useState(initialValue: any) {
memoizedState[cursor] = memoizedState[cursor] || initialValue
const currentCursor = cursor
function setState(newState: any) {
memoizedState[currentCursor] = newState
cursor = 0
render(<App />, document.getElementById('root'))
}
return [memoizedState[cursor++], setState] // 返回当前 state,并把 cursor 加 1
}
После того, как мы нажмем кнопку три раза, данные, которые выведут memoizedState, будут следующими:
Откройте страницу для первого рендеринга, каждый раз при выполнении useState соответствующий setState будет привязан к положению соответствующего индекса, а затем начальное состояние будет сохранено в memoizedState.
При нажатии кнопки будут запущены setCount и setAge, и каждый setState имеет ссылку на соответствующий индекс, поэтому запуск соответствующего setState изменит значение состояния в соответствующем месте.
Это фиктивная реализация useState, поэтому каждый вызов setState имеет процесс повторного рендеринга.
Повторный рендеринг по-прежнему последовательно выполняет useState, но memoizedState уже имеет последнее значение состояния, поэтому инициализированное значение — это не начальное переданное значение, а последнее значение.
Таким образом, ответ на вопрос, оставленный в Разделе 2, очевиден: почему хуки должны гарантировать, что хуки вызываются в одном и том же порядке при каждом рендеринге? Поскольку memoizedState размещает данные в порядке, определенном хуками, если порядок хуков изменится, memoizedState не воспримет это. Так что лучше использовать хуки только на верхнем уровне за раз и не вызывать хуки в циклах, условиях или вложенных функциях.
Наконец, давайте посмотрим, как useState реализован в React.
4. Анализ исходного кода useState
4.1 Вход
Сначала мы находим useState во входном файле packages/react/src/React.js, который происходит из packages/react/src/ReactHooks.js.
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
resolveDispatcher() возвращает ReactCurrentDispatcher.current, поэтому useState на самом деле является ReactCurrentDispatcher.current.useState.
Итак, что такое ReactCurrentDispatcher?
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
}
В итоге мы нашли packages/react-reconciler/src/ReactFiberHooks.js, где реализован useState. Этот файл также содержит основную логику обработки для всех React Hooks.
4.2 Определение типа
4.2.1 Hook
Прежде чем мы начнем, давайте взглянем на определения нескольких типов в ReactFiberHooks.js. Первый — хуки:
export type Hook = {
memoizedState: any, // 指向当前渲染节点 Fiber, 上一次完整更新之后的最终状态值
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any, any> | null, // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any, any> | null, // 缓存的更新队列,存储多次更新行为
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
};
Видно, что структура данных хуков в основном такая же, как и то, что мы реализовали ранее.memoizedState также является массивом.Если быть точным, хуки React представляют собой односвязный список, а Hook.next указывает на следующий хук.
4.2.2 Update & UpdateQueue
Так что же такое baseUpdate и очередь? Давайте взглянем на определения типов Update и UpdateQueue:
type Update<S, A> = {
expirationTime: ExpirationTime, // 当前更新的过期时间
suspenseConfig: null | SuspenseConfig,
action: A,
eagerReducer: ((S, A) => S) | null,
eagerState: S | null,
next: Update<S, A> | null, // link 下一个 Update
priority?: ReactPriorityLevel, // 优先级
};
type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
Обновление называется обновлением и используется при планировании обновления React. UpdateQueue является очередью обновления, а также имеет диспетчеризацию при обновлении. Конкретный процесс планирования обновлений React Fiber и React не будет рассматриваться в этой статье, и позже будут отдельные статьи с дополнительными пояснениями.
4.2.3 HooksDispatcherOnMount & HooksDispatcherOnUpdate
Есть также два определения типа Dispatch, на которые следует обратить внимание: одно — HooksDispatcherOnMount при первой загрузке, а другое — HooksDispatcherOnUpdate при обновлении.
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: createResponderListener,
};
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: createResponderListener,
};
4.3 Первый рендер
4.3.1 renderWithHooks
React Fiber начнет выполняться с beginWork() в packages/react-reconciler/src/ReactFiberBeginWork.js (конкретный процесс React Fiber будет объяснен отдельно в отдельном документе). Для функционального компонента он загружает или обновляет компонент с помощью следующая логика:
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
В updateFunctionComponent обработка хуков:
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
Поэтому мы обнаружили, что основной записью рендеринга React Hooks является renderWithHooks. Другие процессы рендеринга нас не интересуют, в этой статье мы сосредоточимся на renderWithHooks и логике после него.
Вернемся к ReactFiberHooks.js, чтобы посмотреть, что делает renderWithHooks, удаляя отказоустойчивый код и__DEV__, код renderWithHooks выглядит следующим образом:
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
nextCurrentHook = current !== null ? current.memoizedState : null;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
// sideEffectTag = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because nextCurrentHook === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so nextCurrentHook would be null during updates and mounts.
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, refOrContext);
if (didScheduleRenderPhaseUpdate) {
do {
didScheduleRenderPhaseUpdate = false;
numberOfReRenders += 1;
// Start over from the beginning of the list
nextCurrentHook = current !== null ? current.memoizedState : null;
nextWorkInProgressHook = firstWorkInProgressHook;
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnUpdateInDEV
: HooksDispatcherOnUpdate;
children = Component(props, refOrContext);
} while (didScheduleRenderPhaseUpdate);
renderPhaseUpdates = null;
numberOfReRenders = 0;
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
renderedWork.updateQueue = (componentUpdateQueue: any);
renderedWork.effectTag |= sideEffectTag;
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
// These were reset above
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
return children;
}
renderWithHooks состоит из трех частей: первая — это назначение ReactCurrentDispatcher.current, упомянутого в 4.1, затем didScheduleRenderPhaseUpdate и некоторая работа по инициализации. Ядром является первая часть, давайте посмотрим:
nextCurrentHook = current !== null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
Если текущий Файбер пуст, он считается загруженным в первый раз, и ReactCurrentDispatcher.current.useState будет присвоено значение HooksDispatcherOnMount.useState, в противном случае будет присвоено значение HooksDispatcherOnUpdate.useState. Согласно определению типа в 4.2, при первой загрузке useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnMount.useState = mountState, при обновлении useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnUpdate.useState = updateState.
4.3.2 mountState
Сначала посмотрите на реализацию mountState:
// 第一次调用组件的 useState 时实际调用的方法
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 创建一个新的 Hook,并返回当前 workInProgressHook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 新建一个队列
const queue = (hook.queue = {
last: null, // 最后一次更新逻辑, 包括 {action,next} 即状态值和下一次 Update
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any), // 最后一次渲染组件时的状态
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// 绑定当前 fiber 和 queue.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
4.3.3 mountWorkInProgressHook
mountWorkInProgressHook должен создать новый хук и вернуть текущий workInProgressHook, который реализован следующим образом:
// 创建一个新的 hook,并返回当前 workInProgressHook
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
// 只有在第一次打开页面的时候,workInProgressHook 为空
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 已经存在 workInProgressHook 就将新创建的这个 Hook 接在 workInProgressHook 的尾部。
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
4.3.4 dispatchAction
Мы заметили, что mountState также делает очень важную вещь, привязывая текущий файбер и очередь к dispatchAction:
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// 绑定当前 fiber 和 queue
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
Затем давайте посмотрим, как реализовано dispatchAction:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 此分支为 re-render 时的 Fiber 调度处理
didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
expirationTime: renderExpirationTime,
suspenseConfig: null,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 将本次更新周期里的更新记录缓存进 renderPhaseUpdates 中
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
const currentTime = requestCurrentTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// 存储所有的更新行为,以便在 re-render 流程中计算最新的状态值
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
// ... 更新循环链表
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
// 省略特殊情况 Fiber NoWork 时的代码
// 创建一个更新任务,执行 fiber 的渲染
scheduleWork(fiber, expirationTime);
}
}
Первая ветвь if включает в себя планирование Fibre, о котором мы здесь только упоминаем.Эта статья не объясняет подробно Fiber, если вы знаетеfiber === currentlyRenderingFiberПри повторном рендеринге, то есть в текущем цикле обновления генерируется новый цикл. Если это повторный рендеринг, didScheduleRenderPhaseUpdate устанавливается в значение true, а в renderWithHooks, если didScheduleRenderPhaseUpdate имеет значение true, он будет циклически подсчитывать numberOfReRenders для записи количества повторных рендеров; кроме того, nextWorkInProgressHook также будет иметь значение. Таким образом, в последующем коде numberOfReRenders > 0 используется для определения того, является ли он повторным рендерингом, а nextWorkInProgressHook пуст, чтобы судить, является ли он повторным рендерингом.
В то же время, если это повторный рендеринг, все обновления, созданные в процессе обновления, будут записаны на карту renderPhaseUpdates с очередью каждого хука в качестве ключа.
Что же касается специфики работы итогового графика работы, то ее мы разберем отдельно позже.
4.4 Обновление
4.4.1 updateState
Давайте взглянем на метод updateState, который на самом деле вызывается при использовании состояния в процессе обновления:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
// 第一次之后每一次执行 useState 时实际调用的方法
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
Можно обнаружить, что на самом деле updateState наконец вызывает updateReducer. Для действия обновления, инициируемого useState, basicStateReducer напрямую возвращает значение действия (если действие является функцией, вызов его также поможет). Следовательно, useState — это всего лишь частный случай useReduer, а его входящий редюсер — это basicStateReducer, отвечающий за изменение состояния, вместо useReducer можно передать кастомный редьюсер.
4.4.2 updateReducer
Итак, давайте посмотрим, что делает 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;
queue.lastRenderedReducer = reducer;
if (numberOfReRenders > 0) {
// re-render:当前更新周期中产生了新的更新
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
// 所有更新过程中产生的更新记录在 renderPhaseUpdates 这个 Map上,以每个 Hook 的 queue 为 key。
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
// 如果是 re-render,继续执行这些更新直到当前渲染周期中没有更新为止
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
if (hook.baseUpdate === queue.last) {
hook.baseState = newState;
}
queue.lastRenderedState = newState;
return [newState, dispatch];
}
}
return [hook.memoizedState, dispatch];
}
const last = queue.last;
const baseUpdate = hook.baseUpdate;
const baseState = hook.baseState;
let first;
if (baseUpdate !== null) {
if (last !== null) {
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
if (!didSkip) {
didSkip = true;
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime;
}
} else {
markRenderEventTimeAndConfig(
updateExpirationTime,
update.suspenseConfig,
);
// 循环链表,执行每一次更新
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateReducer делится на два случая:
- Без повторного рендеринга, то есть в текущем цикле обновления есть только одно обновление.
- повторный рендеринг, текущий цикл обновления произвел новое обновление.
В 4.3.4 мы упомянули, что numberOfReRenders записывает количество повторных рендеров, если оно больше 0, то это означает, что в текущем цикле обновления произошли новые обновления, тогда продолжаем выполнять эти обновления и создавать новые состояния по редюсеру и update.action, пока в текущем цикле рендеринга не будет обновления, и, наконец, назначьте его Hook.memoizedState и Hook.baseState.
Примечание: На самом деле, если вы используете только useState, вы вряд ли столкнетесь со сценой повторного рендеринга, если только вы не напишете setState прямо в начале функции, но это приведет к бесконечному повторному рендерингу, numberOfReRenders превысит лимит, пусть программа в 4.3.4 dispatchAction Error (4.3.4 скрытый
__DEV__с этой частью отказоустойчивого кода):
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
Затем давайте посмотрим на ситуацию без повторного рендеринга, удалим код, связанный с Fiber, и специальную логику, и сосредоточимся на цикле do-while, который отвечает за циркуляцию связанного списка и выполнение каждого обновления:
do {
// 循环链表,执行每一次更新
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
Еще один момент, который следует отметить, заключается в том, что в этом случае необходимо оценивать приоритет каждого обновления.Если обновление не соответствует текущему общему приоритету обновления, оно будет пропущено, а первое пропущенное обновление станет новым Hook.baseUpdate. Необходимо следить за тем, чтобы последующие обновления выполнялись снова на основе обновления baseUpdate, поэтому результаты могут быть другими. Конкретная логика здесь будет написана и проанализирована отдельно позже. Наконец, его также необходимо присвоить Hook.memoizedState и Hook.baseState.
4.4.3 updateWorkInProgressHook
Чтобы добавить сюда, обратите внимание, что способ, которым первая строка кода получает хук, отличается от mountState, updateWorkInProgressHook должен получить хук, который работает в данный момент. Реализация выглядит следующим образом:
// 获取当前正在工作中的 Hook,即 workInProgressHook
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
Здесь есть два случая: в 4.3.4 мы упомянули, что если nextWorkInProgressHook существует, то он перерисовывается, если перерендерится, значит workInProgressHook должен продолжать обрабатываться в текущем цикле обновления.
Если это не повторный рендеринг, возьмите следующий хук в качестве текущего хука и создайте новый хук, например 4.3.3 mountWorkInProgressHook, и верните workInProgressHook.
Короче говоря, updateWorkInProgressHook получает workInProgressHook в текущем задании.
5. Вывод
Интуитивно я перехватил структуру данных хука в действии, как показано на следующем рисунке:
Обобщите приведенный выше процесс анализа, как показано на следующем рисунке:
Если у вас все еще есть сомнения относительно исходного кода useState, вы можете сами написать небольшую демонстрацию для отладки ключевых точек останова функций.