Проанализируйте процесс обновления рендеринга из исходного кода React.

React.js

Прошло полтора года с тех пор, как я переключился на интерфейс, и фреймворк, с которым я обычно контактирую больше всего, этоReact. После ознакомления с его использованием неизбежно захочется глубже понять принцип его реализации.В Интернете довольно много статей об анализе исходного кода, но я всегда чувствую, что это не так глубоко, как мое собственное чтение и понимание. Поэтому я провел несколько выходных, чтобы понять обычно используемый процесс. Также в этой статье я делюсь своим личным пониманием.

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

React 15

Слои архитектуры

React 15Версия (до Fiber) Весь процесс рендеринга обновления делится на две части:

  • Reconciler(координатор); компонент, отвечающий за поиск изменений
  • Renderer(рендерер); отвечает за рендеринг измененных компонентов на странице

Reconciler

существуетReactсквозьsetState,forceUpdate,ReactDOM.renderдля запуска обновления. Всякий раз, когда происходит обновление,ReconcilerБудет делать следующее:

  1. вызывающий компонентrenderметод, который вернетJSXпреобразовать в виртуальныйDOM
  2. виртуальныйDOMи пустышка на последнем обновленииDOMВ сравнении
  3. Узнайте виртуальные изменения в этом обновлении, сравнивDOM
  4. уведомлятьRendererВизуализировать изменяющийся виртуальный DOM на странице

Renderer

При выполнении воспроизведения на узле обновленияReconcilerПосле этого вы будете уведомленыRendererВыполните соответствующий рендеринг/обновление узла в соответствии с различными «хост-средами».

Недостатки React 15

React 15изdiffпроцессВыполнять обновления рекурсивноиз. Поскольку он рекурсивный,"Неудержимый" однажды начался. когда иерархия слишком глубокая илиdiffЛогика (логика в функции ловушки) слишком сложна, что приводит к тому, что время рекурсивного обновления слишком велико.JsЕсли поток застревает все время, взаимодействие с пользователем и рендеринг застревают. См. пример:count-demo

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>4<li>
<li>3<li>        ->       <li>6<li>

при нажатииbuttonПосле того, как список начинается слева1、2、3Направо2、4、6. Процесс обновления каждого узла в основном синхронен с пользователем, но на самом деле они проходятся последовательно. Конкретные шаги заключаются в следующем:

  1. нажмитеbutton, запуск обновления
  2. Reconcilerобнаружен<li1>необходимо изменить на<li2>, немедленно сообщитьRendererвозобновитьDOM. список становится2、2、3
  3. Reconcilerобнаружен<li2>необходимо изменить на<li4>,уведомлятьRendererвозобновитьDOM. список становится2、4、3
  4. Reconcilerобнаружен<li3>необходимо изменить на<li6>, немедленно сообщитьRendererвозобновитьDOM. список становится2、4、6

Отсюда видноReconcilerа такжеRendererальтернативная работа, когда первый узел изменился на странице, второй узел войдетReconciler. Поскольку весь процесс синхронный, все узлы обновляются одновременно в глазах пользователя.Если вы прервете обновление, вы увидите на странице новое, не полностью обновленное дерево узлов!

Если при достижении второго шага текущая задача внезапно прерывается из-за других задач, так что третий и четвертый шаги не могут быть выполнены, то пользователь увидит:

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>2<li>
<li>3<li>        ->       <li>3<li>

Эта ситуацияReactАбсолютно не желательно. Но этот сценарий применения очень необходим. Представьте, что в какой-то момент пользователь ввел событие ввода, и в этот момент должно произойти обновление.inputОднако, поскольку обновление списка, который не находится в текущей видимой области, приводит к задержке обновления ввода пользователя, работа пользователя отстает. следовательноReactКоманде нужно было найти способ исправить этот недостаток.

React 16

Слои архитектуры

Архитектура React15 не может поддерживать асинхронные обновления и нуждается в рефакторинге, поэтому архитектура React16 изменена на трехуровневую структуру:

  • Планировщик (Scheduler); приоритет задач планирования, задачи с высоким приоритетом попадают в Reconciler первыми
  • Reconciler (координатор); компонент, отвечающий за обнаружение изменений
  • Renderer (рендерер); отвечает за отрисовку измененных компонентов на страницу

Scheduler

React 15правильноReact 16Предлагаемое требование заключается в том, что обновление Diff должно быть прерываемым, тогда в это время возникают две новые две проблемы:Критерии прерывания и оценки;

ReactКоманда использовалаСовместное планирование, т. е. активные прерывания и передачи контроллера.Стандарт суждения - обнаружение тайм-аута. Также необходим механизм, сообщающий, когда прерванные задачи следует возобновить/повторно выполнить.Reactзаимствовано из браузераrequestIdleCallbackинтерфейс, когда в браузере естьУведомлять о выполнении, когда остается время.

по какой-то причинеReactсдатьсяrIdc, но для реализации более полной функцииpolyfill,Прямо сейчасScheduler. Помимо срабатывания функции обратного вызова при простое,SchedulerДля задач также предусмотрены различные приоритеты планирования.

Reconciler

существуетReact 15серединаReconcilerявляется рекурсивнымVirtual DOMиз. а такжеReact16Используется новая структура данных:Fiber.Virtual DOMДерево изменилось с предыдущей древовидной структуры сверху вниз на «граф», основанный на многонаправленном связанном списке.

Процесс обновления изменился с рекурсивного на прерываемый циклический процесс. будет вызываться каждый раз, когда циклshouldYield()Определите, есть ли в настоящее время оставшееся время.Адрес источника.

function workLoopConcurrent() {
    // Perform work until Scheduler asks us to yield
    while (workInProgress !== null && !shouldYield()) {
        workInProgress = performUnitOfWork(workInProgress);
    }
}

ранее проанализированныйReact 15Прерывание выполнения приводит к неполным обновлениям страницы, потому чтоReconcilerа такжеRendererработать попеременно, т.React 16середина,Reconcilerа такжеRendererНет больше попеременной работы. когдаSchedulerпередать заданиеReconcilerназад,Reconcilerпросто для разнообразияVirtual DOMОтметить от имени добавить/удалить/обновитьотметка, без уведомленияRendererрендерить. Что-то вроде этого:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

Только когда все компоненты готовыReconcilerработа будет передана вRendererСделайте обновление рендеринга.

Renderer(Commit)

Rendererсогласно сReconcilerдляVirtual DOMотметьте и выполните соответствующую операцию рендеринга синхронно.

Для примера, который мы использовали в предыдущем разделе, вReact 16Весь процесс обновления в архитектуре таков:

  1. setStateСоздайте обновление с содержимым обновления:state.countот1стали2
  2. обновление даноScheduler,SchedulerЕсли другой задачи с более высоким приоритетом не найдено, поручите эту задачуReconciler
  3. ReconcilerПолучите задание и начните обходVirtual DOM, определите, какойVirtual DOMнужно обновитьVirtual DOMотметка
  4. Reconcilerпройти всеVirtual DOM,уведомлятьRenderer
  5. Rendererсогласно сVirtual DOMТег выполняет соответствующую операцию узла

Среди них шаги 2, 3 и 4 могут быть прерваны в любое время по следующим причинам:

  • Есть другие задачи с более высоким приоритетом, которые необходимо обновить в первую очередь.
  • Для текущего кадра не осталось времени

из-заSchedulerа такжеReconcilerРабота выполняется в памяти, а узлы на странице не обновляются, поэтому пользователь не увидит не полностью обновленные страницы.

Принцип разницы

РеагироватьDiffесть определенныйпомещение, в основном делится на три пункта:

  • Есть несколько случаев, когда DOM перемещается между уровнями.Virtual DOMДеревья сравниваются иерархически, и два дерева будут сравнивать только узлы одного уровня.
  • Разные типы компонентов имеют разную древовидную структуру. Компоненты одного типа имеют сходную древовидную структуру.
  • Набор операций дочерних узлов на одном уровне есть не что иное, какобновить, удалить, добавить, в состоянии пройтиТолькоIDразличать узлы

Будь тоJSXформат все ещеReact.createElementСозданные компоненты React в конечном итоге будут преобразованы вVirtual DOM, что в конечном итоге создаст соответствующийVirtual DOMструктура дерева.React 15Каждое обновление будет новымVirtual DOM, затем пройтирекурсияспособ сравнения старого и новогоVirtual DOMразницу, получить "обновить патч" после сравнения и, наконец, сопоставить с реальнымDOMначальство.React 16Конкретный процесс будет проанализирован позже

Анализ исходного кода

Существует много исходного кода React, и был скорректирован исходный код после 16. В настоящее время последний исходный код на Github зарезервирован.xxx.new.jsа такжеxxx.old.jsдва кода.реагировать на исходный кодсостоит в том, чтобы принятьMonorepoструктуру для управления, различные функции разделены на разныеpackageЕдинственным недостатком может быть то, что адрес метода не очень удобно индексировать.Если вы не знакомы с исходным кодом, может потребоваться глобальный запрос определенной функциональной точки по ключевым словам, а затем проверка по одному. Прежде чем начать, вы можете прочитать официальнуюЧитать руководство

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

  1. Сначала вы должны знатьJSXилиcreateElementВо что превратится закодированный код?
  2. Затем проанализируйте запись приложенияReactDOM.render
  3. Затем дальнейший анализsetStateобновленный процесс
  4. Наконец, подробный анализScheduler,Reconciler,RendererОбщий процесс

Действия, запускающие обновления рендеринга, за исключениемReactDOM.render,setStateКроме того, естьforceUpdate. Но это то же самое, самая большая разница в том, чтоforceUpdateне пойдетshouldComponentUpdateфункция крючка.

структура данных

Fiber

Прежде чем приступить к формальному анализу процесса, я надеюсь, выFiberиметь определенное понимание. Если нет, то я предлагаю вам взглянуть на этовидео. Тогда давайте знакомитьсяReactFiberпримерная структура.

export type Fiber = {
    // 任务类型信息;
    // 比如ClassComponent、FunctionComponent、ContextProvider
    tag: WorkTag,
    key: null | string,
    // reactElement.type的值,用于reconciliation期间的保留标识。
    elementType: any,
    // fiber关联的function/class
    type: any,
    // any类型!! 一般是指Fiber所对应的真实DOM节点或对应组件的实例
    stateNode: any,
    // 父节点/父组件
    return: Fiber | null,
    // 第一个子节点
    child: Fiber | null,
    // 下一个兄弟节点
    sibling: Fiber | null,
    // 变更状态,比如删除,移动
    effectTag: SideEffectTag,
    // 用于链接新树和旧树;旧->新,新->旧
    alternate: Fiber | null,
    // 开发模式
    mode: TypeOfMode,
    // ...
  };

FiberRoot

каждый проходReactDom.renderВизуализированное дерево или приложение инициализируют соответствующийFiberRootОбъект служит отправной точкой для приложения. Его структура данных выглядит следующим образомReactFiberRoot.

type BaseFiberRootProperties = {
  // The type of root (legacy, batched, concurrent, etc.)
  tag: RootTag,
  // root节点,ReactDOM.render()的第二个参数
  containerInfo: any,
  // 持久更新会用到。react-dom是整个应用更新,用不到这个
  pendingChildren: any,
  // 当前应用root节点对应的Fiber对象
  current: Fiber,
  // 当前更新对应的过期时间
  finishedExpirationTime: ExpirationTime,
  // 已经完成任务的FiberRoot对象,在commit(提交)阶段只会处理该值对应的任务
  finishedWork: Fiber | null,
  // 树中存在的最旧的未到期时间
  firstPendingTime: ExpirationTime,
  // 挂起任务中的下一个已知到期时间
  nextKnownPendingLevel: ExpirationTime,
  // 树中存在的最新的未到期时间
  lastPingedTime: ExpirationTime,
  // 最新的过期时间
  lastExpiredTime: ExpirationTime,
  // ...
};

Тип волокна

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 不确定类型;可能是class或function
export const HostRoot = 3; // 树的根
export const HostPortal = 4; // 一颗子树
export const HostComponent = 5; // 原生节点;根据环境而定,浏览器环境就是div等
export const HostText = 6; // 纯文本节点
export const Fragment = 7;

модель

прибытьReact 16.13.1На данный момент встроенные режимы разработки следующие:

export type TypeOfMode = number;
// 普通模式|Legacy模式,同步渲染,React15-16的生产环境用
export const NoMode = 0b0000;
// 严格模式,用来检测是否存在废弃API(会多次调用渲染阶段生命周期),React16-17开发环境使用
export const StrictMode = 0b0001;
// ConcurrentMode 模式的过渡版本
export const BlockingMode = 0b0010;
// 并发模式,异步渲染,React17的生产环境用
export const ConcurrentMode = 0b0100;
// 性能测试模式,用来检测哪里存在性能问题,React16-17开发环境使用
export const ProfileMode = 0b1000;

В этой статье анализируется только режим ConcurrentMode.

JSX с React.createElement

Давайте рассмотрим самые простыеJSXКомпоненты кодирования формата, здесь с помощьюbabelпреобразование кода, кодпосмотри на это

// JSX
class App extends React.Component {
    render() {
        return <div />
    }
}

// babel
var App = /*#__PURE__*/function (_React$Component) {
    _inherits(App, _React$Component);

    var _super = _createSuper(App);

    function App() {
        _classCallCheck(this, App);

        return _super.apply(this, arguments);
    }

    _createClass(App, [{
        key: "render",
        value: function render() {
            return /*#__PURE__*/React.createElement("div", null);
        }
    }]);

    return App;
}(React.Component);

Дело в томrenderметод на самом деле называетсяReact.createElementметод. Тогда нам нужно только проанализироватьcreateElementПросто делай что угодно. давайте сначала посмотримReactElementСтруктура:

let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol === 'function' && Symbol.for) {
    REACT_ELEMENT_TYPE = Symbol.for('react.element');
}

const ReactElement = function (type, key, ref, props) {
    const element = {
        // 唯一地标识为React Element,防止XSS,JSON里不能存Symbol
        ?typeof: REACT_ELEMENT_TYPE,

        type: type,
        key: key,
        ref: ref,
        props: props,
    }
    return element;
}

Очень простая структура данных, функция каждого атрибута понятна с первого взгляда, поэтому я не буду объяснять ее по отдельности. затем проанализируйтеReact.createElementисходный код.

Анти-XSS-атака

Если вы не знакомы с XSS-атаками, рекомендуется сначала прочитать эту статью.Как предотвратить XSS-атаки?. Во-первых, компоненты, которые мы кодируем, будут преобразованы вReactElementОбъект. Операция DOM и генерация — всеJsгенерируется скриптом. Принципиально исключает триXSSАтака (вы думаете).

ноReactпри условииdangerouslySetInnerHTMLприйти какinnerHTMLальтернатива. Если в определенном сценарии интерфейс выдает мнеJSONформатировать данные. мне нужно показать вdivсередина. В случае перехвата злоумышленникомJSONзаменить на абзацReactElementСтруктура формата. Так что же происходит?

Я написал один здесьdemo, при удалении?typeofБудет обнаружено, что будет сообщено об ошибке. а такжеSymbolневозможноJSON, так снаружи тоже недоступноdangerouslySetInnerHTMLатакован. См. исходный код конкретного обнаруженияздесь

const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
    key: true,
    ref: true,
    __self: true,
    __source: true,
};

function createElement(type, config, children) {
    let propName;

    // Reserved names are extracted
    const props = {};

    let key = null;
    let ref = null;

    if (config !== null) {
        if (hasValidRef(config)) {
            ref = config.ref;
        }
        if (hasValidKey(config)) {
            key = '' + config.key;
        }
    }

    // 过滤React保留的关键字
    for (propName in config) {
        if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
            props[propName] = config[propName];
        }
    }

    // 遍历children
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
        props.children = children;
    } else if (childrenLength > 1) {
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;
    }

    // 设置默认props
    if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
            if (props[propName] === undefined) {
                props[propName] = defaultProps[propName];
            }
        }
    }

    return ReactElement(type, key, ref, props);
}

Комментарии должны быть достаточно четкими. Подводя итог, он основан на параметрах для созданияReactElementобъект и привязать соответствующийprops,key,refЖдать;

процесс рендеринга

ReactDOM.renderиспользовать ссылкуздесь

В общем, используйтеReactписать приложения,ReactDOM.renderэто первая функция, которую мы запускаем. Затем мы начинаем сReactDOM.renderЭта функция входа запускает анализrenderвесь процесс.

Исходный код будет часто появляться дляhydrateлогическое суждение и обработка. это сSSRВ сочетании с рендерингом на стороне клиента мы не будем проводить слишком много анализа. Я опущу часть исходного кода

ReactDOM.renderна самом деле правильноReactDOMLegacyвнутреннийrenderСсылка на метод, упрощенная логика выглядит следующим образом:

export function render(
    // React.creatElement的产物
    element: React$Element<any>,
    container: Container,
    callback: ?Function,
) {
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback,
    );
}

на самом деле звонитlegacyRenderSubtreeIntoContainerметод, посмотрите на это еще раз

function legacyRenderSubtreeIntoContainer(
    parentComponent: ?React$Component<any, any>, // 一般为null
    children: ReactNodeList,
    container: Container,
    forceHydrate: boolean,
    callback: ?Function,
) {

    let root: RootType = (container._reactRootContainer: any);
    let fiberRoot;
    if (!root) {
        // [Q]: 初始化容器。清空容器内的节点,并创建FiberRoot
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
            container,
            forceHydrate,
        );
        // FiberRoot; 应用的起点
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
            const originalCallback = callback;
            callback = function () {
                const instance = getPublicRootInstance(fiberRoot);
                originalCallback.call(instance);
            };
        }
        // [Q]: 初始化不能批量处理,即同步更新
        unbatchedUpdates(() => {
            updateContainer(children, fiberRoot, parentComponent, callback);
        });
    } else {
        // 省略... 跟上面类似,差别是无需初始化容器和可批处理
        // [Q]:咦? unbatchedUpdates 有啥奥秘呢
        updateContainer(children, fiberRoot, parentComponent, callback);
    }
    return getPublicRootInstance(fiberRoot);
}

Согласно документации на официальном сайте, на этом шаге в первую очередь будут очищаться существующие узлы в контейнере, если есть асинхронный обратный вызовcallbackОн будет сохранен первым и привязан к соответствующемуFiberRootСсылайтесь на отношение для последующих проходов к правильному корневому узлу. В комментариях я отметил два[Q]представляет две проблемы. Давайте сначала подробно проанализируем эти два вопроса.

инициализация

Что касается именования,legacyCreateRootFromDOMContainerиспользуется для инициализации корневого узла. БудуlegacyCreateRootFromDOMContainerВозвращаемый результат присваиваетсяcontainer._reactRootContainer,а также_reactRootContainerС точки зрения кода он используется в качестве основы для того, был ли он инициализирован, и это также проверяется. Если не веришь мне, откройReactприложение, просмотрите элемент контейнера_reactRootContainerАтрибуты

function legacyCreateRootFromDOMContainer(
  container: Container,
  forceHydrate: boolean,
): RootType {
  // 省略 hydrate ...
  return createLegacyRoot(container, undefined);
}

export function createLegacyRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

function ReactDOMBlockingRoot(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // !!! look here
  this._internalRoot = createRootImpl(container, tag, options);
}

Ряд вызовов функций фактически возвращает экземпляр ReactDOMBlockingRoot. Основное внимание уделяется свойствам_internalRootчерезcreateRootImplпродукт создан.

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // 省略 hydrate ...
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // 省略 hydrate ...
  return root;
}

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
  // 生成 FiberRoot
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // 为Root生成Fiber对象
  const uninitializedFiber = createHostRootFiber(tag);
  // 绑定 FiberRoot 与 Fiber 
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // 生成更新队列
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    baseQueue: null,
    shared: {
      pending: null,
    },
    effects: null,
  };
  fiber.updateQueue = queue;
}

Общая логика заключается в созданииFiberRootобъектroot. и сгенерированоrootсоответствующийFiberобъект, при созданииfiberочередь обновлений. отсюда понятноFiberRootКогда он инициализируется, мы должны помнить об этом в первую очередьFiberRoot, можно считать, что он весьReactНачальная точка приложения.

unbatchedUpdates

Английские комментарии в исходном коде указывают на то, что пакетная обработка не требуется и должна выполняться немедленно. Его входящий параметр является выполнениемupdateContainerфункция обертки. Но когдаelseСудебное решение действительно исполненоupdateContainer. ТакunbatchedUpdatesВ чем загадка?

export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    // !!! look here
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      flushSyncCallbackQueue();
    }
  }
}

export function flushSyncCallbackQueue() {
  // 省略...
  flushSyncCallbackQueueImpl();
}

// 清空同步任务队列
function flushSyncCallbackQueueImpl() {
  if (!isFlushingSyncQueue && syncQueue !== null) {
    isFlushingSyncQueue = true;
    let i = 0;
    try {
      const isSync = true;
      const queue = syncQueue;
      // 以最高优先级来清空队列里的任务
      runWithPriority(ImmediatePriority, () => {
        for (; i < queue.length; i++) {
          let callback = queue[i];
          do {
            callback = callback(isSync);
          } while (callback !== null);
        }
      });
      syncQueue = null;
    } catch (error) {
      // 移除错误的任务
      if (syncQueue !== null) {
        syncQueue = syncQueue.slice(i + 1);
      }
      // 在下一个执行单元恢复执行
      Scheduler_scheduleCallback(
        Scheduler_ImmediatePriority,
        flushSyncCallbackQueue,
      );
      throw error;
    } finally {
      isFlushingSyncQueue = false;
    }
  }
}

существуетunbatchedUpdatesНа самом деле, это больше, чем абзацfinallyлогика в. Логика в основном заключается в обновлении очереди задач синхронизации. Подумайте об этом, почему? Затем объясните вfn(a)В процессе выполнения должна быть задача синхронизации! Тогда продолжайте следитьupdateContainerПосмотри.

updateContainer

Обратите внимание, что здесьupdateContainerуже принадлежатReconcilerО процесс. Продолжайте следить:

export function updateContainer(
    element: ReactNodeList, // 要渲染的组件
    container: OpaqueRoot, // OpaqueRoot就是FiberRoot
    parentComponent: ?React$Component<any, any>,
    callback: ?Function,
): ExpirationTimeOpaque {
    // 根节点Fiber
    const current = container.current;
    const eventTime = requestEventTime();

    const suspenseConfig = requestCurrentSuspenseConfig();
    // [Q]:计算此次任务的过期时间
    const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );

    const context = getContextForSubtree(parentComponent);
    if (container.context === null) {
        container.context = context;
    } else {
        container.pendingContext = context;
    }

    // 创建一个更新任务
    const update = createUpdate(eventTime, expirationTime, suspenseConfig);
    update.payload = { element };

    callback = callback === undefined ? null : callback;
    if (callback !== null) {
        update.callback = callback;
    }

    // 将任务插入Fiber的更新队列
    enqueueUpdate(current, update);
    // 调度任务 scheduleWork为scheduleUpdateOnFiber
    scheduleWork(current, expirationTime);

    return expirationTime;
}

Этот шаг кажется большим количеством кода, но на самом деле он заключается в том, чтобы сначала рассчитать время истечения срока действия текущего обновления.expirationTime, то черезcreateUpdateсоздалupdateобновить задачу, затем передатьenqueueUpdateвставлятькруговая очередь задач, и, наконец, используйтеscheduleUpdateOnFiberдля планирования задач.

Расчет времени истечения

expirationTimeявляется очень важным понятием. В React, чтобы предотвратить постоянное прерывание обновления из-за приоритета, его нельзя выполнить. Реакция установитexpirationTime, когда время истеклоexpirationTime, если обновление не было выполнено, React выполнит принудительное обновление, чтоexpirationTimeэффект.

Это первый раз, когда мы сталкиваемся с его вычислительной логикой. Давайте проанализируем его подробно.

Первым шагом является расчетcurrentTime, который на самом деле преобразуется во встроенный на основе текущей метки времениExpirationTime. посмотри

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
const MAX_SIGNED_31_BIT_INT =  1073741823;
export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = Batched - 1;

export function msToExpirationTime(ms: number): ExpirationTime {
    return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

export function requestCurrentTimeForUpdate() {
    // 省略...
    return msToExpirationTime(now());
}

посмотри сноваcomputeExpirationForFiberконкретная логика расчета

export function computeExpirationForFiber(
    currentTime: ExpirationTime,
    fiber: Fiber,
    suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
    const mode = fiber.mode;
    // 同步模式
    if ((mode & BlockingMode) === NoMode) {
        return Sync;
    }
    // 从Scheduler取得当前优先级
    const priorityLevel = getCurrentPriorityLevel();
    if ((mode & ConcurrentMode) === NoMode) {
        return priorityLevel === ImmediatePriority ? Sync : Batched;
    }

    // ...

    let expirationTime;
    switch (priorityLevel) {
        case ImmediatePriority:
            expirationTime = Sync;
            break;
        case UserBlockingPriority:
            // 跟 computeAsyncExpiration 一样。区别在于 expirationInMs 参数值更小。
            // 因此得到的expirationTime越小,优先级越高
            expirationTime = computeInteractiveExpiration(currentTime);
            break;
        case NormalPriority:
        case LowPriority: // TODO: Handle LowPriority
            // TODO: Rename this to... something better.
            expirationTime = computeAsyncExpiration(currentTime);
            break;
        case IdlePriority:
            expirationTime = Idle;
            break;
        default:
            invariant(false, 'Expected a valid priority level');
    }
}

export const LOW_PRIORITY_EXPIRATION = 5000;
// 这个 BATCH 是那个意思吗?
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(
    currentTime: ExpirationTime,
): ExpirationTime {
    return computeExpirationBucket(
        currentTime,
        LOW_PRIORITY_EXPIRATION,
        LOW_PRIORITY_BATCH_SIZE,
    );
}

function ceiling(num: number, precision: number): number {
    return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
    currentTime,
    expirationInMs, // 5000
    bucketSizeMs, // 250
): ExpirationTime {
    return (
        MAGIC_NUMBER_OFFSET -
        ceiling(
            MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
            bucketSizeMs / UNIT_SIZE,
        )
    );
}

Вкратце, формула расчета выглядит следующим образом:

// current = MAGIC_NUMBER_OFFSET - ((now() / UNIT_SIZE) | 0);
// expirationTime = MAGIC_NUMBER_OFFSET - ((((MAGIC_NUMBER_OFFSET - currentTime + 500) / 25) | 0) + 1) * 25
// => MAGIC_NUMBER_OFFSET - ((((((now() / UNIT_SIZE) | 0) + 500) / 25) | 0) + 1) * 25

в| 0используется для округления. Примечание+ 1Что означает эта операция?два разныхexpirationTimeРазрыв между25кратны , т.е.25msЗадачи внутри одинаковыеexpirationTime. затем непрерывный25msОперации обновления в файле будут объединены в одну задачу!

Как сообщает официальный сайт,legacyШаблоны имеют автоматическую пакетную обработку синтетических событий, но только для одной задачи браузера. НетReactСобытия, которые хотят использовать эту функцию, должны использоватьunstable_batchedUpdates. существуетblockingузор иconcurrentрежим, всеsetStateПо умолчанию все упаковано. Вот два примера для легкого понимания:

После анализаexpirationTimeрасчет, дальше смотретьscheduleUpdateOnFiberлогика.

Отсюда в исходном коде два метода обработки, синхронный и асинхронный, синхронные задачи не пройдутScheduerЗапланированное. Для полноты анализа мы анализируем только асинхронные процессы. часто упоминаемыйexpirationTime, его можно рассматривать как «узел времени истечения» задачи на данный момент, который представляет собой конкретный «момент времени», а не «отрезок времени». Но на разных этапах оно имеет разное значение. Можно определить, обновляется ли компонент или нет, или временной узел обновления определяется им.

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTimeOpaque,
) {
  // 获取FiberRoot
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    return null;
  }
  if (expirationTime === Sync) {
    // 同步任务调度
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
  // 省略...
}

scheduleUpdateOnFiberПросто дляОбновите время истечения срока действия всего «дерева», укоренившегося в текущем узле.Основное внимание уделяетсяensureRootIsScheduledСюда

// 此函数用于调度任务。 一个root(fiber节点)只能有一个任务在执行 
// 如果已经有任务在调度中,将检查已有任务的到期时间与下一级别任务的到期时间相同。
// 每次更新和任务退出前都会调用此函数
// 注意:root是FiberRoot 
function ensureRootIsScheduled(root: FiberRoot) {
    // lastExpiredTime代表过期时间
    const lastExpiredTime = root.lastExpiredTime;
    if (lastExpiredTime !== NoWork) {
        // 特殊情况:过期的工作应同步刷新
        root.callbackExpirationTime = Sync;
        root.callbackPriority = ImmediatePriority;
        root.callbackNode = scheduleSyncCallback(
            performSyncWorkOnRoot.bind(null, root),
        );
        return;
    }
    // 下一个最近的到期时间
    const expirationTime = getNextRootExpirationTimeToWorkOn(root);
    // root有正在处理的调度任务
    const existingCallbackNode = root.callbackNode;
    if (expirationTime === NoWork) {
        if (existingCallbackNode !== null) {
            root.callbackNode = null;
            root.callbackExpirationTime = NoWork;
            root.callbackPriority = NoPriority;
        }
        return;
    }

    // 获取当前任务的过期时间; 同一事件中发生的所有优先级相同的更新都收到相同的到期时间
    const currentTime = requestCurrentTimeForUpdate();
    // 根据下一次调度任务的过期时间与当前任务的过期时间计算出当前任务的优先级
    // 即currentTime小于expirationTime,那么其优先级更高
    const priorityLevel = inferPriorityFromExpirationTime(
        currentTime,
        expirationTime,
    );

    // 如果当前正在处理的任务优先级基于此次任务,取消正在处理的任务!
    if (existingCallbackNode !== null) {
        const existingCallbackPriority = root.callbackPriority;
        const existingCallbackExpirationTime = root.callbackExpirationTime;
        if (
            // 任务必须具有完全相同的到期时间。
            existingCallbackExpirationTime === expirationTime &&
            // 比较两次任务的优先级
            existingCallbackPriority >= priorityLevel
        ) {
            return;
        }
        // 取消调度任务
        cancelCallback(existingCallbackNode);
    }

    // 更新到期时间与优先级
    root.callbackExpirationTime = expirationTime;
    root.callbackPriority = priorityLevel;

    let callbackNode;
    if (expirationTime === Sync) {
        // 省略...
        // 这里会将任务推入同步任务队列,前面分析到 flushSyncCallbackQueueImpl 清空的任务就是从这里推入
    } else {
        // 将任务推入Scheduler调度队列
        callbackNode = scheduleCallback(
            priorityLevel,
            // 绑定
            performConcurrentWorkOnRoot.bind(null, root),
            // 计算超时时间
            { timeout: expirationTimeToMs(expirationTime) - now() },
        );
    }

    // 更新Fiber的当前回调节点
    root.callbackNode = callbackNode;
}

ensureRootIsScheduledОсновная логика разделена на три шага:

  1. Рассчитайте время истечения и приоритет этой задачи.
  2. Если на текущем узле запланирована задача. еслиСвоевременное время одинаково, и приоритет существующей задачи вышеРасписание отменено. В противном случае отмените задание.
  3. впихнуть задачу вSchedulerОчередь планирования в , и установите ее приоритет и срок действия задачи

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

scheduleCallbackзаключается в том, чтобы передать функцию выполнения задачиSchedulerобрабатывать. Поэтому последующие процессы должны ждатьSchedulerЧтобы вызвать выполнение определенных функцийperformConcurrentWorkOnRoot. оrenderПроцесс пока временно анализируется.

Сводка процесса рендеринга

  1. renderпозвонюlegacyRenderSubtreeIntoContainerметод
  2. legacyRenderSubtreeIntoContainer, если это первая визуализация, она будет инициализирована первойFiberRoot, который является отправной точкой приложения. В то же время создается корневой узелFiberпример. здесьFiberRoot.current = Fiber; Fiber.stateNode = FiberRoot.
  3. передачаupdateContainerРассчитывается время истечения срока действия этого обновления. и сгенерировать объект задачиupdate, вставьте его вFiberобновить очередь в , затем вызватьscheduleUpdateOnFiberЗапуск планирования задач
  4. scheduleUpdateOnFiberБудет обновлено время истечения срока действия всего дерева Fiber с узлом Fiber в качестве корневого узла. тогда позвониensureRootIsScheduledпланировать
  5. ensureRootIsScheduledЗадача будет привязана к конкретной функции выполнения. затем передатьSchedulerиметь дело с

процесс setState

Продолжить анализReconcilerа такжеRendererПеред подробностями давайте ковать железо пока горячо знакомитьсяsetStateобработать. Поскольку вызов осуществляется черезthis.setStateмобилизовать, то изComponentНайдите его внутри. Приходитьlookодин разReactBaseClasses

const emptyObject = {};
function Component(props, context, updater) {
    this.props = props;
    this.context = context;
    this.refs = emptyObject;
    // ReactNoopUpdateQueue 是一个没啥意义的空对象
    this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.setState = function (partialState, callback) {
    this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

ComponentНачальная структура проста. мы видим егоsetStateМетод называетсяthis.updater.enqueueSetStateМетод, ноupdateПо умолчанию это пустой бесполезный объект, и мы обычно не передаем его в конструктор.updateпараметры, то этот метод необходимо внедрить позже. Я искал, искал и нашел нечто подобноеclassComponentUpdater

const classComponentUpdater = {
    isMounted,
    enqueueSetState(inst, payload, callback) {
        const fiber = getInstance(inst);
        const currentTime = requestCurrentTimeForUpdate();
        const suspenseConfig = requestCurrentSuspenseConfig();
        const expirationTime = computeExpirationForFiber(
            currentTime,
            fiber,
            suspenseConfig,
        );
        // 生成此次setState的更新对象
        const update = createUpdate(expirationTime, suspenseConfig);
        update.payload = payload;
        if (callback !== undefined && callback !== null) {
            update.callback = callback;
        }
        // 更新任务入队
        enqueueUpdate(fiber, update);
        scheduleWork(fiber, expirationTime);
    },
    enqueueReplaceState(inst, payload, callback) {
        // 同上类似
    },
    enqueueForceUpdate(inst, callback) {
        // 同上类似
    },
};

эй, ты нашел это?enqueueSetStateЛогика здесь немного знакома. Фактически, это наш предыдущий анализrenderПроцедураupdateContainerПроцесс такой же. Оглянись, если не помнишь. Потом просто анализируемclassComponentUpdaterкак вводить какComponentизupdateхарактеристики.

предыдущий анализrenderВ процессе мы проанализировали толькоГенерировать сегменты задач и помещать их в очередь отправки,还没有对组件的初始化有过分析。 отComponentПредположение заключается в инициализации в конструкторе.ComponentкогдаReactЧто такое инъекция для нас? Следуйте этой линии мысли для следующего анализа. Сначала давайте посмотрим наbeginWorkКусок кода в методе,beginWorkМетод будет подробно проанализирован позже. Здесь сначала узнайте, что он используется для создания подкомпонентовFiberобъект.

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
): Fiber | null {
    // 尝试复用 current 节点
    if (current !== null) {
        // 省略...
    }
    // 不能复用则 update 或者 mount
    switch (workInProgress.tag) {
        // 省略...
        case ClassComponent: {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps =
                workInProgress.elementType === Component
                    ? unresolvedProps
                    : resolveDefaultProps(Component, unresolvedProps);
            return updateClassComponent(
                current,
                workInProgress,
                Component,
                resolvedProps,
                renderExpirationTime,
            );
        }
        // 省略...
    }
}

beginWorkКод в разделе разделен на две части. отдельно для обработкиmountа такжеupdateлогика. Процесс нашего анализа — это первая инициализация, далее следуетmountобработать.beginWorkбудет варьироваться в зависимости отtagВызов различных методов, давайте сначала посмотрим здесьupdateClassComponent

function updateClassComponent(
    current: Fiber | null,
    workInProgress: Fiber,
    Component: any,
    nextProps,
    renderExpirationTime: ExpirationTime,
) {
    // 省略 context 的处理...

    // 组件的实例
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    // instance为null 说明组件第一次渲染
    if (instance === null) {
        if (current !== null) {
            // 重置current与wip的依赖(备份)
            current.alternate = null;
            workInProgress.alternate = null;
            // 标记为新增节点
            workInProgress.effectTag |= Placement;
        }
        // 初始化组件实例
        constructClassInstance(workInProgress, Component, nextProps);
        // 挂载; 并调用相应的生命周期
        mountClassInstance(
            workInProgress,
            Component,
            nextProps,
            renderExpirationTime,
        );
        shouldUpdate = true;
    } else {
        // 省略更新逻辑...
    }
    // TODO:执行 render 新建子Fiber。
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
    return nextUnitOfWork;
}
function constructClassInstance(
    workInProgress: Fiber,
    ctor: any,
    props: any,
): any {
    let context = emptyContextObject;
    // 省略 context 相关逻辑...

    const instance = new ctor(props, context);
    const state = (workInProgress.memoizedState =
        instance.state !== null && instance.state !== undefined
            ? instance.state
            : null);
    adoptClassInstance(workInProgress, instance);

    // 省略 context 相关逻辑...
    return instance;
}
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
    instance.updater = classComponentUpdater;
    workInProgress.stateNode = instance;
    // 绑定实例与Fiber,方便后续更新使用
    setInstance(instance, workInProgress);
}

можно увидеть, когдаinstanceдляnull, будут выполняться следующие процессы

  1. и отметьте текущуюeffectTagдляPlacement, представляющий новый узел
  2. Инициализируйте экземпляр spawn, затем привяжите его кFiber(workInProgress)на и привязатьupdateАтрибуты
  3. в конце концов позвонитmountClassInstanceдля монтирования узла и вызова соответствующего жизненного цикла.

Пока что последующий процесс обновления выглядит следующим образомrenderПроцесс такой же, поэтому нет необходимости повторять анализ~

Scheduler

SchedulerдаReactОтдельная реализация команды для планирования задачrIdcизpolyfill.ReactНамерения команды не ограничиваютсяReactВ этом сценарии приложения я хочу обслуживать больше предприятий и стать инструментом для более широкого применения.

очередь с минимальным приоритетом

Поскольку задачи имеют разное время истечения и приоритеты, для управления приоритетными задачами необходима структура данных.ReactсерединаexpirationTimeМеньшие задачи должны обрабатываться более предпочтительно, поэтому эта структура данных, очевидно, представляет собой очередь с минимальным приоритетом.а такжеReactЭто очередь с наименьшим приоритетом, основанная на небольшой верхней куче.. Просто посмотрите на код напрямую.SchedulerMinHeap

type Heap = Array<Node>;
type Node = {|
  id: number,
    sortIndex: number,
|};

// 插入到堆末尾
export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

// 获取堆顶任务,sortIndex/id 最小的任务
export function peek(heap: Heap): Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

// 删除堆顶任务
export function pop(heap: Heap): Node | null {
  const first = heap[0];
  if (first !== undefined) {
    const last = heap.pop();
    if (last !== first) {
      heap[0] = last;
      siftDown(heap, last, 0);
    }
    return first;
  } else {
    return null;
  }
}

// 向上维持小顶堆
function siftUp(heap, node, i) {
  let index = i;
  while (true) {
    // 位运算;对应根据节点求其父节点-> i / 2 - 1
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (parent !== undefined && compare(parent, node) > 0) {
      // parent 更大,交换位置
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return;
    }
  }
}

// 向下维持小顶堆
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  while (index < length) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // // 如果左子节点或右子节点小于目标节点(父节点),则交换
    if (left !== undefined && compare(left, node) < 0) {
      if (right !== undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  // 先比较sort index,再比较 task id
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

Специальная реализация состоит в том, чтобы моделировать структуру минимальной кучи с массивом. Видно, что каждый раз, когда задача вставлена ​​или удалена, минимальная структура кучи будет восстановлена, а правила сортировки следующие:sortIndex,taskIdв качестве доп. в реакцииsortIndexСоответствующее на самом деле время истечения срока действия.taskIdПо сравнению с добавочной последовательностью задач. Это последующие анализы.

Открытое планирование задач

Ранее было проанализировано, чтоensureRootIsScheduledсоздаст узел задачи вscheduleCallbackвпихнуть задачу вSchedulerсередина. Итак, начнем со способа входа в команду для этой задачи, чтобы разобрать пошагово.

var taskIdCounter = 1;

// 目前Scheduler对外的api都是unstate_级别的,表示不是稳定版本
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 实际是调用performance.now() 或者 Date.now() 前者更精确
  var currentTime = getCurrentTime();

  var startTime;
  var timeout;
  // 根据是否有延迟来确定开始时间
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
    // [Q1]:有超时配置直接用。否则根据优先级计算
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel);
  } else {
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }

  // 过期时间等于开始时间+超时时间
  var expirationTime = startTime + timeout;

  // 一个task的数据结构就是这样啦。
  var newTask = {
    // 相同超时时间的任务会对比id,那就是先到先得咯
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // [Q2]:下面出现了一个延迟队列(timerQueue)和一个任务队列(taskQueue)
  if (startTime > currentTime) {
    // This is a delayed task.
    // 说明这是一个延迟任务;即options.delay存在嘛
    newTask.sortIndex = startTime;
    // 如果开始时间大于当前时间,就将它 push 进这个定时器队列,说明这个是一个等待队列
    push(timerQueue, newTask);
    // 如果任务队列为空,说明所有任务都被延迟,且newTask是最早的延迟任务。
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // 如果正在进行超时处理,先取消,后续再重新开始
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 发起一个超时处理
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    // 非延迟任务丢入任务队列
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // 如果没在调度中则开启调度;
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // [Q]开启调度
      requestHostCallback(flushWork);
    }
  }
  // [A]:还回这个task的引用
  return newTask;
}

Из этого кода вы можете увидеть структуру данных задачи планирования и основу сортировки задач.sortIndexПо сути, это время истечения задачи.expirationTime,а такжеidявляется возрастающей последовательностью. В примечаниях отмечены несколько проблем, а ниже приводится подробный анализ

расчет времени ожидания

// 立即执行
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 用户行为阻塞
var USER_BLOCKING_PRIORITY = 250;
// 默认五秒过期时间
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// 永不过期, maxSigned31BitInt为v8 32为系统最大有效数值
var IDLE_PRIORITY = maxSigned31BitInt;

function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;
    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY;
    case IdlePriority:
      return IDLE_PRIORITY;
    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;
    case NormalPriority:
    default:
      return NORMAL_PRIORITY_TIMEOUT;
  }
}

Видно, что приоритет преобразуется в конкретное время постоянного уровня, чем выше приоритет.timeoutвремя ниже.

taskQueue & timerQueue

существуетstartTime > currentTimeВ условной ветви задачи помещаются вtaskQueueа такжеtimerQueue. И эти две очереди на самом деле являются структурой минимальной кучи, которую мы анализировали ранее.taskQueueпредставляет задачу, запланированную в настоящее время, в то время какtimerQueueПредставляет очередь отложенных задач. В процессе планирования задачtimerQueueЗадачи при переходе наtaskQueue, этот шаг будет проанализирован позже.

Конкретный процесс планирования

Мы видим, что когда задача вставляется в очередь планирования, если она не запланирована в это время, она вызоветrequestHostCallbackМетод начинает планирование и проходит вflushworkкак функция ввода.

requestHostCallback = function(callback) {
  // 这里将传入的callback缓存起来了
  scheduledHostCallback = callback;
  // 是否在消息循环中
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    port.postMessage(null);
  }
};

Судя по кодуrHCРоль только кэшироватьcallbackкоторыйflushworkЭто входная функция. и отправил пустойmessage. Так что точка с этимportЧто это такое. На самом деле вотReactкак имитироватьrequestIdleCallbackместо.

MessageChannel имитирует RIC для кругового планирования

незнакомыйMessageChannelВы можете узнать первым. Первый взглядSchedulerкак используется.

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

Можно узнать, когдаport.postMessageКогда появляется сообщение, функция, которая фактически обрабатывает сообщение,performWorkUntilDeadline.

let isMessageLoopRunning = false;
let scheduledHostCallback = null;

const performWorkUntilDeadline = () => {
  // scheduledHostCallback 具体是由 scheduledHostCallback 赋值的
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // [Q]:截止时间 = 当前时间 + yieldInterval
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;
    try {
      // 是否还有剩余任务。scheduledHostCallback 可能是 flushwork
      const hasMoreWork = scheduledHostCallback(
        hasTimeRemaining,
        currentTime,
      );
      if (!hasMoreWork) {
        // 没有更多任务 停止循环,并清楚scheduledHostCallback引用
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // 如果还有任务,则继续发消息。类似一个递归的操作
        port.postMessage(null);
      }
    } catch (error) {
      // 如果一个任务出错了。直接跳过执行下一个任务,并抛出错误
      port.postMessage(null);
      throw error;
    }
  } else {
    // 重置循环状态
    isMessageLoopRunning = false;
  }
  // [Q]: 目前不知道这是啥
  needsPaint = false;
};

Как обычно, здесь есть несколько вопросов, требующих тщательного анализа.

yieldInterval

Судя по названию и использованию, я думаю, что это должно быть репрезентативнымвремя выполнения задачи.

// 默认是5
let yieldInterval = 5;

forceFrameRate = function (fps) {
  // ??? 看不起我144hz
  if (fps < 0 || fps > 125) {
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
      'forcing framerates higher than 125 fps is not unsupported',
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    yieldInterval = 5;
  }
};

forceFrameRateявляется внешнимapiЭтот интерфейс используется для динамической настройки периода выполнения запланированных задач.

deadline & needsPaint

let deadline = 0;
let maxYieldInterval = 300;
let needsPaint = false;

if (
  enableIsInputPending &&
  navigator !== undefined &&
  navigator.scheduling !== undefined &&
  navigator.scheduling.isInputPending !== undefined
) {
  const scheduling = navigator.scheduling;
  shouldYieldToHost = function () {
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      // 没有时间了。可能希望让主线程让出控制权,以便浏览器可以执行高优先级任务,主要是绘制和用户输入
      // 因此如果有绘制或者用户输入行为,则应该让出,放回true
      // 如果两者都不存在,那么可以在保持响应能力的同时降低产量
      // 但是存在非`requestPaint`发起的绘制状态更新或其他主线程任务(如网络事件)
      // 因此最终在某个临界点还是得让出控制权
      if (needsPaint || scheduling.isInputPending()) {
        // 有待处理的绘制或用户输入
        return true;
      }
      // 没有待处理的绘制或输入。但在达到最大产量间隔时也需要释放控制权
      return currentTime >= maxYieldInterval;
    } else {
      return false;
    }
  };

  requestPaint = function () {
    needsPaint = true;
  };
} else {
  shouldYieldToHost = function () {
    return getCurrentTime() >= deadline;
  };

  requestPaint = function () { };
}

Первое, что должно быть ясно, это то, чтоshouldYieldToHostа такжеrequestPaintдаSchedulerВнешние интерфейсные функции. Конкретное использование будет проанализировано позже.

Как видно из кода,deadlineИспользование используется вshouldYieldToHostсерединаПроверьте, не истек ли тайм-аут расписания. Очистка по умолчанию - это прямое сравнение текущего времени.currentTimeа такжеdeadlineценность . но,в поддержкуnavigator.schedulingокружающая обстановка,ReactБудет больше соображений, то есть рисование в браузере и ввод пользователя должны иметь ограниченный отклик, в противном случае время планирования может быть соответствующим образом увеличено..

Здесь давайте сначала подведем итоги процесса планирования запуска, чтобы не запутаться.

  1. requestHostCallbackГотов выполнить заданиеscheduledHostCallback
  2. requestHostCallbackЗапустите цикл планирования задач
  3. MessageChannelполучать сообщения и звонитьperformWorkUntilDeadlineвыполнять задачи
  4. performWorkUntilDeadlineСначала рассчитайте расписаниеdeadline. затем выполнить задание
  5. После выполнения задачи он будет судить о наличии следующей задачи в соответствии с возвращаемым значением. Если есть, рекурсивное выполнение достигается через цикл обработки сообщений.performWorkUntilDeadline. В противном случае завершить цикл сообщений

Фронт анализирует только логику выполнения цикла планирования задач. Конкретные задачи выполняютсяscheduledHostCallbackэталонная функцияflushWork.

выполнение задачи

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod codepath.
      // 官方注释说,生成环境不会去catch workLoop抛出的错误
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

flushWorkРабота относительно проста. просто сбросьте несколько флагов и, наконец, вернулисьworkLoopрезультат выполнения. Тогда внимание должно быть сосредоточено на этой функции.

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // [Q]: 这是作甚?
  advanceTimers(currentTime);
  // 取出顶端任务。即最优先的任务
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    // debug 用的,不管
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      // 任务未过期,并且当前调度的deadline到了,将任务放到下次调度周期进行; shouldYieldToHost 
      currentTask.expirationTime > currentTime &&
      // 这两个前面分析过了; hasTimeRemaining一直为true,那还判断有啥意义???
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 计算当前任务是否已经超时
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      // [Q]: 执行callback,比如前面render流程分析到的 performConcurrentWorkOnRoot
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // continuationCallback 成立,则取代当前任务的callback
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // continuationCallback 不成立,从任务队列弹出
        // 防止任务被其他地方取出,得判断一下
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      // em.... 又是它
      advanceTimers(currentTime);
    } else {
      // 任务被取消了,弹出任务
      // 回顾下ensureRootIsScheduled 中调用 cancelCallback 的情况
      pop(taskQueue);
    }
    // 再次从顶端取任务
    // 注意:如果 continuationCallback 成立的话,是没有pop当前任务的。此次取到的还是当前任务
    currentTask = peek(taskQueue);
  }
  // performWorkUntilDeadline 中判断 hasMoreWork 的逻辑就是这里啦!
  if (currentTask !== null) {
    return true;
  } else {
    // [Q]:检测延迟队列中的任务是不是过期
    let firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

Общие комментарии к процессу уже очень подробные. Старые правила, анализ нескольких отмеченных проблем.

advanceTimers

function advanceTimers(currentTime) {
  // 遍历 timerQueue 中的任务;将超时的任务转移到 taskQueue 中去
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // 任务被取消
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // 超时任务转移
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // 未过时的继续挂起
      return;
    }
    timer = peek(timerQueue);
  }
}

wookLoopПервый вызов записи функцииadvanceTimersЭто реорганизация задач и обновление очереди задач. и каждый раз после этогоwhileзвонокПоскольку выполнение задачи занимает определенное время, все очереди задач необходимо обновить после выполнения..

continuationCallback

первыйcontinuationCallbackпроизводитсяcallbackрешенный.callbackВозвращаемое значение может быть функцией, что означает, что текущая задача должна быть повторно обработана один раз. Позвольте мне оставить вопрос здесь и проанализировать его позжеcallbackКогда конкретная реализация, мы будем далее анализировать

requestHostTimeout & handleTimeout

существуетwookLoopконец, когдаcurrentTask === nullКогда придет время, он проверит, истек ли срок действия задачи в очереди задержки.

requestHostTimeout = function (callback, ms) {
  taskTimeoutID = setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
};

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // 重新梳理任务队列
  advanceTimers(currentTime);

  // isHostCallbackScheduled 为true。说明有新任务进来了
  if (!isHostCallbackScheduled) {
    // 如果上面的 advanceTimers 梳理了过期的延迟任务到任务队列中,则执行
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // 否则递归调用该方法
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

Видно, что на самом деле это происходит после завершения выполнения задач в очереди задач. Запросить, есть ли просроченные задачи из очереди отложенных задач рекурсивным методом, и если они есть, передать их в очередь задач и выполнить.

сюда,SchedulerБыл проанализирован весь процесс от ввода задачи до циклического планирования и выполнения задачи. Чтобы сделать простое резюме процесса:

  1. unstable_scheduleCallbackСоздать задачу, поместить в отложенную очередь, если задача отложенаtimerQueue, иначе отправить в очередь задачtaskQueue
  2. Если созданная задача является отложенной, вызовитеrequestHostTimeoutиспользование методаsetTimeoutПриходитьРекурсивно проверить, не истек ли срок действия задачи. В противном случае напрямую инициируйте планирование задачrequestHostCallback
  3. requestHostCallbackпройти черезMessageChannelизport2отправить сообщениеport1, конкретная функция обработкиperformWorkUntilDeadline
  4. performWorkUntilDeadlineКрайний срок для этого планирования будет рассчитан, и в то же время использоватьцикл сообщенийвыполнять задачи рекурсивно
  5. Задача решается специальноwookLoopвоплощать в жизнь. который удаляет задачу из очереди задачtaskQueueВерх кучи извлекается и выполняется по очереди. Если очередь задач пуста, вызовитеrequestHostTimeoutВключить обнаружение рекурсии.

Reconciler

После анализаSchedulerСледующим шагом после логики является анализReconcilerлогика. наши клишеDiffБольшая часть логики обновления происходит вReconcilerЭтап, который включает в себя большое количество вычислений и оптимизаций обновления компонентов.

проанализировано вышеSchedulerпроцесс планирования. и конкретно вSchedulerказнен вcallbackдаperformConcurrentWorkOnRoot. Давайте взглянем

// 被Scheduler调用的入口函数
function performConcurrentWorkOnRoot(root, didTimeout) {
    // 重置
    currentEventTime = NoWork;

    if (didTimeout) {
        // 任务已经超时
        const currentTime = requestCurrentTimeForUpdate();
        // 将过期时间标记为当前,以在单个批处理中同步处理已过期的工作。
        markRootExpiredAtTime(root, currentTime);
        // 调度一个同步任务
        ensureRootIsScheduled(root);
        return null;
    }

    // 获取下一个到期(更新)时间. 将以此作为本次渲染的执行必要性判断
    const expirationTime = getNextRootExpirationTimeToWorkOn(root);
    if (expirationTime !== NoWork) {
        const originalCallbackNode = root.callbackNode;

        // TODO:刷新被动的Hooks
        flushPassiveEffects();

        // 如果根或到期时间已更改,则丢弃现有堆栈并准备新的堆栈。 否则,我们将从中断的地方继续。
        if (
            root !== workInProgressRoot ||
            expirationTime !== renderExpirationTime
        ) {
            // [Q]: 重置数据;
            // 设置 renderExpirationTime 为expirationTime
            // 复制 root.current 为 workInProgress等
            prepareFreshStack(root, expirationTime);
            startWorkOnPendingInteractions(root, expirationTime);
        }

        if (workInProgress !== null) {
            // 省略...
            do {
                try {
                    workLoopConcurrent();
                    break;
                } catch (thrownValue) {
                    handleError(root, thrownValue);
                }
            } while (true);
            // 省略...
        }

        if (workInProgress !== null) {
            // 仍然有任务要做。说明是超时了,退出而不提交。
            stopInterruptedWorkLoopTimer();
        } else {
            stopFinishedWorkLoopTimer();

            const finishedWork: Fiber = ((root.finishedWork =
                root.current.alternate): any);
            root.finishedExpirationTime = expirationTime;
            // commit;开始 Renderer 流程
            finishConcurrentRender(
                root,
                finishedWork,
                workInProgressRootExitStatus,
                expirationTime,
            );
        }
    }
    return null;
}

Во-первых, он определит, истекло ли время ожидания задачи.Если время ожидания истекло, задача будет выполняться синхронно, чтобы предотвратить прерывание задачи. Если таймаута нет, тоprepareFreshStackВыполните некоторую работу по инициализации. затем вошелworkLoopConcurrentцикл.

prepareFreshStack

// 本次渲染的到期时间
let renderExpirationTime: ExpirationTime = NoWork;

function prepareFreshStack(root, expirationTime) {
    // 省略...
    if (workInProgress !== null) {
        // workInProgress 不为空说明之前有中断的任务。放弃
        let interruptedWork = workInProgress.return;
        while (interruptedWork !== null) {
            unwindInterruptedWork(interruptedWork);
            interruptedWork = interruptedWork.return;
        }
    }
    workInProgressRoot = root;
    // 从current 复制 wip; 并重置effectList
    workInProgress = createWorkInProgress(root.current, null);
    // 设置renderExpirationTime为下一个到期时间
    renderExpirationTime = expirationTime;
    // 省略...
}

если текущийwipНе пусто, что указывает на то, что в прошлый раз была прервана задача, путем непрерывного возврата до тех пор, покаrootnode для отмены прерванной задачи. Затем с того же времени фронт отFiberRootПолучите время истечения следующей задачи из , и назначьте егоrenderExpirationTimeВ качестве времени истечения для этого рендеринга.

workLoopConcurrent

workLoopConcurrentКод был размещен в начале этой статьи, так что давайте еще раз посмотрим здесь

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    // 第一次入参workInProgress为FiberRoot的Fiber
    // 后续将上一次返回值(子Fiber)作为入参
    workInProgress = performUnitOfWork(workInProgress);
  }
}

workLoopConcurrentРабота в основном круговое сравнениеcurrentа такжеworkInProgressдваFiberДерево. существуетwipсреда для измененияFiberотметкаeffectTag. При этом он будет обновляться/создаваться снизу вверхDOMузел, образующий закадровыйDOMдерево, наконец-то сдалиRendererиметь дело с.

«Рекурсия» на основе цикла

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

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
    // 旧的 Fiber, 用于对比
    const current = unitOfWork.alternate;

    // 省略...
    // [Q]: 处理当前Fiber节点,还回下一个子节点Fiber
    let next = beginWork(current, unitOfWork, renderExpirationTime);

    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    // 没有子节点
    if (next === null) {
        next = completeUnitOfWork(unitOfWork);
    }

    ReactCurrentOwner.current = null;
    return next;
}

// 尝试完成当前的Fiber,然后移至下一个同级。如果没有更多的同级,返回父fiber。
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
    workInProgress = unitOfWork;
    do {
        // 旧的 Fiber, 用于对比
        const current = workInProgress.alternate;
        const returnFiber = workInProgress.return;

        // Check if the work completed or if something threw.
        if ((workInProgress.effectTag & Incomplete) === NoEffect) {
            // [Q]: 创建/更新当前Fiber对应的节点实例
            let next = completeWork(current, workInProgress, renderExpirationTime);
            stopWorkTimer(workInProgress);
            resetChildExpirationTime(workInProgress);

            if (next !== null) {
                // 产生了新的子节点
                return next;
            }

            // [Q]:后面是在构建 effectList 的单向链表
            // 先省略...
        } else {
            // 有异常抛出。根据是否是boundary来决策是捕获还是抛出异常
            // 省略...
        }

        const siblingFiber = workInProgress.sibling;
        // 是否存在兄弟节点
        if (siblingFiber !== null) {
            return siblingFiber;
        }
        workInProgress = returnFiber;
    } while (workInProgress !== null);

    if (workInProgressRootExitStatus === RootIncomplete) {
        workInProgressRootExitStatus = RootCompleted;
    }
    return null;
}

сначала выполнитьbeginWorkВыполните операции узла и создайте дочерние узлы, дочерние узлы вернутся, чтобы статьnext,если有nextВернуть. вернуться кworkLoopConcurrentПозже,workLoopConcurrentОн будет судить, истек ли срок его действия, и если он еще не истек, метод будет вызван снова.

еслиnextЕсли его нет, то это означает, что текущий узел завершил обход дочерних узлов вниз, а значит, боковые ветви этого поддерева были пройдены, и эту часть работы можно завершить. воплощать в жизньcompleteUnitOfWork, в основном разделенный на несколько шагов

  1. completeUnitOfWorkпервый звонокcompleteWorkСоздать/обновить текущийFiberСоответствующий экземпляр узла (например, собственный узел DOM)instance, а обновленный сабFiberэкземпляр, вставленный вinstanceФормирует внеэкранное дерево рендеринга.
  2. в настоящее время естьFiberузел существуетeffectTagзатем добавьте его кeffectListсередина
  3. Узнайте, есть лиsiblingБратский узел, если есть, верните родственный узел, потому что у этого узла также могут быть дочерние узлы, вам нужно пройтиbeginWorkработать.
  4. Если нет родственного узла. продолжайте подниматься до тех пор, покаrootузел или находится в узле сsiblingродственный узел.
  5. Если наroot, то его возврат такжеnull, а это значит, что обход всего дерева закончился, можноcommit. Если в середине встречается родственный узел, он такой же, как и первый.3шаг

Текстовое выражение может быть не очень понятным, просто посмотрите на пример:

workLoopConcurrent

Порядок выполнения такой:

Текстовый узел «Привет» не будет выполнятьсяbeginWork/completeWork,потому чтоReactТолько для одного текстового дочернего узлаFiber, будут рассматриваться специально

1. App beginWork
2. div Fiber beginWork
3. span Fiber beginWork
4. span Fiber completeWork
5. div Fiber completeWork
6. p Fiber beginWork
7. p Fiber completeWork
8. App Fiber completeWork

beginWork

beginWorkАнализ передsetStateбыл проанализирован, когдаmountЛогика, соответствующая этапу. Так что здесь мы только анализируемupdateлогика. Давайте взглянемbeginWorkпримерно работа.

/**
 * @param {*} current 旧的Fiber
 * @param {*} workInProgress 新的Fiber
 * @param {*} renderExpirationTime 下一次到期时间,即本次渲染有效时间
 * @returns 子组件 Fiber
 */
function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
): Fiber | null {
    const updateExpirationTime = workInProgress.expirationTime;

    // 尝试复用 current 节点
    if (current !== null) {
        // 省略...
        // 复用 current
        return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderExpirationTime,
        );
    }

    workInProgress.expirationTime = NoWork;

    // 不能复用则 update 或者 mount
    switch (workInProgress.tag) {
        // 省略...
        case ClassComponent: {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps =
                workInProgress.elementType === Component
                    ? unresolvedProps
                    : resolveDefaultProps(Component, unresolvedProps);
            return updateClassComponent(
                current,
                workInProgress,
                Component,
                resolvedProps,
                renderExpirationTime,
            );
        }
        case HostRoot:
            return updateHostRoot(current, workInProgress, renderExpirationTime);
        case HostComponent:
            return updateHostComponent(current, workInProgress, renderExpirationTime);
        case HostText:
            return updateHostText(current, workInProgress);
        // 省略...    
    }
}

Мы продолжаем наш анализupdateClassComponentанализироватьupdateобработать.

function updateClassComponent(

    current: Fiber | null,
    workInProgress: Fiber,
    Component: any,
    nextProps,
    renderExpirationTime: ExpirationTime,
) {

    // 提前处理context逻辑。省略....

    // 组件的实例
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    if (instance === null) {
        // mount. wip.effectTag = Placement
        // 省略...
    } else {
        // update. wip.effectTag = Update | Snapshot
        // 调用 render 之前的生命周期,getDerivedStateFromProps | UNSAFE_componentWillReceiveProps(可能两次)
        // 接着调用shouldComponentUpdate判断是否需要更新
        // 最后更新props 和 state
        shouldUpdate = updateClassInstance(
            current,
            workInProgress,
            Component,
            nextProps,
            renderExpirationTime,
        );
    }
    // 执行 render 新建子Fiber。
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
    return nextUnitOfWork;
}

function finishClassComponent(
    current: Fiber | null,
    workInProgress: Fiber,
    Component: any,
    shouldUpdate: boolean,
    hasContext: boolean,
    renderExpirationTime: ExpirationTime,
) {
    // 引用应该更新,即使shouldComponentUpdate返回false
    markRef(current, workInProgress);

    const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;

    // 无需更新且没有发送错误则直接复用current
    if (!shouldUpdate && !didCaptureError) {
        if (hasContext) {
            invalidateContextProvider(workInProgress, Component, false);
        }
        // 复用current
        return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderExpirationTime,
        );
    }

    const instance = workInProgress.stateNode;

    // Rerender
    ReactCurrentOwner.current = workInProgress;
    let nextChildren = instance.render();

    // PerformedWork 提供给 React DevTools 读取
    workInProgress.effectTag |= PerformedWork;
    if (current !== null && didCaptureError) {
        // 出错了。
        // 省略...
    } else {
        reconcileChildren(
            current,
            workInProgress,
            nextChildren,
            renderExpirationTime,
        );
    }

    workInProgress.memoizedState = instance.state;

    if (hasContext) {
        invalidateContextProvider(workInProgress, Component, true);
    }

    return workInProgress.child;
}

export function reconcileChildren(
    current: Fiber | null,
    workInProgress: Fiber,
    nextChildren: any,
    renderExpirationTime: ExpirationTime,
) {
    if (current === null) {
        // mount的组件
        workInProgress.child = mountChildFibers(
            workInProgress,
            null,
            nextChildren,
            renderExpirationTime,
        );
    } else {
        // update的组件
        workInProgress.child = reconcileChildFibers(
            workInProgress,
            current.child,
            nextChildren,
            renderExpirationTime,
        );
    }
}

Наконец вернулсяworkInProgress.child,а такжеbeginWorkто же, согласноcurrent === nullразличатьmountа такжеupdate.

ФактическиmountChildFibersа такжеreconcileChildFibersоба указывают на одну и ту же функциюreconcileChildFibers. Разница во втором параметреcurrentFirstChild. еслиnull, создаст новыйFiberобъект, в противном случае повторное использование и обновлениеprops. НапримерreconcileSingleElementИспользуется для обработки случаев, когда имеется только один узел.

completeWork

function completeWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
): Fiber | null {
    const newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
        //省略...
        case HostComponent: {
            popHostContext(workInProgress);
            const rootContainerInstance = getRootHostContainer();
            const type = workInProgress.type;
            // fiber节点对应的DOM节点是否存在
            // update
            if (current !== null && workInProgress.stateNode != null) {
                // 为 wip 计算出新的 updateQueue
                // updateQueue 是一个奇数索引的值为变化的prop key,偶数索引的值为变化的prop value 的数组
                updateHostComponent(
                    current,
                    workInProgress,
                    type,
                    newProps,
                    rootContainerInstance,
                );

                if (current.ref !== workInProgress.ref) {
                    markRef(workInProgress);
                }
            } else {
                // mount
                if (!newProps) {
                    return null;
                }

                const currentHostContext = getHostContext();
                // 是不是服务端渲染
                let wasHydrated = popHydrationState(workInProgress);
                if (wasHydrated) {
                    // 省略...
                } else {
                    // 生成真实DOM
                    let instance = createInstance(
                        type,
                        newProps,
                        rootContainerInstance,
                        currentHostContext,
                        workInProgress,
                    );

                    // 将子孙DOM节点插入刚生成的DOM节点中,从下往上,构成一颗离屏DOM树
                    appendAllChildren(instance, workInProgress, false, false);

                    workInProgress.stateNode = instance;

                    // 与updateHostComponent类似的处理 props
                    if (
                        finalizeInitialChildren(
                            instance,
                            type,
                            newProps,
                            rootContainerInstance,
                            currentHostContext,
                        )
                    ) {
                        markUpdate(workInProgress);
                    }
                }

                if (workInProgress.ref !== null) {
                    markRef(workInProgress);
                }
            }
            return null;
        }
        //省略...
    }

}

первый иbeginWorkто же, согласноcurrent === nullСуждениеmountещеupdate.

updateКогда в основном выполняются следующие действия, конкретный исходный кодdiffProperties:

  • рассчитать новыйSTYLE prop
  • рассчитать новыйDANGEROUSLY_SET_INNER_HTML prop
  • рассчитать новыйCHILDREN prop

каждый раз новыйprop, будетpropKeyа такжеnextPropПары хранятся в массивеupdatePayloadсередина. наконецupdatePayloadназначить наwip.updateQueue.

mountВ то время есть много вещей, с которыми нужно иметь дело, которые примерно следующие:

  • createInstance: дляFiberУзел генерирует соответствующий реальныйDOMузел
  • appendAllChildren: потомкиDOMВставка узла только что созданаDOMв узле. Снизу вверх образуют полныйDOMДерево
  • finalizeInitialChildren: существуетsetInitialPropertiesОбработка регистрации событий в . существуетsetInitialDOMPropertiesсогласно сpropsинициализацияDOMАтрибуты

Стоит отметить, чтоappendAllChildrenметод. из-заcompleteWorkЭто относится к процессу поиска с возвратом, и каждый вызовappendAllChildrenсгенерированные потомкиDOMУзел вставляет текущий сгенерированныйDOMпод узел. затем при возврате к корнюrootузел, весьDOMДеревья обновлены.

effectList

каждый разcompleteWorkПосле этого он представляет, что узел был обработан. Я сказал раньше,Reconcilerотметит узел, который изменилсяeffectTag, для использования вRendererсудя по узлуeffectTagВыполнение конкретных обновлений.

Таким образом, вcompleteWorkверхняя функцияcompleteUnitOfWorkВ (то есть код, пропущенный ранее), каждый раз, когда он выполняетсяcompleteWorkбудет поддерживатьeffectListодносвязный список. если текущийFiberсуществуетeffectTag, затем вставьте в связанный список.

// 构建 effectList 的单向链表
if (
    returnFiber !== null &&
    (returnFiber.effectTag & Incomplete) === NoEffect
) {
    // firstEffect 为链表头结点
    if (returnFiber.firstEffect === null) {
        returnFiber.firstEffect = workInProgress.firstEffect;
    }
    // lastEffect 为链表尾节点
    if (workInProgress.lastEffect !== null) {
        if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
        }
        returnFiber.lastEffect = workInProgress.lastEffect;
    }

    const effectTag = workInProgress.effectTag;

    // 跳过NoWork和PerformedWork tag。后者是提供给React Tools读取
    if (effectTag > PerformedWork) {
        if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
        } else {
            returnFiber.firstEffect = workInProgress;
        }
        returnFiber.lastEffect = workInProgress;
    }
}

Слишком далеко,ReconcilerПроцесс заканчивается. Оглядываясь назад на резюме в начале, стало ли оно яснее~

Renderer(Commit)

CommitКод этапа относительно прост по сравнению с двумя другими. Его запись находится в функции ввода планирования задач, проанализированной ранее.performConcurrentWorkOnRootконецfinishConcurrentRender. Последняя вызываемая функцияcommitRootImpl. Взгляните на код:

let nextEffect: Fiber | null = null;

function commitRootImpl(root, renderPriorityLevel) {
    // 省略...
    const finishedWork = root.finishedWork;
    const expirationTime = root.finishedExpirationTime;
    if (finishedWork === null) {
        return null;
    }
    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;

    // commit不可中断。 总是同步完成。
    // 因此,现在可以清除这些内容以允许安排新的回调。
    root.callbackNode = null;
    root.callbackExpirationTime = NoWork;
    root.callbackPriority = NoPriority;
    root.nextKnownPendingLevel = NoWork;
    // 省略...

    // 获取effectList
    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) {
        if (finishedWork.lastEffect !== null) {
            finishedWork.lastEffect.nextEffect = finishedWork;
            firstEffect = finishedWork.firstEffect;
        } else {
            firstEffect = finishedWork;
        }
    } else {
        firstEffect = finishedWork.firstEffect;
    }

    if (firstEffect !== null) {
        // 省略...
        nextEffect = firstEffect;
        do {
            // [Q]: 执行 snapshot = getSnapshotBeforeUpdate()
            // 结果赋值为 Fiber.stateNode.instance.__reactInternalSnapshotBeforeUpdate = snapshot
            commitBeforeMutationEffects();
        } while (nextEffect !== null);
        // 省略...
        nextEffect = firstEffect;
        do {
            // [Q]: 根据Fiber.effectTag 执行具体的增删改DOM操作
            // 如果是卸载组件,还会调用 componentWillUnmount()
            commitMutationEffects(root, renderPriorityLevel);
        } while (nextEffect !== null);
        // 省略...
        nextEffect = firstEffect;
        do {
            // [Q]: 调用 render 后的生命周期
            // current === null ? componentDidMount : componentDidUpdate
            commitLayoutEffects(root, expirationTime);
        } while (nextEffect !== null);
        stopCommitLifeCyclesTimer();

        nextEffect = null;

        // 告诉Scheduler在帧末尾停止调度,这样浏览器就有机会绘制。
        requestPaint();
        // 省略...
    } else {
        // 省略...
    }
    // 省略...
    return null;
}

Много кода опущено, оставлено основное содержание. Основная логика состоит в том, чтобы получитьReconcilerподдерживаетсяeffectListПосле связанного списка просмотрите связанный список три раза и выполните следующие действия:

  1. ПолучатьSnapsshot; дляcomponentDidUpdateтретий параметр
  2. согласно сFiber.effectTagВыполнение определенных операций над компонентами или DOM
  3. Вызов функций жизненного цикла всех компонентов

commitBeforeMutationEffects

увидеть полный кодcommitBeforeMutationLifeCycles, где тайClassComponentОсновная логика компонента следующая:

const current = nextEffect.alternate;
finishedWork = nextEffect;
if (finishedWork.effectTag & Snapshot) {
    if (current !== null) {
        const prevProps = current.memoizedProps;
        const prevState = current.memoizedState;
        const instance = finishedWork.stateNode;
        const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
                ? prevProps
                : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
        );
        instance.__reactInternalSnapshotBeforeUpdate = snapshot;
    }
}

commitMutationEffects

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;

        if (effectTag & ContentReset) {
            // 把节点的文字内容设置为空字符串
            commitResetTextContent(nextEffect);
        }

        if (effectTag & Ref) {
            const current = nextEffect.alternate;
            if (current !== null) {
                // 把ref置空,后续会设置ref,所以之前ref上的值需要先清空
                commitDetachRef(current);
            }
        }
        let primaryEffectTag =
            effectTag & (Placement | Update | Deletion | Hydrating);
        switch (primaryEffectTag) {
            case Placement: {
                commitPlacement(nextEffect);
                // 从effectTag中清除Placement标记
                nextEffect.effectTag &= ~Placement;
                break;
            }
            case PlacementAndUpdate: {
                // Placement
                commitPlacement(nextEffect);
                nextEffect.effectTag &= ~Placement;

                // Update
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Update: {
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Deletion: {
                // componentWillUnmount
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break;
            }
            // 省略...
        }
        nextEffect = nextEffect.nextEffect;
    }
}

Кажется, нечего сказать. Стоит отметить, что перед запуском сначала будет вызыватьсяcommitDetachRefБудуrefСсылка очищается. Тогда для разныхeffectTagвыполнять различныеDOMработать.

  • commitPlacement;Добавить новый узел. Алгоритм расчета позиции вставки узла можно увидеть ниже;
  • commitWork; согласно сReconcilerсуществуетdiffPropertiesрассчитанныйupdateQueueмассив для выполненияDOMвозобновить
  • commitDeletion; Этот шаг вызовет каждый компонент в поддереве по очереди сверху внизcomponentWillUnmountфункция

commitLayoutEffects

function commitLayoutEffects(
    root: FiberRoot,
    committedExpirationTime: ExpirationTime,
) {
    while (nextEffect !== null) {
        setCurrentDebugFiberInDEV(nextEffect);

        const effectTag = nextEffect.effectTag;

        if (effectTag & (Update | Callback)) {
            recordEffect();
            const current = nextEffect.alternate;
            commitLayoutEffectOnFiber(
                root,
                current,
                nextEffect,
                committedExpirationTime,
            );
        }

        if (effectTag & Ref) {
            recordEffect();
            commitAttachRef(nextEffect);
        }

        resetCurrentDebugFiberInDEV();
        nextEffect = nextEffect.nextEffect;
    }
}

function commitLifeCycles(
    finishedRoot: FiberRoot,
    current: Fiber | null,
    finishedWork: Fiber,
    committedExpirationTime: ExpirationTime,
): void {
    switch (finishedWork.tag) {
        // ...
        case ClassComponent: {
            const instance = finishedWork.stateNode;
            if (finishedWork.effectTag & Update) {
                if (current === null) {
                    instance.componentDidMount();
                } else {
                    const prevProps =
                        finishedWork.elementType === finishedWork.type
                            ? current.memoizedProps
                            : resolveDefaultProps(finishedWork.type, current.memoizedProps);
                    const prevState = current.memoizedState;
                    instance.componentDidUpdate(
                        prevProps,
                        prevState,
                        instance.__reactInternalSnapshotBeforeUpdate,
                    );
                }
            }
            const updateQueue = finishedWork.updateQueue;
            if (updateQueue !== null) {
                // 调用setState注册的回调函数
                commitUpdateQueue(finishedWork, updateQueue, instance);
            }
            return;
        }
        // ...
    }
}

или перебирать каждыйFiberузел. еслиClassComponent, необходимо вызвать метод жизненного цикла. В то же время для обновленногоClassComponent, нужно судить о призванииsetStateЕсть ли callback-функция, если да, то ее нужно вызывать вместе здесь. в конце концов позвонитcommitAttachRefвозобновитьrefЦитировать.

CommitНа этом процесс этапа заканчивается.

чтобы быть откровенным,ReactИсходников там действительно много. Требуется много времени и сил, чтобы детально проанализировать каждый пункт. В этой статье анализируется только общий процесс, и многие детали не анализируются на месте. В будущем я займу некоторое время, чтобы изучить некоторые детали. В конечном счете, в настоящее время это только лицом к лицу, и не достигнуто эффекта анализа лицом к лицу. Многие взгляды являются личным пониманием, и они написаны для обучения и общения.