Эволюция оптимизации времени выполнения React

внешний интерфейс JavaScript React.js
Эволюция оптимизации времени выполнения React

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

Всего два месяца назад,ReactТолько что вышла версия 18, не забудьте прочитать ее подробно в прошлый разReactИсходный код, или три года назад, читалReactВ 15-й версии я в основном изучал механизм рендеринга виртуального DOM, механизм выполнения setState иReactСинтетические события , также написал следующие статьи о React:

После того, как я закончил свое исследование, я не мог не вздохнуть, и я никогда не буду читать это снова.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, если эти два узких места удастся устранить, приложение сможет оставаться отзывчивым.

существуетCPUOn, наша главная проблема в том, что выполнение более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 бита,TransitionLanes9 мест. Причина в том, что чем ниже приоритет обновления, тем легче его прервать (если все дорожки с текущим приоритетом заняты, текущий приоритет будет понижен на один приоритет), что приводит к отставанию, поэтому больше битов необходимы. И наоборот, наиболее оптимально синхронно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, не рекомендуется сразу переходить к нему, потому что некоторые коды действительно трудны для понимания.

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

Интерфейс Douyin срочно нуждается в талантах. Если вы хотите присоединиться к нам, пожалуйста, добавьте меня в WeChat и свяжитесь со мной.ConardLiСвяжитесь со мной, и статья впервые будет опубликована в моем публичном аккаунте WeChat.кодSecret GardenВы также можете обратить внимание.

Если в статье есть какие-либо ошибки, пожалуйста, оставьте сообщение со мной в области комментариев.Если эта статья поможет вам, пожалуйста, поставьте лайк и подпишитесь. Ваши лайки и внимание - лучшая поддержка для меня!