Это четвертая статья моего анализа исходного кода React, предыдущие статьи были посвящены подробному анализу кода, но я думаю, что этот метод может быть не очень хорошим. Поэтому, начиная с этой статьи, я планирую вынести содержание, изученное в исходном коде, в отдельную статью, которая может быть более дружелюбной к читателям.
Информация, связанная со статьей
- Исходный код React 16.8.6 Китайские комментарии, если вы хотите прочитать исходный код, но боитесь его не понять, вы можете изучить его через мой репозиторий
- Сборник всех предыдущих статей
Зачем нужно расписание?
Мы все знаем, что JS-движок рендеринга — это взаимоисключающие отношения. Если выполняется JS-код, то работа движка рендеринга будет остановлена. Если у нас есть очень сложные составные компоненты, требующие повторного рендеринга, то стек вызовов может быть очень длинным.
Стек вызовов слишком длинный, и если сложные операции выполняются в середине, это может привести к ухудшению взаимодействия с пользователем из-за блокировки механизма рендеринга на долгое время. Планирование должно решить эту проблему.
React назначит каждую задачу в соответствии с приоритетом задачи.expirationTime
, для обработки задач с более высоким приоритетом до истечения срока действия, а задачи с высоким приоритетом также могут прерывать задачи с низким приоритетом (что приводит к многократному выполнению некоторых функций жизненного цикла), чтобы добиться отсутствия В случае влияния на работу пользователя, обновление рассчитывается по сегментам (то есть с разделением по времени).
Как React реализует планирование
React реализует планирование в основном двумя вещами:
- Рассчитать expriationTime задачи
- Версия полифилла, реализующая requestIdleCallback.
Затем позвольте автору представить вам две части контента один за другим.
expriationTime
ExpriationTime кратко представил свою роль в предыдущей статье, На этот раз мы можем сравнить приоритеты между различными задачами и рассчитать время ожидания задач.
Так как же рассчитывается это время?
Текущее время относится кperformance.now()
, этот API будет возвращать метку времени с точностью до миллисекунды (конечно, не с высокой точностью), и не все браузеры совместимы.performance
API. При использованииDate.now()
Если да, то точность будет хуже, но для удобства мы единообразно считаем текущее время какperformance.now()
.
Константа относится к значению, основанному на разных приоритетах.В настоящее время в React существует пять приоритетов, а именно:
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
Их соответствующие соответствующие значения различны, конкретное содержание выглядит следующим образом.
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
То есть предположим, что текущее время равно 5000, и есть две задачи с разными приоритетами для выполнения. первый принадлежитImmediatePriority
, последний принадлежитUserBlockingPriority
, то время, рассчитанное для двух задач, равно4999
а также5250
. Через это время вы можете сравнить размер, чтобы узнать, чей приоритет выше, или вы можете получить время ожидания задачи, вычитая текущее время.
requestIdleCallback
законченныйexpriationTime
, следующая тема - реализоватьrequestIdleCallback
Теперь давайте сначала разберемся, что делает эта функция
Методы обратного вызова этой функции вызываются последовательно в течение периода простоя браузера, что позволяет нам выполнять некоторые задачи в цикле событий, не затрагивая чувствительные к задержке события, такие как анимация и взаимодействие с пользователем.
На приведенном выше рисунке мы также видим, что метод обратного вызова выполняется после рендеринга. Итак, представив функцию функции, поговорим о ее совместимости.
Совместимость этой функции не очень, и у нее есть фатальный недостаток:
requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work.
то естьrequestIdleCallback
Обратный вызов можно вызывать только 20 раз в секунду, что совершенно неудовлетворительно для существующей ситуации, поэтому команда React намерена реализовать эту функцию самостоятельно.
Если вы хотите узнать больше о заменеrequestIdleCallback
содержание, вы можете прочитатьПроблема.
Как реализовать requestIdleCallback
выполнитьrequestIdleCallback
Ядром функции является только одна точка,Как вызвать метод обратного вызова несколько раз, когда браузер простаивает и после рендеринга?
Говоря о многократном выполнении, то вы должны использовать таймер. Среди различных таймеров толькоrequestAnimationFrame
имеет определенную степень точности, поэтомуrequestAnimationFrame
теперь реализованоrequestIdleCallback
шаг.
requestAnimationFrame
Метод обратного вызова будет выполняться перед каждой перерисовкой, а также у него есть недостаток: функция обратного вызова не будет выполняться, когда страница находится в фоновом режиме, поэтому нам нужно сделать исправление этой ситуации.
rAFID = requestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
// 定时 100 毫秒是算是一个最佳实践
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, 100);
когдаrequestAnimationFrame
Если не выполняется, будетsetTimeout
Чтобы исправить это, два таймера могут внутренне отменить друг друга.
использоватьrequestAnimationFrame
Мы только что выполнили этот шаг несколько раз. Далее нам нужно понять, как узнать, простаивает ли текущий браузер?
Все мы знаем, что во фрейме браузер может реагировать на события взаимодействия с пользователем, выполнять JS и выполнять ряд вычислений и рендеринга. Предполагая, что наш текущий браузер поддерживает 60 кадров в секунду, это означает, что время одного кадра составляет 16,6 миллисекунды. Если вышеуказанные операции превысят 16,6 миллисекунд, рендеринг не будет завершен и кадр будет падать, что повлияет на пользовательский опыт; если вышеуказанные операции не занимают 16,6 миллисекунд, то мы думаем, что в данный момент есть время простоя, так что мы можем выполнить задание.
Итак, далее нам нужно выяснить, осталось ли еще время в текущем кадре, которое мы могли бы использовать.
let frameDeadline = 0
let previousFrameTime = 33
let activeFrameTime = 33
let nextFrameTime = performance.now() - frameDeadline + activeFrameTime
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
nextFrameTime = 8;
}
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
Суть приведенной выше части кода состоит в том, чтобы получить время, затраченное на каждый кадр, и время следующего кадра. Проще говоря, если предположить, что текущее время равно 5000, а браузер поддерживает 60 кадров, то 1 кадр составляет примерно 16 миллисекунд, тогда время следующего кадра будет рассчитано как 5016.
После получения времени следующего кадра нам нужно только сравнить, меньше ли текущее время времени следующего кадра, чтобы мы могли четко знать, есть ли еще свободное время для выполнения задачи.
Затем последний шаг — как выполнить задачу после рендеринга. Здесь нужно использовать знание цикла событий
Наверное, все знают разницу между микрозадачами и макрозадачами, поэтому я не буду здесь повторять содержание этой части. Из приведенного выше рисунка видно, что только задача макроса будет выполняться первой после рендеринга, поэтому задача макроса — это то, что мы делаем для достижения этого шага.
Но есть много способов сгенерировать макро-задачу и у каждого есть приоритет, поэтому, чтобы выполнить задачу быстрее, мы должны выбрать способ с более высоким приоритетом. Здесь мы выбралиMessageChannel
для выполнения этой задачи не выбирайтеsetImmediate
Причина в том, что совместимость слишком плохая.
слишком далеко,requestAnimationFrame
+ Рассчитать время кадра и время следующего кадра +MessageChannel
что мы достигаемrequestIdleCallback
три ключевых момента.
процесс планирования
Так много было сказано выше, в этом разделе мы разберем весь процесс планирования в будущем.
- Во-первых, у каждой задачи будет свой приоритет, сложив текущее время и константу, соответствующую приоритету, мы можем вычислить
expriationTime
,Задачи с высоким приоритетом прерывают задачи с низким приоритетом - Прежде чем планировать, оцените текущую задачуСрок действия истек, не нужно планировать, если он истекает, звоните напрямую
port.postMessage(undefined)
, чтобы задачу с истекшим сроком можно было выполнить сразу после рендеринга - Если срок действия задачи не истек, пройти
requestAnimationFrame
Запустите таймер и вызовите метод обратного вызова перед перерисовкой - В методе обратного вызова нам сначала нужноРассчитать время каждого кадра и время следующего кадра, затем выполните
port.postMessage(undefined)
-
channel.port1.onmessage
Будет вызываться после рендеринга, в этом процессе нам сначала нужно судитьЯвляется ли текущее время меньше, чем время следующего кадра. Если меньше, значит, у нас еще есть свободное время для выполнения задачи, если больше, значит, в текущем кадре свободного времени нет, в это время нам нужно определить, есть ли у каких-либо задач свободное время. истекший.Если он просрочен, неважно, 3721 это или 3721, вам все равно придется выполнить эту задачу.. Если срок его действия не истекает, вы можете перенести задачу только на следующий кадр, чтобы посмотреть, можно ли ее выполнить.
Планирование не только для React
В будущем планирование будет доступно не только React. Поскольку React попытался разделить модуль планирования на библиотеку, эту библиотеку можно в будущем поместить в ваши собственные приложения, чтобы улучшить взаимодействие с пользователем.
И у сообщества тоже есть предложения, я надеюсь, что браузер может прийти с этой функцией, вы можете прочитать подробностиэта библиотека.
наконец
Чтение исходного кода — очень утомительный процесс, но и польза от него огромна. Если у вас возникнут какие-либо вопросы в процессе чтения, вы можете связаться со мной в области комментариев.
Кроме того, написание этой серии является очень трудоемким проектом. Вам необходимо поддерживать комментарии к коду. Вы должны написать статью как можно больше, чтобы читатели могли ее понять. Наконец, вы должны добавить рисунки. Если вы считаете, что статья выглядит ладно, пожалуйста, не скупитесь на свои.
Наконец, если вы считаете, что контент полезен, вы можете обратить внимание на мой официальный аккаунт «Внешняя часть действительно забавная», вас ждет много хорошего.