Буквально на прошлой неделе я поделился в компании решением по оптимизации среды выполнения React. Ниже приводится текстовая версия обмена. Статья относительно длинная и в ней много сухого товара. Я думаю, у вас будет другое понимание React после читая это.
Всего два месяца назад,React
Только что вышла версия 18, не забудьте прочитать ее подробно в прошлый разReact
Исходный код, или три года назад, читалReact
В 15-й версии я в основном изучал механизм рендеринга виртуального DOM, механизм выполнения setState иReact
Синтетические события , также написал следующие статьи о React:
- [Подробно реагировать] механизм выполнения setState
- [Подробно о реакции] Механизм событий React
- [Углубленное реагирование] Углубленный анализ процесса рендеринга и характеристик виртуального DOM.
- [Углубленное реагирование] От Mixin до HOC и Hook
После того, как я закончил свое исследование, я не мог не вздохнуть, и я никогда не буду читать это снова.React
Исходный код, по сравнению с другими фреймворками,React
Исходный код действительно трудно читать.
Однако вслед заReact 16-18
Обновить,React
Базовая архитектура . Это также разожгло мое любопытство к тому, что случилось с исходным кодом React, поэтому я решил снова прочитать последнюю версию исходного кода.
К счастью, предки сажали деревья, а последующие поколения наслаждались тенью.В сообществе есть много больших коров, которые сделали очень хорошие интерпретации исходного кода, такие как KassonДемистификация технологии React, 7кмИсходный код графического React. Согласно этим туториалам я реорганизовал новую версиюReact
Раздел исходного кода архитектуры, весь процесс был пересмотрен, а важные модули были прочитаны и исследованы сами по себе.
Я не думаю, что нужно публиковать новую серию интерпретаций исходного кода.Первые две проделали хорошую работу.Однако я надеюсь помочь вам проанализировать и интерпретировать ее короче.React
Основное направление развития, несколько последних крупных обновлений, что сделано.
С 16 лет по сей день,React
пережил 15-18 несколько крупных релизов, кромеHooks,React
Есть несколько крупных обновлений в новых функциях, пока некоторое время назад у React, который долгое время молчал, наконец не появилась волна новых API.
Однако в предыдущих версиях он не сидел сложа руки и создал для нас множество концепций,Concurrent Mode、Fiber、Suspense、lanes、Scheduler、Concurrent Rendering
, эти концепции отпугивают некоторых начинающих разработчиков.
Основная цель этой статьи состоит в том, чтобыReact
Эволюцию основной стратегии оптимизации в несколько этапов, чтобы четко разобраться в этих понятиях, см.React
Что вы делаете в последние несколько лет, и, кстати, давайте интерпретировать эти особенности последнего обновления. В совместном использовании мы можем не анализировать конкретный процесс планирования и детали в деталях, но мы выберем некоторые исходные коды для интерпретации некоторых стратегий оптимизации.
Так почему же тема этой статьи — среда выполнения? Давайте сначала взглянем на сравнение дизайна нескольких основных фреймворков.
Идеи дизайна нескольких основных фреймворков JS
прежде всегоReact
,React
Это фреймворк повторного выполнения, после изменения данных он не работает напрямую.dom
вместо создания нового так называемого виртуальногоdom
, это помогает нам решить проблемы с кроссплатформенностью и совместимостью, а такжеdiff
Алгоритм обеспечивает минимальное рабочее поведение, и все это выполняется во время выполнения.
В последнее время было жаркоSvelte
, который является типичной структурой перекомпиляции.Как разработчику, нам нужно только написать шаблоны и данные.Svelte
компиляция и предварительная обработка, код будет в основном преобразован в нативныйDOM
действовать,Svelte
Исполнение также максимально приближено к родномуjs
из.
Так,Vue
Эта структура с хорошим компромиссом между временем выполнения и предварительной компиляцией сохраняет виртуальныеdom
, но будет управлять виртуальнымdom
В предварительной компиляции было сделано достаточно оптимизаций производительности для получения обновлений по требованию.
Итак, давайте посмотрим, что такое оптимизация во время компиляции.
Что такое оптимизация во время компиляции?
Vue
Используется шаблонный синтаксис.Особенность шаблона в том, что синтаксис ограничен.Мы можем использоватьv-if v-for
Эти указанные грамматики закодированы, хотя это недостаточно динамично, но поскольку грамматики являются перечислимыми, на предварительно скомпилированном уровне можно делать больше прогнозов, что позволяетVue
Лучшая производительность во время выполнения. Ниже мы можем увидетьVue 3.0
Определенные оптимизации, сделанные во время компиляции.
Традицияvdom
изDiff
Алгоритм всегда основан наvdom
Иерархия дерева проходится слой за слоем, поэтомуdiff
Производительность будет положительно связана с размером шаблона, независимо от количества динамических узлов. В случае, если некоторые компоненты имеют лишь небольшое количество динамических узлов во всем шаблоне, эти обходы являются пустой тратой производительности.
Например, в приведенном выше примере кода эти статические узлы вряд ли изменятся на этапе обновления компонента. если вы можетеdiff
Этап, пропускающий статический контент, позволяет избежать бесполезного обхода и сравнения дерева dom.
существуетVue3.0
Внутри есть такая похожая стратегия оптимизации, ееcompiler
Согласно динамическим свойствам узла, для каждого виртуальногоdom
создавать разныеpatchflag
, скажем, узел имеет динамическийtext
или с динамическимclass
, будут отмечены разнымиpatchflag
.
потомpatchflag
Переделкаblock tree
, может быть достигнуто целевое обновление различных узлов.
тупиковая среда выполнения
Добро пожаловать назадReact
, его собственная идея чистаJS
Этот способ написания очень гибкий, но он также затрудняет выполнение слишком многих действий во время компиляции, а оптимизацию во время компиляции, подобную приведенной выше, трудно достичь. Итак, мы можем видетьReact
Оптимизации нескольких основных версий в основном выполняются во время выполнения.
Итак, что нас больше всего беспокоит во время выполнения?
Во-первых, это проблема процессора, частота обновления основных браузеров, как правило,60Hz
, который обновляется каждую секунду60
раз, наверное16.6ms
Обновите браузер один раз. из-заGUI
визуализировать нить иJS
Потоки взаимоисключающие, поэтомуJS
Выполнение скрипта и макет браузера и рендеринг не могут выполняться одновременно.
В этот16.6ms
время, браузер должен завершить обаJS
Исполнение стиля также необходимо для завершения перестановки и перекраски стиля, еслиJS
Выполнение заняло слишком много времени, чтобы превысить16.6ms
, это обновление не успеет выполнить компоновку стилей и отрисовку стилей, поэтому оно застрянет на странице.
IO
Проблема легче понять, много компонентов, которые нужно ждать некоторой сетевой задержки, так как может возникнуть задержка в сети, снижая его восприятие задержки сети? Что нам нужно решить проблему.
Хорошо, мы только что закончили говорить о том, почемуReact
Все основные стратегии оптимизации во время выполнения, а также основные проблемы, решаемые во время выполнения, рассмотрим их подробно.React
Какие обновления и изменения были внесены в эти последние основные версии.
React 15 — Полуавтоматическая пакетная обработка
Давайте сначала посмотримReact 15
,React
Вот после этой версии и начался пожар, а после этой версии,React
Обновления также становятся медленнее.
Архитектура
Структура этой версии относительно проста, в основном разделена наReconciler
а такжеRenderer
две части.
-
Reconciler
(координатор) - ответственный за звонкиrender
Сгенерируйте виртуальный DOM для DIFF, узнайте виртуальный DOM после изменения -
Renderer
(рендерер) - отвечает за получениеReconciler
Уведомлять, визуализировать измененные компоненты в текущей среде хоста, например в браузере, разные среды хоста будут иметь разные значения.Renderer
.
пакетная обработка
Теперь давайте рассмотрим,React 15
Введена оптимизация: партия, однаReact
Классическое интервью: «SetState синхронизирован или асинхронен» исходит из этого, я часто спрашиваю, когда беру интервью, я представил это в статье двухлетней давности:Изучение практических проблемsetState
исполнительный механизм.
Например, следующий код вызывается четыре раза за жизненный цикл.setState
, последние два из которых находятся вsetTimeout
в обратном вызове.
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);
}, 0);
}
render() {
return null;
}
};
Рассмотрим два случая:
- Предположение
React
Механизм пакетной обработки вообще отсутствует, тогда выполнитеsetState
Рендеринг страницы будет запущен немедленно, и порядок печати должен быть1、2、3、4
- Предположение
React
Существует идеальный механизм пакетной обработки, поэтому весь рендеринг должен обрабатываться единообразно после выполнения всей функции, а порядок печати должен быть0、0、0、0
На самом деле порядок печати приведенного выше кода в этой версии такой:0、0、2、3
,отsetTimeout
Мы можем увидеть результат печати в обратном вызове,setState
Сам вызов является синхронным, и причина, по которой извне не может получить результат немедленно, заключается в механизме пакетной обработки React.
просто такsetState
является синхронным, когда запускается несколько раз одновременноsetState
Браузер всегда будет заблокирован потоком JS, затем браузер будет пропускать кадры, что приведет к зависанию страницы, поэтомуReact
Был введен пакетный механизм, в основном для объединения обновлений, запущенных в одном контексте, в одно обновление.
Мы можем посмотреть исходный код_processPendingState
Эта функция, эта функция используется для объединенияstate
Временная очередь и, наконец, вернуть объединенныйstate
.
_processPendingState: function (props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = _assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;
},
Нам просто нужно сосредоточиться на следующем коде:
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
Если объект передается, он, очевидно, будет объединен один раз:
Object.assign(
nextState,
{index: state.index+ 1},
{index: state.index+ 1}
)
Если передается функция, параметры функцииpreState
является результатом предыдущего слияния, поэтому результат вычисления является точным.
Если в среде, где требуется пакетная обработка (React
жизненный цикл, синтетические события) сколько бы раз его не вызывалиsetState
, не будет выполнять обновление, но обновитstate
депозит_pendingStateQueue
, сохраните компонент, который нужно обновить, вdirtyComponent
. При выполнении последнего механизма обновления, на примере жизненного цикла, все компоненты, то есть компонент верхнего уровняdidmount
позжеisBranchUpdate
Установите значение «ложь». В это время накопленный ранееsetState
.
React
внутренне черезbatchedUpdates
Функция вызывает все функции, которые необходимо пакетировать, логика выполнения примерно следующая:
batchedUpdates(onClick, e);
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
// ...
try {
return fn(a);
} finally {
// ....
}
}
потому чтоbatchedUpdates
Он вызывается синхронно.Если внутри fn есть асинхронное выполнение, пакетная обработка уже была выполнена, поэтому эта версия пакетной обработки не может обрабатывать асинхронные функции, также известные как полуавтоматическая пакетная обработка.
И что,React
предоставил намunstable batchedUpdates
Такие функции позволяют выполнять пакетную обработку вручную.
Реагировать на 15 недостатков
Хотя вReact 15
Внедрить логику оптимизации, такую как пакетная обработка, но из-заReact 15
Сама архитектура обновляется синхронно и рекурсивно, если узлов много, даже если только один разstate
изменять,React
Он также требует сложных рекурсивных обновлений: как только обновление начнется, оно не может быть прервано на середине, а основной поток не может быть освобожден до тех пор, пока не будет пройдено все дерево.
Мы можем сослаться на этот пример на рисунке, когда иерархия глубокая, время рекурсивного обновления превышает16ms
, если в это время выполняется пользовательская операция или рендеринг анимации, она будет отображаться как зависшая.
React 16 — Делаем параллельный режим возможным
Архитектура
Далее давайте посмотрим наReact 16
Эта версия по сравнению сReact 15
, мы видим, что в новой архитектуре есть еще один слойScheduler
, то есть планировщик, а затем вReconciler
Этот слой, используйтеFiber
Архитектура была переработана. Конкретные детали будут представлены в последующих главах.
-
Scheduler
(Планировщик) - Приоритет задач планирования, задачи с высоким приоритетом вводятся первымиReconciler
-
Reconciler
(координатор) — компонент, отвечающий за поиск изменений (с помощьюFiber
рефакторинг) -
Renderer
(рендерер) — отвечает за отрисовку измененных компонентов на страницу
React
, и в последующих основных версиях использовалась эта архитектура.
Помимо изменений в архитектуре,React
В этом издании представлена очень важная концепция,Concurrent Mode
.
Concurrent Mode
React
Официальное описание выглядит следующим образом:
Параллельный режим — это набор новых функций React, которые помогают приложениям оставаться отзывчивыми и соответствующим образом настраиваются в зависимости от производительности устройства пользователя и скорости Интернета.
Чтобы поддерживать отзывчивость приложения, нам нужно понять, что мешает приложению оставаться отзывчивым?
Ключевым моментом является сохранение отклика приложения.Сначала мы можем подумать о том, что ограничивает отклик приложения?
В предыдущем разделе мы также упомянули, что основным узким местом во время выполнения являетсяCPU、IO
, если эти два узких места удастся устранить, приложение сможет оставаться отзывчивым.
существуетCPU
On, наша главная проблема в том, что выполнение более16.6 ms
, страница зависнет, затемReact
Решение состоит в том, чтобы зарезервировать некоторое время для потока JS во время каждого кадра браузера,React
Используйте это время для обновления компонентов. Когда зарезервированное время недостаточно,React
Управление потоком обратно в браузер, чтобы у него было время, чтобы сделать пользовательский интерфейс,React
Затем дождитесь следующего кадра, чтобы продолжить прерванную работу.
На самом деле, как мы упоминали выше, эта операция разделения длинных задач на каждый кадр и выполнение небольшой задачи в каждом кадре — это то, что мы часто называем временными срезами.
Затем на IO проблема, которую необходимо решить, заключается в том, что после отправки сетевого запроса он не может быстро ответить, потому что ему нужно дождаться возврата данных перед дальнейшей операцией.React
Я надеюсь решить эту проблему, контролируя приоритет рендеринга компонентов.
Фактически,Concurrent Mode
Это новый набор архитектуры, предназначенный для решения двух вышеперечисленных проблем: сделать рендеринг компонентов «прерываемым» и иметь «приоритет», который включает в себя несколько разных модулей, каждый из которых отвечает за разные задачи.
Во-первых, давайте посмотрим, как сделать рендеринг компонента «прерываемым»?
Основная архитектура - волокно
В предыдущей главе мы говорили оReact15
изReconciler
Он выполняется рекурсивным образом, а данные хранятся в стеке рекурсивных вызовов, этот метод рекурсивного обхода точно не прерывается.
так,React
Потребовалось 2 года, чтобы реконструировать архитектуру Fiber,React16
изReconciler
на основеFiber
Реализация узла. каждыйFiber
Узел соответствуетReact element
, обратите внимание, что это соответствие, а не равенство. мы называемrender
Результат, полученный функцией,React element
,а такжеFiber
узел, поReact Element
созданный.
Ниже приведенFiber
Пример узла, за исключением того, что он содержит тип компонента, соответствующий компонентуDOM
Помимо информации,Fiber
Узел также сохраняет состояние, измененное компонентом в этом обновлении, работу, которую необходимо выполнить, нужно ли его удалить, вставить на страницу или обновить.
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 动态工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
Кроме того, мы также можем видеть связь между текущим узлом и другими узлами, т.е.Fiber
узел включает егоchild
(первый дочерний узел),sibling
(родственный узел),return
(родительский узел) и другие свойства.
двойной кеш
существуетReact
Одновременно может быть не более двух волоконных деревьев. Дерево волокон, соответствующее содержимому, отображаемому на текущем экране, называетсяcurrent Fiber
дерево, строящееся в памяти дерево Fiber называетсяworkInProgress Fiber
дерево, они проходятalternate
ссылка на недвижимость.
Корневой узел приложения React будет использоватьcurrent
указатель на текущийcurrent Fiber
Дерево.
когдаworkInProgress Fiber
После того, как построение дерева завершено и передано рендереру для рендеринга на странице, корневой узелcurrent
указатель будет указывать наworkInProgress Fiber
дерево, на этот разworkInProgress Fiber
дерево становитсяcurrent Fiber
Дерево.
из-заReact
Изменен механизм рендеринга дерева DOM на дваFiber
Дерево работает попеременно, поэтому мы можем переключить указатель после завершения обновления, а отказаться от модификации на другое дерево мы можем в любой момент до переключения указателя. Это позволяет прервать обновление.
Выше мы упомянули несколько концепций,current Fiber
,workInProgress Fiber
, объект jsx, которыйReact Element
и настоящие узлы DOM.
Так,Reconciler
Задача состоит в том, чтобы использовать алгоритм Diff для сравненияcurrent Fiber
а такжеReact Element
, сгенерироватьworkInProgress Fiber
, эта фаза прерываема,Renderer
работа заключается в том, чтобы поставитьworkInProgress Fiber
Преобразован в настоящий узел DOM.
Планировщик - Планировщик
Если мы все еще используемReactDOM.render
работать синхронноFiber
структуру, тоFiber
Архитектура та же, что и до рефакторинга. Но когда мы сотрудничаем с упомянутым выше разделением времени, мы можем назначить время выполнения для каждой единицы работы в соответствии с текущей производительностью среды хоста, чтобы достичь «асинхронного прерываемого обновления».
Scheduler
Это может помочь нам завершить это дело.Мы видим, что наша задача обновления, которая занимает много времени, разбита на маленькие кусочки. Это оставляет браузеру время для выполнения компоновки стилей и отрисовки стилей, уменьшая вероятность пропущенных кадров.
Анимационный эффект на картинке тоже стал очень шелковистым.
requestIdelCallback
На рисунке выше, есть некоторые вещи, которые браузер делает в одном кадре. Здесь мы можем видеть, что когда все сделано, вызов сделанrequestIdleCallback
Функция, в этой функции мы можем получить оставшееся время текущего кадра браузера.
что этоAPI
Для чего это можно использовать? Давайте посмотрим на пример:
Если у нас есть такая очень долгая трудоемкая задача в левом коде, которую нужно выполнить, без другой дополнительной обработки, то время выполнения для выполнения всей задачи должно быть более16.6ms
из.
Здесь мы используемrequestIdleCallback
Эта функция может разделить большую задачу на несколько небольших задач и постепенно выполнять небольшие задачи, когда в каждом кадре есть свободное время.
с этимAPI
, мы можем заставить браузер выполнять скрипт только в периоды простоя. Суть квантования времени, то есть реализации симуляцииrequestIdleCallback
эта функция.
Из-за проблем с совместимостью и частотой обновления кадров,
React
не используется напрямуюrequestIdleCallback
, вместо использованияMessageChannel
Реализация моделирования, принцип тот же.
прерывание обновления
существуетReact
изrender
сцена, включиConcurrent Mode
, перед каждым обходом он будет проходить черезScheduler
который предоставилshouldYield
Способ определяет, необходимо ли прервать обход, чтобы браузер успел представлять, обратитесь к следующемуworkLoopConcurrent
функция.
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
Наиболее важным моментом для определения того, прервана ли она, является то, израсходовано ли оставшееся время каждой задачи Роль функции shouldYield() состоит в том, чтобы проверить, истекло ли время.
shouldYield(...) --> Scheduler_shouldYield(...) --> unstable_shouldYield(...)
--> shouldYieldToHost(...)
--> getCurrentTime() >= deadline
-->
var yieldInterval = 5; var deadline = 0;
var performWorkUntilDeadline = function() {
...
var currentTime = getCurrentTime()
deadline = currentTime + yieldInterval
...
}
Видно, что вSchdeduler
, то каждый раз, когда он истечет, он будет выскакивать из рабочего цикла, отдавать управление потоком браузеру, а затем продолжать текущую работу для следующей задачи. Таким образом, длинныйJS
Задача будет разбита на несколько подзадач.
Ниже мы можем посмотреть на этот код,yieldInterval
будет основано на текущем устройствеfps
Выполнять динамические вычисления, которые соответствуют тому, что мы упоминали ранее.Concurrent Mode
Определение этой концепции помогает приложениям оставаться отзывчивыми и корректироваться соответствующим образом в зависимости от производительности устройства пользователя и скорости Интернета.
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
// reset the framerate
yieldInterval = 5;
}
Fiber
Координация архитектурыScheduler
ДостигнутоConcurrent Mode
Нижележащий слой — «асинхронные прерываемые обновления».
isInputPending
Ну, теперь мы на самом деле не просто используемReact
может наслаждаться этой стратегией оптимизации.
существуетChrome 87
Версия,React
Команда работала с командой Chrome, чтобы добавить новый API в браузер.isInputPending
. Это также первый API, использующий концепцию прерываний операционной системы для веб-разработки.
даже если не пользуюсьReact
, мы также можем использовать этоAPI
, чтобы сбалансироватьJS
Приоритет между выполнением, отрисовкой страницы и пользовательским вводом.
Мы можем посмотреть на приведенный выше пример кода, используя разумныйisInputPending
метод, мы можем своевременно реагировать на ввод пользователя при отображении страницы, а когда требуется выполнить длительную JS-задачу, мы можем передатьisInputPending
чтобы прервать выполнение JS и вернуть управление браузеру для выполнения ответа пользователя.
приоритетное управление
Если обновление прерывается в середине выполнения, а затем перезапускается новое обновление, мы можем сказать: последнее обновление прервало предыдущее обновление.
Чтобы привести простой пример, мы сейчас едим, и вдруг вам звонит ваша девушка, возможно, вам придется прервать операцию приема пищи, ответить на звонок, а затем продолжить прием пищи.
Другими словами, приоритет ответа на телефонный звонок выше, чем приоритет приема пищи. Согласно результатам исследования взаимодействия человека с компьютером, React отдает разные приоритеты обновлениям статуса, генерируемым в разных сценариях, например:
- Если это метод жизненного цикла: он имеет наивысший приоритет и выполняется синхронно.
- Контролируемый пользовательский ввод: например, ввод текста в поле ввода и его синхронное выполнение.
- Некоторые интерактивные события, такие как анимация, выполняются с высоким приоритетом.
- Другие: например, запросы данных или использование
suspense
,transition
Такие обновления выполняются с низким приоритетом.
Например, мы посмотрим на цифры этих двух обновлений: прежде всего, у нас есть изменение такого текущего обновления темы, это обновление его, низкого приоритета и потребляемого времени. Итак, тема меняет это обновление состоянияrender
Когда этап не завершен, пользовательInput
В поле вводится новый символ.
Приоритет операции пользовательского ввода относительно высок, в это время мы сначала прервем операцию обновления темы, отдадим приоритет реагированию на пользовательский ввод, а затем продолжим обновление темы на основе результатов предыдущего обновления.render
и зафиксировать процесс.
Это операция, при которой задача с высоким приоритетом прерывает задачу с низким приоритетом. Далее давайте посмотрим наReact
В исходном коде как реализован приоритет?
приоритет задачи
Давайте сначала взглянем на этот код, который объявляет пять разных приоритетов:
- ImmediatePriority представляет приоритет немедленного выполнения, самый высокий уровень
- UserBlockingPriority: приоритет, представляющий уровень блокировки пользователя.
- NormalPriority: это наиболее распространенный нормальный приоритет.
- LowPriority: представляет более низкий приоритет
- IdlePriority: самый низкий приоритет, указывающий, что задача может простаивать.
существуетReact
Внутренне, везде, где задействовано приоритетное планирование, оно будет использоватьсяrunWithPriority
Эта функция, эта функция принимает приоритет и функцию обратного вызова, при внутреннем вызове этой функции обратного вызова метод получения приоритета получит приоритет, переданный в качестве первого параметра.
Итак, как эти различные переменные приоритета влияют на конкретные задачи обновления?
Мы можем посмотреть на приведенный выше код, через разные переменные приоритета, мы будем вычислять время истечения разной длины.expirationTime
. Каждая задача обновления будет иметь одинexpirationTime
, чем ближе время истечения задачи к текущему времени, тем выше приоритет задачи.
Так,expirationTime
, КstartTime
То есть текущее время плюсtimeout
полученный. НапримерImmediatePriority
Соответствующий тайм-аут равен -1, тогда время истечения этой задачи короче текущего времени, что указывает на то, что она истекла и должна быть выполнена немедленно.
Тогда мы всеReact
Что касается приложения, то одновременно могут генерироваться разные задачи, нашаScheduler
Что ж, это поможет нам сначала найти задачу с наивысшим приоритетом и запланировать ее обновление. Итак, как мы можем найти высокоприоритетные задачи как можно быстрее?
Фактически,Scheduler
Все задачи, которые готовы и могут быть выполнены, хранятся вtaskQueue
В очереди, и эта очередь использует структуру данных малой верхней кучи. В малой верхней куче все задачи располагаются по времени истечения задачи, от маленькой к большой, так чтоScheduler
Требуется только сложность O (1), чтобы найти самую раннюю просроченную задачу или задачу с наивысшим приоритетом в очереди.
Приоритет волокна
Итак, механизм приоритетов, о котором мы только что говорили, на самом делеReact
изScheduler
Механизм приоритетаReact
внутренний,Scheduler
Это независимый пакет, он отвечает только за планирование задач, и ему все равно, для чего эта задача, даже если мы будемScheduler
вырватьсяReact
Также можно использовать.
такScheduler
Внутренний механизм приоритета также не зависит отReact
Да, у React тоже есть свой механизм приоритетов, потому что нам нужно знать, что вFiber
на деревьях, которыеFiber
И какие объекты обновления имеют высокий приоритет.
существуетReact16
середина,Fiber
а такжеUpdate
Приоритет задачи аналогичен приоритету задачи.React
даст каждой операции приоритет в соответствии с различнымиFiber
узлаUpdate
добавить одинexpirationTime
.
Но из-за некоторых проблем,React
Уже тутFiber
больше не используется вexpirationTime
Чтобы выразить приоритет, мы поговорим об этом позже.
изменения жизненного цикла
в новомReact
В архитектуре визуализация компонента делится на две фазы: первая фаза (также называемаяrender
стадия) может бытьReact
Прерванный, однажды прерванный, все сделанное на этом этапе отбрасывается, когдаReact
После решения срочных вопросов компонент все равно будет перерендерен, в это время будет переделана работа первого этапа.
Второй этап называетсяcommit
Этап, когда он начнет, нельзя прервать, то есть работа второго этапа будет непосредственно заканчиваться рендерингом этого компонента.
Точка разделения двух фаз равнаrender
функция.render
Все функции жизненного цикла, предшествующие функции (включаяrender
) относятся к первому этапу, а все последующие ко второму этапу. включиConcurrent Mode
Позже,render
Все предыдущие жизненные циклы могут быть прерваны или вызваны повторно:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
Если мы введем побочные эффекты в эти повторные выполнения в середине жизни, это может привести к непредсказуемым проблемам с нашей программой, так что вотReact v16.3
,React
Просто введите новую функцию жизненного циклаgetDerivedStateFromProps
, этот жизненный цикл является статическим методом, в котором его вообще нельзя передатьthis
Для доступа к текущему компоненту вход может быть только через параметры, а влияние на отрисовку компонента может быть только через возвращаемое значение.
так,getDerivedStateFromProps
должна быть чистой функцией,React
Требование таких чистых функций вынуждает разработчиков приспосабливатьсяConcurrent Mode
.
Что, после вышеуказанного прерывания и приоритета реализации,React
Уже можно заставить программу преодолеть проблему с процессором и сохранить ее отзывчивость, так что насчет проблемы с вводом-выводом?
Suspense
React 16.6
недавно добавленный<Suspense>
Компонент, который в основном решает проблему ввода-вывода во время выполнения.
Suspense
Компонент можно заставить «ждать» асинхронной операции, пока асинхронная операция не завершится перед рендерингом. Мы можем обратиться к следующему коду, мы передаемSuspense
Реализует ленивую загрузку компонента.
const MonacoEditor = React.lazy(() => import('react-monaco-editor'));
<Suspense fallback={<div>Editor Loading...</div>}>
<MonacoEditor
height={500}
language="json"
theme="vs"
value={errorFileContext}
options={{}}
/>
</Suspense>
так зачем говоритьSuspense
Можно ли решить проблему IO? Мы также можем реализовать эту ленивую загрузку другими способами.
использоватьSuspense
, мы можем уменьшить приоритет состояния загрузки и уменьшить проблему заставки. Например, когда данные возвращаются в ближайшее время, мы можем напрямую отображать статус загрузки, а не отображать его, чтобы избежать заставки; если тайм-аут не возвращается, статус загрузки будет отображаться явно. По сутиSuspense
Поддерево компонентов в дереве компонентов имеет более низкий приоритет, чем остальная часть дерева компонентов.
Мы можем представить, что без Suspense нам, возможно, придется реализовать его самостоятельно.loading
, то этоloading
Он имеет тот же приоритет, что и рендеринг других компонентов, в настоящее время независимо от того,IO
Независимо от того, насколько быстро, наш экран будет мерцать.
то если запросIO
В течение этого времени мы используем это время для загрузки других компонентов, пока время достаточно мало, нам не нужно показыватьLodaing
, что может уменьшить проблему заставки.
Конечно,Suspense
Роль не только в этом, важнее оптимизироватьReact
Проблема внутреннего ожидания метода записи асинхронной операции, об этом мы здесь говорить не будем.
Реагировать на 16 недостатков
несмотря на то чтоReact 16
Основная работа выполняетсяConcurrent Mode
, но это не значитConcurrent Mode
можно стабильно использовать,React 16
Выполнение всей этой работы просто делаетConcurrent Mode
называется возможным, а вConcurrent Mode
Я сделал несколько небольших попыток.В версии 16 синхронный режим рендеринга все еще используется по умолчанию, ради масштабного открытия в будущем.Concurrent Mode
, у него еще много работы.
React 17 — переходная версия стабильного параллельного режима
Нет новых функций?
Мы видим, чтоReact17
В журнале изменений практически нет новых функций, но из единственных официальных описаний, которые мы можем найти:React17
Является переходной версией для стабилизации СМ.
из-заConcurrent Mode
принесBreaking Change
Это сделает многие библиотеки несовместимыми, и мы не все сможем использовать их в новых проектах, поэтомуReact
Он предоставляет нам поддержку сосуществования нескольких версий одного проекта, и еще одна очень важная поддержка: использованиеLanes
рефакторингCM
алгоритм приоритета.
Сосуществование нескольких версий
Кратко поговорим о сосуществовании нескольких версий.
React
Он принимает метод делегирования событий, который реализует набор механизмов событий и имитирует процесс всплытия и захвата событий, в основном для сглаживания проблем совместимости различных браузеров.
Например, он не включает их, когда вы их объявляете.attach
соответствоватьDOM
на узле. Наоборот,React
будет прямо вdocument
для каждого типа события на узлеattach
процессор. Этот подход не только дает преимущества в производительности на больших деревьях приложений, но и упрощает добавление новых функций.
Но если их больше одного на страницеReact
версия, они все будут вdocument
Зарегистрируйтесь на мероприятие. Это нарушит механизм всплытия событий, внешнее дерево все равно получит событие, что делает вложенность разных версийReact
трудно выполнить.
ЭтоReact
изменитьattach
событие дляDOM
Причина базовой реализации.
существуетReact 17
середина,React
поместит событиеattach
прибытьReact
корень дерева визуализацииDOM
контейнер вместоattach
прибытьdocument
Уровень :
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
Это позволяет сосуществовать нескольким версиям.
Новый алгоритм приоритета - полосы
Мы упоминали выше,Scheduler
Приоритеты в React несовместимы с приоритетами в React. React 16
До,React
существуетFiber
также используется вexpirationTime
указывает приоритет, но вReact 17
середина,React
использоватьLanes
Переработан алгоритм приоритета Fiber.
Ну и предыдущийexpirationTime
В чем проблема? существуетexpirationTime
Когда он был впервые разработан,React
В системе нет концепции асинхронного рендеринга Suspense. Если сейчас такой сценарий: есть 3 задачи, их приоритеты A > B > C, обычно их нужно выполнять только в порядке приоритета.
Но теперь возникла ситуация, когда задачи A и C привязаны к процессору, а задачи B — к вводу-выводу (Suspense
вызовет удаленный API, который является задачей ввода-вывода), то естьA(cpu) > B(IO) > C(cpu)
, в данном случае высокий приоритетIO
Прерывания задач с низким приоритетомCPU
Задача, очевидно, неразумная.
затем используйтеexpirationTime
, он использует определенный приоритет в качестве стандарта обновления приоритета всего дерева, а не конкретного компонента.На данный момент наше требование состоит в том, чтобы отделить задачу B от пакета задач и обработать ее первойcpu
Задания А и С, если пройденоexpirationTime
Это сложно реализовать, трудно выразить концепцию пакета и трудно отделить одну задачу от пакета задач, в настоящее время нам нужен более детальный приоритет.
Так,Lanes
появился. использовался раньшеexpirationTime
Указанные поля изменены наlane
. Например:
update.expirationTime -> update.lane
fiber.expirationTime -> fiber.lanes
Lane
а такжеLanes
отношение между единственным и множественным числом, представляющее одну задачу, определяется какLane
, представляющие несколько задач, определяются какLanes
.
Lane
Тип , определяется как бинарная переменная.Таким образом, когда мы делаем расчет приоритета, мы используем битовые операции.При частом обновлении он занимает меньше памяти и вычисляется быстрее.
React определяет в общей сложности 18 типовLane/Lanes
Переменные, каждая переменная занимает 1 или более битов, каждаяLane/Lanes
имеют соответствующие приоритеты.
В коде мы можем обнаружить, что чем ниже приоритетlanes
Чем больше бит занято. НапримерInputDiscreteLanes
(то есть приоритет дискретного взаимодействия) занимает 2 бита,TransitionLanes
9 мест. Причина в том, что чем ниже приоритет обновления, тем легче его прервать (если все дорожки с текущим приоритетом заняты, текущий приоритет будет понижен на один приоритет), что приводит к отставанию, поэтому больше битов необходимы. И наоборот, наиболее оптимально синхронноSyncLane
нет необходимости в дополнительномlanes
.
React 18 — более гибкий одновременный рендеринг
Совсем недавно,React
Альфа-версия 18 была выпущена из-заConcurrent Mode
Огромный перерыв, вызванный изменением,React
Его пока нельзя включить по умолчанию. так,React
Просто измените имя.Concurrent Rendering
Механизм параллельного рендеринга.
существуетReact 17
В версии React уже поддерживается сосуществование нескольких версий, поэтому React рекомендует поэтапные обновления, а не один размер подходит всем. Только обновления, вызванные этими новыми функциями, будут включать параллельный рендеринг, поэтому вы можете использовать его без изменения большого количества кода.React 18
, вы можете попробовать эти новые функции в своем собственном темпе.
createRoot
React
дает нам три режима, те, которые мы использовали раньшеReactDOM.render
Созданное приложение принадлежитlegacy
, в этом режиме обновление все еще синхронно, один разrender
этап соответствует один разcommit
сцена.
При использованииReactDOM.createRoot
В созданном приложении по умолчанию включен параллельный рендеринг, это видно наReact 18
,createRoot
Эта функция больше неunstable
.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const container = document.getElementById('root');
// Create a root.
const root = ReactDOM.createRoot(container);
// Render the top component to the root.
root.render(<App />);
Кроме того, существуетcreateBlockingRoot
функция созданаblocking
режим, эта функция нам удобна для перехода между двумя вышеуказанными режимами.
Ниже мы также можем увидеть сравнение функций, поддерживаемых разными режимами.
Пакетная оптимизация
Мы упоминали выше, что вReact 15
середина,React
Реализован механизм пакетной обработки первой версии. Если мы инициируем несколько обновлений в обратном вызове события, они будут объединены в одно обновление для обработки.
Основная причинаbatchedUpdates
Сама эта функция вызывается синхронно, еслиfn
Существует внутреннее асинхронное выполнение, и пакет уже был выполнен, поэтому эта версия пакета не может обрабатывать асинхронные функции.
Но когдаReact
Здесь у нас есть много обновлений состояния для асинхронных обратных вызовов, поэтому вReact 18
Ну а если включить асинхронный рендеринг, то эту проблему можно решить.
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);
}, 0);
}
render() {
return null;
}
};
существуетConcurrent
В режиме обновления объединяются в приоритетном порядке.
Мы видим, что окончательный результат нашего предыдущего кода стал0、0、1、1
, почему этот вывод? Давайте кратко рассмотрим, как выглядит пакетная обработка на основе приоритетов:
Соответствие компонентамfiber
устанавливатьupdate
После этого он войдет в «процесс планирования». Мы также упоминали вышеScheduler
Роль планирования заключается в выборе другого приоритетаupdate
Тот, у кого наивысший приоритет, входит в процесс обновления с этим приоритетом. Процесс после входа в расписание примерно следующий:
Во-первых, мы вынимаем самый высокий приоритет всех актуальных приоритетовLane
, то согласноLane
Получите приоритет, который необходимо запланировать на этот раз.
Затем нам нужно получить, есть ли предыдущее расписание перед выполнением формального процесса обновления, и если да, то сравнить его с приоритетом этого расписания.
Если это первое исполнениеsetState
,этоexistingCallbackPriority
Его точно нет, так что первый выезд обновит процессperformConcurrentWorkOnRoot
пройти черезscheduleCallback
Планирование.
но второй разsetState
Заходи, потому что раньше было расписание, и оно согласуется с локальным приоритетом, оно будет напрямуюreturn
, больше не звонитscheduleCallback
правильноperformConcurrentWorkOnRoot
Расписание.
Затем через определенный промежуток времени все предыдущие обновления одного приоритета вместе войдут в формальный процесс обновления. из-за последнегоsetState
вsetTimeout
называется в,setTimeout
При более низком приоритете все будет выполнено в следующем пакете, поэтому окончательный результат печати0、0、1、1
.
Выше приведен процесс автоматической пакетной обработки на основе приоритета. При таком процессе нам не нужноReact
предоставил намunstable_batchedUpdates
Это ручная пакетная функция.
startTransition
Далее давайте посмотрим React 18
Добавлен новый API:startTransition
:
этоAPI
Это позволяет нам вручную различать несрочные обновления состояния, что по сути является контролем приоритета рендеринга компонентов. Вот, например, сейчас такая сцена: мы идемInput
Поле вводит значение, а затем необходимо предоставить некоторые данные, отфильтрованные по введенному нами значению.
Поскольку вам нужно каждый раз динамически отображать отфильтрованное значение, вы можете сохранить входное значение в состоянии.Ваш код может выглядеть следующим образом:
setInputValue (input) ;
setSearchQuery (input) ;
Прежде всего, значение, введенное пользователем, должно быть отображено немедленно, но отфильтрованные данные ассоциации могут не нуждаться в такой быстрой визуализации.Если мы не делаем никакой дополнительной обработки, вReact 18
Раньше все обновления отображались немедленно.Если у вас много исходных данных, объем вычислений, которые вам нужно выполнять каждый раз, когда вы вводите новое значение (отфильтровывая подходящие данные на основе входного значения), очень велик, поэтому каждый раз может быть задержка после ввода пользователем.
Поэтому в прошлом мы могли добавлять некоторые операции защиты от сотрясений, чтобы искусственно задерживать вычисление и рендеринг отфильтрованных данных.
новыйstartTransition API
позволяет нам маркировать данные какtransitions
условие.
import { startTransition } from 'react';
// Urgent: Show what was typed
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setSearchQuery(input);
});
все вstartTransition
Обновления в обратном вызове считаются несрочной обработкой, если есть более срочное обновление (например, пользователь снова вводит новое значение), указанное выше обновление будет прервано, и обновление не будет продолжаться до тех пор, пока не появятся другие срочные операции.
Как насчет того, что это элегантнее, чем мы искусственно внедряем анти-шейк 😇
в то же время,React
также предоставляет намisPending
Хук для флагов перехода:
import { useTransition } from 'react' ;
const [ isPending , startTransition ] = useTransition ( ) ;
Вы можете использовать это в сочетании с некоторыми анимациями загрузки:
{ isPending && < Spinner / > }
Ниже приведен более типичный пример:
Перетаскивание ползунка слева изменит количество отображаемых узлов в дереве. Перетаскивание верхнего ползунка изменяет угол наклона дерева. Вверху есть радар кадров, который может отображать пропадание кадров в процессе обновления в режиме реального времени. когда не нажимаешьUse startTransition
кнопку, перетащите ползунок вверху. Видно, что перетаскивание не плавное, а радар кадров вверху показывает пропущенные кадры.
В это время мы ставимtree
изrender
помещатьstartTransition
в, хотяtree
Обновление все еще очень лагает, но радар не пропускает кадры.
startTransition
Реализация на самом деле очень проста, все вstartTransition
Операция, выполняемая в обратном вызове, получитisTransition
Тег, согласно этой метке, будет обновлять React с более низким приоритетом.
useDeferredValue
Помимо ручной маркировки приоритета определенных операций, мы также можем отметить приоритет определенного состояния.React 18
дает нам новый крючокuseDeferredValue
.
Например, сейчас у нас есть такая сцена. После того, как пользователь вводит некоторую информацию, нам нужно выполнить некоторую обработку информации и отобразить ее в следующих деталях. Если эта обработка занимает много времени, непрерывный ввод пользователя будет казаться застрявшим. Мы можем посмотреть на этот пример на рисунке, на самом деле все входы являются непрерывными входами.
На самом деле нам нужен быстрый ответ на ввод пользователя, но на самом деле это не имеет значения, если рендеринг приведенных ниже деталей займет некоторое время.
В это время мы можем пройтиuseDeferredValue
СоздаватьdeferredText
, что на самом деле означаетdeferredText
Рендер помечен как низкоприоритетный, и у него есть еще один параметр — максимальное время задержки для этого рендера. Мы можем приблизительно предположить, чтоuseDeferredValue
Механизм реализации должен соответствоватьexpairedTime
похож.
На рисунке видно, что пользовательский ввод больше не будет ощущаться как зависший.
Так в чем же разница между ним и нашим ручным антивибратором?
Основная проблема со стабилизацией заключается в том, что независимо от того, насколько быстро наш компьютер ее рендерит, она имеет фиксированную задержку, иuseDeferredValue
Ну, это задержит приоритет только тогда, когда рендеринг занимает много времени, в большинстве случаев лишней задержки не будет.
Поддержка ленивой загрузки в SSR
Наконец, этоSuspense
Теперь, до React 18, режим SSR не поддерживается для использованияSuspense
компонент, а в React 18 серверные компоненты также поддерживают использование<Suspense>
сейчас: если вы завернете компонент в<Suspense>
, сервер сначалаfallback
компоненты в качествеHTML
потоковая передача после загрузки основного компонента,React
отправит новыйHTML
для замены всего компонента.
<Layout>
< Article />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</Layout>
Например, приведенный выше код,<Article>
Компоненты будут сначала визуализированы,<Comments>
компоненты будутfallback
заменить<Spinner>
. однажды<Comments>
После загрузки компонентаReact
только отправит его в браузер, заменив<Spinner>
компоненты.
наконец
Наконец, если вы хотите прочитать исходный код React, не рекомендуется сразу переходить к нему, потому что некоторые коды действительно трудны для понимания.
Рекомендуется прочитать наброски следующих двух руководств, сначала понять разделение общей архитектуры исходного кода, затем фактически выполнить отладку и пройти весь процесс и, наконец, ввести каждый модуль для целевого чтения в соответствии с вашими потребностями. .
- Исходный код графического React:GitHub.com/7Opening/реагировать-…
- Технология React показала:react.iamkasong.com/
Интерфейс Douyin срочно нуждается в талантах. Если вы хотите присоединиться к нам, пожалуйста, добавьте меня в WeChat и свяжитесь со мной.ConardLiСвяжитесь со мной, и статья впервые будет опубликована в моем публичном аккаунте WeChat.кодSecret GardenВы также можете обратить внимание.
Если в статье есть какие-либо ошибки, пожалуйста, оставьте сообщение со мной в области комментариев.Если эта статья поможет вам, пожалуйста, поставьте лайк и подпишитесь. Ваши лайки и внимание - лучшая поддержка для меня!