нажмитеВойдите в репозиторий отладки исходного кода React.
Обзор
Как только взаимодействие с пользователем приводит к обновлению, создается объект обновления для переноса нового состояния. Несколько обновлений будут объединены в кольцевой связанный список: updateQueue, смонтированный на оптоволокне, Затем на этапе beginWork файбера будет зацикливаться updateQueue, и обновления в нем будут обрабатываться по очереди.Это общий процесс обработки обновлений, то есть суть вычисления нового состояния компонента. В React компоненты класса и корневые компоненты используют один тип объекта обновления, а функциональные компоненты используют другой тип объекта обновления, но все они следуют аналогичному набору механизмов обработки. А пока сосредоточимся на объекте обновления компонента класса.
Связанные концепции
Как происходит обновление? В компоненте класса обновление можно сделать, вызвав setState:
this.setState({val: 6});
в то время как setState фактически вызоветenqueueSetState
, создайте объект обновления и вызовитеenqueueUpdate
Поместите его в updateQueue.
const classComponentUpdater = {
enqueueSetState(inst, payload, callback) {
...
// 依据事件优先级创建update的优先级
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;
enqueueUpdate(fiber, update);
// 开始调度
scheduleUpdateOnFiber(fiber, lane, eventTime);
...
},
};
Если предположить, что узел B генерирует обновление, то updateQueue узла B в конечном итоге будет иметь следующий вид:
A
/
/
B ----- updateQueue.shared.pending = update————
/ ^ |
/ |_______|
C -----> D
UpdateQueue.shared.pending хранит одно за другим обновления. Ниже мы объясним структуру следующих обновлений и updateQueue.
структура обновления
Объект обновления, как носитель обновления, должен хранить обновленную информацию.
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
- eventTime: время генерации обновления, если обновление не было выполнено из-за недостаточного приоритета, оно истечет по тайм-ауту и будет выполнено немедленно
- пер.: приоритет обновления, то есть приоритет обновления
- suspenseConfig: связанная с приостановкой задачи
- тег: указывает тип обновления (UpdateState, ReplaceState, ForceUpdate, CaptureUpdate)
- полезная нагрузка: состояние, переносимое обновлением.
- В компонентах класса: есть две возможности: объекты ({}) и функции ((prevState, nextProps): newState => {})
- В корневом компоненте: React.element, первый параметр ReactDOM.render
- обратный вызов: можно понимать как обратный вызов setState
- next: указатель на следующее обновление
Структура updateQueue
На компоненте может быть несколько обновлений, поэтому для файбера нужен связанный список для хранения этих обновлений, это updateQueue, и его структура следующая:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
Давайте предположим, что обновление было сгенерировано, а затем посмотрим на значение этих полей в зависимости от момента, когда обновление было обработано:
- baseState: состояние, вычисленное из предыдущего обновления, которое является вычисленным состоянием этих обновлений до первого пропущенного обновления. Будет использовать его в качестве основы для расчета текущего состояния
- firstBaseUpdate: первый пропущенный объект обновления в updateQueue во время предыдущего обновления.
- lastBaseUpdate: в предыдущем обновлении последнее обновление в очереди перехватывалось от первого пропущенного обновления до последнего обновления в updateQueue.
- shared.pending: Очередь обновлений, в которой хранится это обновление, является фактической очередью обновлений. Общий означает, что текущий узел разделяет очередь обновлений с узлом workInProgress.
- эффекты: массив. Сохранить обновление с помощью update.callback !== null
Несколько моментов для объяснения:
- Для сценариев, в которых создается несколько объектов обновления, вызовите setState несколько раз.
this.setState({val: 2});
this.setState({val: 6});
Результирующая структура updateQueue выглядит следующим образом:
Видно, что это односторонний кольцевой связанный список.
u1 ---> u2
^ |
|________|
- О том, почему очередь обновлений циклическая.
Вывод: это потому, что удобно найти первый элемент связанного списка. updateQueue указывает на свое последнее обновление, а updateQueue.next указывает на свое первое обновление.
Только представьте, если вы не используете кольцевой связанный список, updateQueue указывает на последний элемент, и вам нужно пройти, чтобы получить заголовок связанного списка. Даже если updateQueue указывает на первый элемент, при добавлении обновления все равно необходимо пройти до конца, чтобы подключить новое дополнение к связанному списку. В круговом связанном списке вам нужно запомнить только хвост, и вы можете найти голову без операции обхода. Понимание концепции является главным приоритетом, давайте посмотрим на реализацию:
function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
return;
}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; // ppending是真正的updateQueue,存储update
const pending = sharedQueue.pending;
if (pending === null) { // 若链表中没有元素,则创建单向环状链表,next指向它自己
update.next = update;
} else {
// 有元素,现有队列(pending)指向的是链表的尾部update,
// pending.next就是头部update,新update会放到现有队列的最后
// 并首尾相连
// 将新队列的尾部(新插入的update)的next指向队列的首部,实现
// 首位相连
update.next = pending.next; // 现有队列的最后一个元素的next指向新来的update,实现把新update
// 接到现有队列上
pending.next = update;
} // 现有队列的指针总是指向最后一个update,可以通过最后一个寻找出整条链表
sharedQueue.pending = update;
}
- Что касается firstBaseUpdate и lastBaseUpdate, два из них фактически образуют связанный список: baseUpdate, основанный на текущем обновлении, этот связанный список сохраняет первое пропущенное низкоприоритетное обновление в последнем updateQueue, в очередь всех обновлений между последним обновлением. Что касается baseState, то это состояние, вычисляемое этими обновлениями до первого пропущенного обновления.
Эти два момента немного сложно понять.Давайте проиллюстрируем пример: Например, есть следующая очередь updateQueue:
A1 -> B1 -> C2 -> D1 - E2
Буквы обозначают статус обновления, а числа указывают приоритет обновления. В модели дорожек можно понять, что чем меньше число, тем выше приоритет, поэтому 1 > 2
В первый раз, когда очередь обрабатывается с приоритетом рендеринга 1, когда встречается C2, ее приоритет не равен 1, и она пропускается. Затем, пока updateQueue не будет обработан на этот раз, связанный список baseUpdate в это время
C2 -> D1 - E2
После завершения этого обновления выполняется firstBaseUpdate.C2
, lastBaseUpdateE2
, базовое состояниеABD
.
Используйте firstBaseUpdate и lastBaseUpdate для записи всех обновлений от пропущенного обновления до последнего обновления и используйте baseState для записи состояния, вычисленного этими обновлениями перед пропущенным обновлением. Это делается для того, чтобы результаты всех приоритетных обновлений в итоговой очереди updateQueue соответствовали ожидаемым результатам. То есть, хотяA1 -> B1 -> C2 -> D1 - E2
При первом вычислении этого связанного списка с приоритетом 1 результат будет ABD (поскольку приоритет 2 пропущен), но окончательный результат должен быть ABCDE, потому что все объекты обновления в очереди все. В результате обработки давайте подробно проанализировать механизм обработки updateQueue.
Обновленный механизм обработки
Обновление обработки делится на три этапа: этап подготовки, этап обработки и этап завершения. Первые два этапа в основном связаны с updateQueue, а последний этап — присвоение вновь вычисленного состояния волокну.
Этап подготовки
Приведите в порядок updateQueue. Из-за приоритета низкоприоритетное обновление будет пропущено и будет ожидать следующего выполнения, в ходе которого может быть сгенерировано новое обновление. Так что при обработке обновления может быть две очереди обновлений:Что осталось с прошлого раза и что нового на этот раз.осталось с прошлого разаЭто все обновления от firstBaseUpdate до lastBaseUpdate;Добавлено на этот разЭто обновление вновь созданных.
На этапе подготовки две очереди объединяются, и объединенная очередь больше не является циклической, что удобно для прохождения и обработки от начала до конца. Кроме того, поскольку вышеуказанные операции являются updateQueue обработанного узла workInProgress, также необходимо снова управлять текущим узлом для поддержания синхронизации. Цель состоит в том, чтобы снова создать новый узел workInProgress на основе текущего узла после прерывания рендеринга. по высокоприоритетной задаче. , обновления, которые не были обработаны ранее, не будут потеряны.
этап обработки
Прокрутите очередь обновлений, отсортированную на предыдущем шаге. Здесь есть два важных момента:
- Обрабатывает ли это обновление обновление, зависит от его приоритета (update.lane) и приоритета рендеринга (renderLanes).
- Результаты расчета этого обновления основаны на baseState.
Недостаточный приоритет
Обновления с недостаточным приоритетом будут пропущены.Помимо пропуска, это также делает три вещи:
- Поместите пропущенное обновление в связанный список, состоящий из firstBaseUpdate и lastBaseUpdate (то есть baseUpdate), и дождитесь обработки следующего низкоприоритетного обновления.
- Запись baseState, baseState в это время является результатом всех обновлений, которые были обработаны до обновления с низким приоритетом, и записывается только тогда, когда оно пропускается в первый раз, потому что, когда задача с низким приоритетом будет переделана, она будет быть пропущен с первого обновления начинается обработка.
- Запишите приоритет пропущенного обновления и поместите его в workInProgress.lanes после того, как процесс обновления будет близок к завершению.Это ключ к повторному запуску планирования и повторному выполнению задач с низким приоритетом.
Что касается второго пункта, то комментарии в заголовке файла ReactUpdateQueue.js поясняют его, и для простоты понимания объясню еще раз.
第一次更新的baseState 是空字符串,更新队列如下,字母表示state,数字表示优先级。优先级是1 > 2的
A1 - B1 - C2 - D1 - E2
第一次的渲染优先级(renderLanes)为 1,Updates是本次会被处理的队列:
Base state: ''
Updates: [A1, B1, D1] <- 第一个被跳过的update为C2,此时的baseUpdate队列为[C2, D1, E2],
它之前所有被处理的update的结果是AB。此时记录下baseState = 'AB'
注意!再次跳过低优先级的update(E2)时,则不会记录baseState
Result state: 'ABD'--------------------------------------------------------------------------------------------------
第二次的渲染优先级(renderLanes)为 2,Updates是本次会被处理的队列:
Base state: 'AB' <- 再次发起调度时,取出上次更新遗留的baseUpdate队列,基于baseState
计算结果。
Updates: [C2, D1, E2] Result state: 'ABCDE'
достаточный приоритет
Если приоритет обновления достаточен, в основном есть две вещи:
- Считается, что если очередь baseUpdate не пуста (ранее было пропущенное обновление), текущее обновление помещается в очередь baseUpdate.
- Обработайте обновление, вычислите новое состояние.
Помещение обновлений с достаточным приоритетом в baseUpdate можно комбинировать с обновлением с низким приоритетом, ставящим в очередь baseUpdate выше. Фактически это означает, что однажды пропущенное обновление используется как начальная точка, а последующие обновления до последнего перехватываются независимо от приоритета. Давайте использовать пример выше, чтобы проиллюстрировать.
A1 - B2 - C1 - D2
B2被跳过,baseUpdate队列为
B2 - C1 - D2
Это делается для того, чтобы конечные результаты всех обновлений соответствовали ожидаемым результатам всех обновлений, запускаемых действиями пользователя. Например, хотя A1 и C1 предпочтительно выполняются в первый раз, отображаемый результат — AC, но это только для того, чтобы отреагировать на временный результат, созданный взаимодействием с пользователем во времени.На самом деле, результат C1 должен зависеть от результат вычисления B2 при втором рендеринге и начале обработки очереди B2 — C1 — D2 в соответствии с результатом обработки обновления предварительного заказа B2 (baseState — A), а окончательный результат — ABCD. в предоставленномВысокоприоритетные задачи сокращаются в очередьпример, чтобы продемонстрировать это.
Процесс изменения 0 -> 2 -> 3, жизненный цикл установит состояние 1 (задача A2), а событие щелчка будет состояние + 2 (задача A1).При нормальных обстоятельствах A2 запланирован нормально, но рендеринг не завершен.В это время А1 режет очередь,обновить очередь А2 - А1,чтобы сначала реагировать на высокоприоритетные обновления,пропустить А2 и сначала вычислить А1,число меняется с 0 на 2,baseUpdate это А2 - A1, а baseState равно 0. Затем переделайте задачи с низким приоритетом. Процесс baseUpdate A2 - A1, вычисление на основе baseState(0), окончательный результат равен 3.
этап завершения
В основном выполняйте задания и расставляйте приоритеты.
- Назначьте updateQueue.baseState. Если в этом рендере не пропущено ни одно обновление, то ему присваивается вновь вычисленное состояние, в противном случае ему присваивается обновление перед первым пропущенным обновлением.
- Назначьте firstBaseUpdate и lastBaseUpdate из updateQueue, то есть, если на этот раз обновление будет пропущено, назначьте перехваченную очередь связанному списку baseUpdate для updateQueue.
- Обновите дорожки узла workInProgress. Стратегия обновления заключается в том, что если ни один приоритет не пропущен, это означает, что на этот раз обновление было обработано, и дорожки очищаются. В противном случае поместите приоритет обновления с низким приоритетом в дорожки. Я сказал раньше,
Это ключ к запуску другого расписания для повторного выполнения задач с низким приоритетом.
- Обновите memoizedState на узле workInProgress.
реализация исходного кода
Выше в основном описан весь процесс обработки обновления, теперь давайте посмотрим на реализацию исходного кода. Код этой части находится вprocessUpdateQueue
В функции задействовано большое количество операций со связанными списками, а кода больше,
Давайте сначала посмотрим на его структуру, и я выделил три этапа.
function processUpdateQueue<State>(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void {
// 准备阶段
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
let firstBaseUpdate = queue.firstBaseUpdate;
let lastBaseUpdate = queue.lastBaseUpdate;
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) { /* ... */ }
if (firstBaseUpdate !== null) { // 处理阶段
do { ... } while (true);
// 完成阶段
if (newLastBaseUpdate === null) {
newBaseState = newState;
}
queue.baseState = ((newBaseState: any): State);
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState;
}
}
После понимания вышеуказанных концепций и основной структуры исходного кода, полный код выпускается, но нерелевантные части удаляются.Я добавил комментарий.Полезнее будет понять сравнение трех процессов.В противном случае работа связанный список все еще немного сложен.
function processUpdateQueue<State>(
workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void {
// 准备阶段----------------------------------------
// 从workInProgress节点上取出updateQueue
// 以下代码中的queue就是updateQueue
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
// 取出queue上的baseUpdate队列(下面称遗留的队列),然后
// 准备接入本次新产生的更新队列(下面称新队列)
let firstBaseUpdate = queue.firstBaseUpdate;
let lastBaseUpdate = queue.lastBaseUpdate;
// 取出新队列
let pendingQueue = queue.shared.pending;
// 下面的操作,实际上就是将新队列连接到上次遗留的队列中。
if (pendingQueue !== null) { queue.shared.pending = null;
// 取到新队列
const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next;
// 将遗留的队列最后一个元素指向null,实现断开环状链表
// 然后在尾部接入新队列
lastPendingUpdate.next = null;
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
// 将遗留的队列中最后一个update的next指向新队列第一个update
// 完成接入
lastBaseUpdate.next = firstPendingUpdate; } // 修改遗留队列的尾部为新队列的尾部
lastBaseUpdate = lastPendingUpdate;
// 用同样的方式更新current上的firstBaseUpdate 和
// lastBaseUpdate(baseUpdate队列)。
// 这样做相当于将本次合并完成的队列作为baseUpdate队列备份到current节
// 点上,因为如果本次的渲染被打断,那么下次再重新执行任务的时候,workInProgress节点复制
// 自current节点,它上面的baseUpdate队列会保有这次的update,保证update不丢失。
const current = workInProgress.alternate;
if (current !== null) {
// This is always non-null on a ClassComponent or HostRoot
const currentQueue:UpdateQueue<State> = (current.updateQueue: any);
const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
if (currentLastBaseUpdate !== lastBaseUpdate) {
if (currentLastBaseUpdate === null) {
currentQueue.firstBaseUpdate = firstPendingUpdate;
} else {
currentLastBaseUpdate.next = firstPendingUpdate;
}
currentQueue.lastBaseUpdate = lastPendingUpdate;
}
}
}
// 至此,新队列已经合并到遗留队列上,firstBaseUpdate作为
// 这个新合并的队列,会被循环处理
// 处理阶段-------------------------------------
if (firstBaseUpdate !== null) { // 取到baseState
let newState = queue.baseState;
// 声明newLanes,它会作为本轮更新处理完成的
// 优先级,最终标记到WIP节点上
let newLanes = NoLanes;
// 声明newBaseState,注意接下来它被赋值的时机,还有前置条件:
// 1. 当有优先级被跳过,newBaseState赋值为newState,
// 也就是queue.baseState
// 2. 当都处理完成后没有优先级被跳过,newBaseState赋值为
// 本轮新计算的state,最后更新到queue.baseState上
let newBaseState = null;
// 使用newFirstBaseUpdate 和 newLastBaseUpdate // 来表示本次更新产生的的baseUpdate队列,目的是截取现有队列中
// 第一个被跳过的低优先级update到最后的所有update,最后会被更新到
// updateQueue的firstBaseUpdate 和 lastBaseUpdate上
// 作为下次渲染的遗留队列(baseUpdate)
let newFirstBaseUpdate = null;
let newLastBaseUpdate = null;
// 从头开始循环
let update = firstBaseUpdate;
do {
const updateLane = update.lane;
const updateEventTime = update.eventTime;
// isSubsetOfLanes函数的意义是,判断当前更新的优先级(updateLane)
// 是否在渲染优先级(renderLanes)中如果不在,那么就说明优先级不足
if (!isSubsetOfLanes(renderLanes, updateLane)) {
const clone: Update<State> = {
eventTime: updateEventTime,
lane: updateLane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
// 优先级不足,将update添加到本次的baseUpdate队列中
if (newLastBaseUpdate === null) {
newFirstBaseUpdate = newLastBaseUpdate = clone;
// newBaseState 更新为前一个 update 任务的结果,下一轮
// 持有新优先级的渲染过程处理更新队列时,将会以它为基础进行计算。
newBaseState = newState;
} else {
// 如果baseUpdate队列中已经有了update,那么将当前的update
// 追加到队列尾部
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
/* *
* newLanes会在最后被赋值到workInProgress.lanes上,而它又最终
* 会被收集到root.pendingLanes。
* 再次更新时会从root上的pendingLanes中找出渲染优先级(renderLanes),
* renderLanes含有本次跳过的优先级,再次进入processUpdateQueue时,
* update的优先级符合要求,被更新掉,低优先级任务因此被重做
* */
newLanes = mergeLanes(newLanes, updateLane);
} else {
if (newLastBaseUpdate !== null) {
// 进到这个判断说明现在处理的这个update在优先级不足的update之后,
// 原因有二:
// 第一,优先级足够;
// 第二,newLastBaseUpdate不为null说明已经有优先级不足的update了
// 然后将这个高优先级放入本次的baseUpdate,实现之前提到的从updateQueue中
// 截取低优先级update到最后一个update
const clone: Update<State> = {
eventTime: updateEventTime,
lane: NoLane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig);
// 处理更新,计算出新结果
newState = getStateFromUpdate( workInProgress, queue, update, newState, props, instance, );
const callback = update.callback;
// 这里的callback是setState的第二个参数,属于副作用,
// 会被放入queue的副作用队列里
if (callback !== null) {
workInProgress.effectTag |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
} // 移动指针实现遍历
update = update.next;
if (update === null) {
// 已有的队列处理完了,检查一下有没有新进来的,有的话
// 接在已有队列后边继续处理
pendingQueue = queue.shared.pending;
if (pendingQueue === null) {
// 如果没有等待处理的update,那么跳出循环
break;
} else {
// 如果此时又有了新的update进来,那么将它接入到之前合并好的队列中
const lastPendingUpdate = pendingQueue;
const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
lastPendingUpdate.next = null;
update = firstPendingUpdate;
queue.lastBaseUpdate = lastPendingUpdate;
queue.shared.pending = null;
}
}
} while (true);
// 如果没有低优先级的更新,那么新的newBaseState就被赋值为
// 刚刚计算出来的state
if (newLastBaseUpdate === null) {
newBaseState = newState;
}
// 完成阶段------------------------------------
queue.baseState = ((newBaseState: any): State);
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes; workInProgress.memoizedState = newState;
}
}
Логика useReducer в хуках для обновления состояния вычисления в основном такая же, как здесь.
Суммировать
После приведенного выше прочесывания видно, что вся обработка обновлений вращается вокруг приоритета. Целью всей функции processUpdateQueue является обработка обновлений, но для того, чтобы обновления обрабатывались в соответствии с приоритетом, а они не были случайными, потому что следует фиксированному набору правил: после пропуска приоритета запоминать состояние на на этот раз и очередь обновления после этого приоритета, и резервное копирование очереди на текущий узел, что имеет решающее значение для упорядоченной и полной обработки объекта обновления, а также гарантирует, что окончательный представленный результат обработки и результат взаимодействие, вызванное поведением пользователя, остается согласованным.