Покопайтесь в полной работе React

React.js

нажмитеВойдите в репозиторий отладки исходного кода React.

Обзор

Каждый волоконный узел при обновлении проходит две фазы: beginWork и completeWork. После разницы (см.Глубокое понимание принципа React Diff), узел workInProgress перейдет в фазу завершения. Все узлы workInProgress, полученные в это время, согласовываются с помощью алгоритма diff, что означает, что для узла в основном определена форма его волокна, но есть еще два момента:

  • В настоящее время изменилась только форма волокна, для волокна нативного DOM-компонента (HostComponent) и текстового узла (HostText) соответствующий DOM-узел (fiber.stateNode) не изменился.
  • Новый узел workInProgress, сгенерированный Diff, содержит флаг (т.е. effectTag).

Исходя из этих двух характеристик, работа CompleteWork в основном включает в себя:

  • создавать или обновлять узлы DOM,
    • В процессе построения первый слой первого слоя дочернего узла будет вставлен в текущий узел снизу вверх.
    • В процессе обновления будут вычислены атрибуты узла DOM.Как только атрибуты необходимо будет обновить, EffectTag обновления будет помечен для узла workInProgress, соответствующего узлу DOM.
  • Соберите список эффектов снизу вверх и, наконец, соберите в корне

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

Знания, рассматриваемые в этом разделе,

  • Создание и монтирование узла DOM
  • Обработка свойств DOM
  • Сбор списка эффектов
  • обработка ошибок

обработать

completeUnitOfWork — это вход в фазу completeWork. Внутри него есть цикл, который обходит узел workInProgress снизу вверх, по очереди обрабатывая узлы.

Для обычных узлов workInProgress будет выполняться completeWork. Это завершит работу, связанную с DOM, такую ​​как обновление свойств и событий привязки для компонента HostComponent.

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    if ((completedWork.effectTag & Incomplete) === NoEffect) {
      // 如果workInProgress节点没有出错,走正常的complete流程
      ...

      let next;

      // 省略了判断逻辑
      // 对节点进行completeWork,生成DOM,更新props,绑定事件
      next = completeWork(current, completedWork, subtreeRenderLanes);

      if (next !== null) {
        // 任务被挂起的情况,
        workInProgress = next;
        return;
      }

      // 收集workInProgress节点的lanes,不漏掉被跳过的update的lanes,便于再次发起调度
      resetChildLanes(completedWork);

      // 将当前节点的effectList并入父级节点
       ...

      // 如果当前节点他自己也有effectTag,将它自己
      // 也并入到父级节点的effectList
    } else {
      // 执行到这个分支说明之前的更新有错误
      // 进入unwindWork
      const next = unwindWork(completedWork, subtreeRenderLanes);
      ...

    }

    // 查找兄弟节点,若有则进行beginWork -> completeWork
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {

      workInProgress = siblingFiber;
      return;
    }
    // 若没有兄弟节点,那么向上回到父级节点
    // 父节点进入complete
    completedWork = returnFiber;
    // 将workInProgress节点指向父级节点
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 到达了root,整棵树完成了工作,标记完成状态
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

Поскольку большинство волоконных узлов React в конечном итоге будут воплощены в виде DOM, в этой статье в основном анализируется поток обработки, связанный с HostComponent.

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {

  ...

  switch (workInProgress.tag) {
    ...
    case HostComponent: {
      ...
      if (current !== null && workInProgress.stateNode != null) {
        // 更新
      } else {
        // 创建
      }
      return null;
    }
    case HostText: {
      const newText = newProps;
      if (current && workInProgress.stateNode != null) {
        // 更新
      } else {
        // 创建
      }
      return null;
    }
    case SuspenseComponent:
    ...
  }
}

Из структуры completeWork видно, что в зависимости от тега волокна выполняется различная обработка. Обработка HostComponent и HostText аналогична как для их узлов DOM, так и методы обработки делятся на обновление и создание. Если current существует и workInProgress.stateNode (экземпляр DOM, соответствующий узлу workInProgress) существует, это означает, что узел DOM узла workInProgress уже существует, и выполняется логика обновления, в противном случае он создается.

Обновление узла DOM на самом деле является обновлением атрибута, которое будет в следующемDOM属性的处理 -> 属性的更新Как уже упоминалось, давайте взглянем на создание и вставку узлов DOM.

Создание и вставка узла DOM

Мы знаем, что в настоящее время completeWork имеет дело с новым волокном, сгенерированным после алгоритма diff. Для нового волокна типа HostComponent оно может иметь или не иметь узлы DOM. Если нет, вам нужно сначала выполнить операцию создания, а затем вставки, тем самым вводя алгоритм вставки DOM.

if (current !== null && workInProgress.stateNode != null) {
    // 表明fiber有dom节点,需要执行更新过程
} else {
    // fiber不存在DOM节点
    // 先创建DOM节点
    const instance = createInstance(
      type,
      newProps,
      rootContainerInstance,
      currentHostContext,
      workInProgress,
    );

    //DOM节点插入
    appendAllChildren(instance, workInProgress, false, false);

    // 将DOM节点挂载到fiber的stateNode上
    workInProgress.stateNode = instance;

    ...

}

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

Графический алгоритм

В это время на этапе completeWork дерево workInProgress будет пройдено снизу вверх до корня, и каждый слой будет вставлен в DOM в соответствии с указанными выше правилами. Ниже приведен пример, чтобы понять этот процесс.

Это структура дерева волокон, и дерево workInProgress в конечном итоге станет этой формой.

  1              App
                  |
                  |
  2              div
                /
               /
  3        <List/>--->span
            /
           /
  4       p ----> 'text node'
         /
        /
  5    h1

Обход DFS при построении дерева workInProgress проходит весь путь от beginWork к узлам на этом пути. В это время он прошел самый глубокий узел h1, его beginWork закончился, и он начинает входить в стадию completeWork. , глубина иерархии — 5-й слой.

Уровень 5

  1              App
                  |
                  |
  2              div
                /
               /
  3        <List/>
            /
           /
  4       p
         /
        /
  5--->h1

В это время узел workInProgress указывает на волокно h1, а соответствующий ему узел dom — h1.После создания метки dom он входит вappendAllChildren, поскольку текущим узлом workInProgress является h1, его дочерний элемент имеет значение null, дочерние узлы не могут быть вставлены, и выход. Узел h1 завершает работу и возвращается к узлу p на уровне 4.

Домовое дерево в это время

      h1

Уровень 4

  1              App
                  |
                  |
  2              div
                /
               /
  3        <List/>
            /
           /
  4 --->  p ----> 'text node'
         /
        /
  5    h1

В это время узел workInProgress указывает на волокно p, а соответствующий ему узел dom — это p, введитеappendAllChildren, найдите, что дочерний узел p — это h1 и является компонентом HostComponent, вставьте h1 в p, а затем выясните, есть ли у дочернего узла h1 одноуровневые узлы того же уровня. Если не найдено, выйти.

Вся работа узла p завершена, а его сестринского узла: компонент 'текст' типа HostText будет использоваться в качестве следующей единицы работы, выполнить beginWork и затем войти в completeWork. Теперь его нужно выполнитьappendAllChildren, не нашел ребенка, Операции вставки не выполняются. Своя работа тоже сделана, возвращаемся к родительскому узлу<List/>, войдите на 3-й этаж

Домовое дерево в это время

        p      'text'
       /
      /
     h1

Уровень 3

  1              App
                  |
                  |
  2              div
                /
               /
  3 --->   <List/>--->span
            /
           /
  4       p ----> 'text'
         /
        /
  5    h1

В этот момент узел workInProgress указывает на<List/>Файбер, завершите работу над ним, поскольку в настоящее время он является пользовательским компонентом, не принадлежит HostComponent, поэтому он не будет вставлен в дочерний узел.

Найдите диапазон родственных узлов, выполните команду beginWork для диапазона, а затем выполните команду completeWork, выполните операцию вставки дочернего узла диапазона, обнаружите, что у него нет дочерних узлов, и выйдите. Вернитесь к div родительского узла и войдите во второй слой.

Домовое дерево в это время

                span

        p      'text'
       /
      /
     h1

Уровень 2

  1              App
                  |
                  |
  2 --------->   div
                /
               /
  3        <List/>--->span
            /
           /
  4       p ---->'text'
         /
        /
  5    h1

В это время узел workInProgress указывает на волокно элемента div, выполняет над ним команду completeWork и выполняет вставку дочернего узла элемента div. Поскольку его ребенок<List/>, не удовлетвореныnode.tag === HostComponent || node.tag === HostTextусловие, поэтому он не будет вставлен в div. продолжай смотреть вниз<List/>Потомок , находит p, вставляет P в div, затем ищет брата p, находит «текст» и также вставляет его в div. После этого равноправный узел уже не может быть найден, в это время он возвращается на третий уровень.<List/>узел.

<List/>Существует диапазон родственных узлов, который вставляет диапазон в div. Так как у span нет потомков, выйдите.

Домовое дерево в это время

             div
          /   |   \
         /    |    \
       p   'text'  span
      /
     /
    h1

1-го уровняВ это время узел workInProgress указывает на волокно приложения. Поскольку это пользовательский узел, операция вставки дочернего узла не будет выполняться на нем.

На данный момент дерево DOM в основном построено. В этом процессе мы можем заключить несколько правил:

  1. При вставке узла dom в узел вставляется только dom первого уровня его дочерних узлов. Вы можете думать об этой вставке как восходящем процессе сбора узлов DOM. Дом под первым слоем дочерних узлов был вставлен в первый слой дочерних узлов при вставке первого слоя дочерних узлов.Процесс выполнения работы слой за слоем снизу вверх аналогичен накоплению узлов дом.

  2. Всегда отдавайте приоритет тому, может ли он быть вставлен, а затем смотрите вниз, а затем находите родственный узел.

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

   fiber树      DOM树

   <App/>       div
     |           |
    div        input
     |
  <Input/>
     |
   input

Поскольку дочерний компонент нативного DOM-компонента может быть компонентом класса или функциональным компонентом, он сначала проверит себя и обнаружит, что он не является нативным компонентом DOM и не может быть вставлен в DOM, соответствующий родительскому волоконному узлу, поэтому он необходимо выполнить поиск до тех пор, пока не будет найден собственный компонент DOM, не будет выполнена вставка и, наконец, не будет найден узел волокна того же уровня из этого слоя, и узел того же уровня также будет выполняться.先自检,再检查下级,再检查下级的同级операция.

Видно, что вставка узлов также идет в глубину. Стоит отметить, что весь этот процесс вставки на самом деле не вставляет DOM в настоящую страницу, он просто работает с stateNode на волокне. Фактическая вставка в DOM происходит на этапе фиксации.

Исходный код вставки узла

Ниже приведен исходный код алгоритма узла вставки, который можно сравнить с описанным выше процессом.

  appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    // 找到当前节点的子fiber节点
    let node = workInProgress.child;
    // 当存在子节点时,去往下遍历
    while (node !== null) {
      if (node.tag === HostComponent || node.tag === HostText) {
        // 子节点是原生DOM 节点,直接可以插入
        appendInitialChild(parent, node.stateNode);
      } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
        appendInitialChild(parent, node.stateNode.instance);
      } else if (node.tag === HostPortal) {
        // 如果是HostPortal类型的节点,什么都不做
      } else if (node.child !== null) {
        // 代码执行到这,说明node不符合插入要求,
        // 继续寻找子节点
        node.child.return = node;
        node = node.child;
        continue;
      }
      if (node === workInProgress) {
        return;
      }
      // 当不存在兄弟节点时往上找,此过程发生在当前completeWork节点的子节点再无子节点的场景,
      // 并不是直接从当前completeWork的节点去往上找
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }
        node = node.return;
      }
      // 当不存在子节点时,从sibling节点入手开始找
      node.sibling.return = node.return;
      node = node.sibling;
    }
  };

Обработка свойств DOM

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

создание собственности

Создание атрибутов относительно просто обновить, этот процесс происходит в конце построения узла DOM, вызываяfinalizeInitialChildrenФункция завершает настройку свойств нового узла.

if (current !== null && workInProgress.stateNode != null) {
    // 更新
} else {
    ...
    // 创建、插入DOM节点的过程
    ...

    // DOM节点属性的初始化
    if (
      finalizeInitialChildren(
        instance,
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
      )
     ) {
       // 最终会依据textarea的autoFocus属性
       // 来决定是否更新fiber
       markUpdate(workInProgress);
     }
}

finalizeInitialChildrenв конце концов позвонитsetInitialProperties, чтобы завершить настройку свойства. Этот процесс понять несложно, главное вызватьsetInitialDOMPropertiesЗадавайте свойства непосредственно в узлах DOM (на этом этапе события привязываются)

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // 设置行内样式
      setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 设置innerHTML
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    }
     ...
     else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      // 绑定事件
      if (nextProp != null) {
        ensureListeningTo(rootContainerElement, propKey);
      }
    } else if (nextProp != null) {
      // 设置其余属性
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

обновление атрибута

Если существующий DOM-узел обновляется, это означает, что нужно обновить только атрибут, потому что узел уже существует, и нет ни удаления, ни добавления.updateHostComponentФункция отвечает за обновление атрибутов DOM-узла, соответствующего HostComponent, и в коде не так много для понимания.

  updateHostComponent = function(
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    // 新旧props相同,不更新
    if (oldProps === newProps) {
      return;
    }

    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();

    // prepareUpdate计算新属性
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );

    // 最终新属性被挂载到updateQueue中,供commit阶段使用
    workInProgress.updateQueue = (updatePayload: any);

    if (updatePayload) {
      // 标记workInProgress节点有更新
      markUpdate(workInProgress);
    }
  };

Видно, что он делает только одно — вычисляет новый атрибут и монтирует его в updateQueue узла workInProgress.

[ 'style', { color: 'blue' }, title, '测试标题' ]

Этот результат даетсяdiffPropertiesВычисляемый, он сравнивает lastProps и nextProps для вычисления updatePayload.

Например, в следующем компоненте событие щелчка, привязанное к div, изменит его свойства.

class PropsDiff extends React.Component {
    state = {
        title: '更新前的标题',
        color: 'red',
        fontSize: 18
    }
    onClickDiv = () => {
        this.setState({
            title: '更新后的标题',
            color: 'blue'
        })
    }
    render() {
        const { color, fontSize, title } = this.state
        return <div
            className="test"
            onClick={this.onClickDiv}
            title={title}
            style={{color, fontSize}}
            {...this.state.color === 'red' && { props: '自定义旧属性' }}
        >
            测试div的Props变化
        </div>
    }
}

lastProps и nextProps соответственно

lastProps
{
  "className": "test",
  "title": "更新前的标题",
  "style": { "color": "red", "fontSize": 18},
  "props": "自定义旧属性",
  "children": "测试div的Props变化",
  "onClick": () => {...}
}

nextProps
{
  "className": "test",
  "title": "更新后的标题",
  "style": { "color":"blue", "fontSize":18 },
  "children": "测试div的Props变化",
  "onClick": () => {...}
}

Что они изменили, так это то, что propsKeystyle、title、props, после diff окончательный напечатанный файл updatePayload равен

[
   "props", null,
   "title", "更新后的标题",
   "style", {"color":"blue"}
]

diffPropertiesВнутренние правила можно резюмировать следующим образом:

Если есть свойство (propKey), оно находится в

  • Он существует в lastProps, но не существует в nextProps. Отметьте значение propKey как нулевое, чтобы указать на удаление.
  • Не существует в lastProps, существует в nextProps, добавьте propKey и соответствующее значение в nextProps для updatePayload
  • Он существует в lastProps, а также существует в nextProps. Добавьте propKey и соответствующее значение в nextProps для updatePayload.

Взгляните на исходный код против этого правила:

export function diffProperties(
  domElement: Element,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
  rootContainerElement: Element | Document,
): null | Array<mixed> {

  let updatePayload: null | Array<any> = null;

  let lastProps: Object;
  let nextProps: Object;

  ...

  let propKey;
  let styleName;
  let styleUpdates = null;

  for (propKey in lastProps) {
    // 循环lastProps,找出需要标记删除的propKey
    if (
      nextProps.hasOwnProperty(propKey) ||
      !lastProps.hasOwnProperty(propKey) ||
      lastProps[propKey] == null
    ) {
      // 对propKey来说,如果nextProps也有,或者lastProps没有,那么
      // 就不需要标记为删除,跳出本次循环继续判断下一个propKey
      continue;
    }
    if (propKey === STYLE) {
      // 删除style
      const lastStyle = lastProps[propKey];
      for (styleName in lastStyle) {
        if (lastStyle.hasOwnProperty(styleName)) {
          if (!styleUpdates) {
            styleUpdates = {};
          }
          styleUpdates[styleName] = '';
        }
      }
    } else if(/*...*/) {
      ...
      // 一些特定种类的propKey的删除
    } else {
      // 将其他种类的propKey标记为删除
      (updatePayload = updatePayload || []).push(propKey, null);
    }
  }
  for (propKey in nextProps) {
    // 将新prop添加到updatePayload
    const nextProp = nextProps[propKey];
    const lastProp = lastProps != null ? lastProps[propKey] : undefined;
    if (
      !nextProps.hasOwnProperty(propKey) ||
      nextProp === lastProp ||
      (nextProp == null && lastProp == null)
    ) {
      // 如果nextProps不存在propKey,或者前后的value相同,或者前后的value都为null
      // 那么不需要添加进去,跳出本次循环继续处理下一个prop
      continue;
    }
    if (propKey === STYLE) {
      /*
      * lastProp: { color: 'red' }
      * nextProp: { color: 'blue' }
      * */
      // 如果style在lastProps和nextProps中都有
      // 那么需要删除lastProps中style的样式
      if (lastProp) {
        // 如果lastProps中也有style
        // 将style内的样式属性设置为空
        // styleUpdates = { color: '' }
        for (styleName in lastProp) {
          if (
            lastProp.hasOwnProperty(styleName) &&
            (!nextProp || !nextProp.hasOwnProperty(styleName))
          ) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = '';
          }
        }
        // 以nextProp的属性名为key设置新的style的value
        // styleUpdates = { color: 'blue' }
        for (styleName in nextProp) {
          if (
            nextProp.hasOwnProperty(styleName) &&
            lastProp[styleName] !== nextProp[styleName]
          ) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = nextProp[styleName];
          }
        }
      } else {
        // 如果lastProps中没有style,说明新增的
        // 属性全部可放入updatePayload
        if (!styleUpdates) {
          if (!updatePayload) {
            updatePayload = [];
          }
          updatePayload.push(propKey, styleUpdates);
          // updatePayload: [ style, null ]
        }
        styleUpdates = nextProp;
        // styleUpdates = { color: 'blue' }
      }
    } else if (/*...*/) {
      ...
      // 一些特定种类的propKey的处理
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        // 重新绑定事件
        ensureListeningTo(rootContainerElement, propKey);
      }
      if (!updatePayload && lastProp !== nextProp) {
        // 事件重新绑定后,需要赋值updatePayload,使这个节点得以被更新
        updatePayload = [];
      }
    } else if (
      typeof nextProp === 'object' &&
      nextProp !== null &&
      nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
    ) {
      // 服务端渲染相关
      nextProp.toString();
    } else {
       // 将计算好的属性push到updatePayload
      (updatePayload = updatePayload || []).push(propKey, nextProp);
    }
  }
  if (styleUpdates) {
    // 将style和值push进updatePayload
    (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
  }
  console.log('updatePayload', JSON.stringify(updatePayload));
  // [ 'style', { color: 'blue' }, title, '测试标题' ]
  return updatePayload;
}

Различие атрибутов узла DOM монтирует updateQueue с новыми атрибутами для узла workInProgress. Если updateQueue узла не пуста, он будет помечен тегом EffectTag обновления, и на этапе фиксации будет обрабатываться updateQueue.

if (updatePayload) {
  markUpdate(workInProgress);
}

Коллекция цепочек эффектов

После beginWork и описанных выше операций с DOM измененный узел workInProgress помечен тегом effectTag.

Как только узел workInProgress содержит тег effectTag, его необходимо обработать на этапе фиксации. Каждый узел workInProgress имеет firstEffect и lastEffect, которые представляют собой односвязный список, представляющий себя и все узлы workInProgress, которые содержат effectTag на своих дочерних узлах. Этап completeWork также собирает цепочку эффектов слой за слоем в процессе восходящего обхода и, наконец, собирает ее в корне для использования на следующем этапе фиксации.

Реализация относительно проста.Для узла workInProgress он сначала объединяет свой существующий effectList с родительским узлом, а затем определяет, есть ли у него effectTag, и если да, то также объединяет его с родительским узлом.

 /*
* effectList是一条单向链表,每完成一个工作单元上的任务,
* 都要将它产生的effect链表并入
* 上级工作单元。
* */
// 将当前节点的effectList并入到父节点的effectList
if (returnFiber.firstEffect === null) {
  returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
  }
  returnFiber.lastEffect = completedWork.lastEffect;
}

// 将自身添加到effect链,添加时跳过NoWork 和
// PerformedWork的effectTag,因为真正
// 的commit用不到
const effectTag = completedWork.effectTag;

if (effectTag > PerformedWork) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork;
  } else {
    returnFiber.firstEffect = completedWork;
  }
  returnFiber.lastEffect = completedWork;
}

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

обработка ошибок

Обработка ошибок в completeUnitWork является частью механизма ограничения ошибок.

Границы ошибок — это компонент React, когда-то использовавшийся в компоненте класса.getDerivedStateFromErrorилиcomponentDidCatch, он может перехватывать ошибки, возникающие в его поддереве, то это граница ошибок.

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

if ((completedWork.effectTag & Incomplete) === NoEffect) {

} else {
  // 有Incomplete的节点会进入到这个判断分支进行错误处理
}

Откуда берется неполный

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

Функция рендеринга в параллельном режиме: В renderRootConcurrent при построении дерева workInProgress try...catch используется для обертывания функции выполнения, что дает возможность иметь дело с узлами, сообщающими об ошибках.

do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);

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

throwExceptionОтметит этот неправильный узел workInProgressIncomplete 的 effectTag, указывая на то, что он не завершен, найдите узел, который может обработать ошибку (т. е. границу ошибки) вверх, и добавьте effectTag для ShouldCapture. Кроме того, создайте обновление, представляющее ошибку,getDerivedStateFromErrorположить полезную нагрузку,componentDidCatchВключите обратный вызов. UpdateQueue последнего узла в очереди обновлений.

throwExceptionПосле завершения выполнения вернитесь к узлу workInProgress, где произошла ошибка, и выполнитеcompleteUnitOfWork, цель состоит в том, чтобы завершить ошибку на текущем узле, потому что он сам по себе неверен, и нет смысла рендерить вниз.

function handleError(root, thrownValue):void {
  ...

  // 给当前出错的workInProgress节点添加上 Incomplete 的effectTag
  throwException(
    root,
    erroredWork.return,
    erroredWork,
    thrownValue,
    workInProgressRootRenderLanes,
  );

  // 开始对错误节点执行completeWork阶段
  completeUnitOfWork(erroredWork);

  ...

}

Важно: Найдите границу ошибки от узла, где произошла ошибка, и сделайте отметку.Отметка является тегом effectTag функции ShouldCapture.

Границы ошибки снова обновлены

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

Что делает логика обработки ошибок:

  • Выполнить на неисправном узлеunwindWork.
  • Отметьте родительский узел (returnFiber) узла ошибки наIncomplete, цель состоит в том, что когда родительский узел выполняется для завершенияUnitOfWork, он также может быть выполнен unwindWork, а затем проверить, является ли это границей ошибки.
  • Очищает цепочку эффектов на родительском узле узла ошибки.

Дело в том, чтоunwindWorkПроверим, является ли узел границей ошибки, давайте взглянем на ключевой код unwindWork:

function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
  switch (workInProgress.tag) {
    case ClassComponent: {

      ...

      const effectTag = workInProgress.effectTag;
      if (effectTag & ShouldCapture) {
        // 删它上面的ShouldCapture,再打上DidCapture
        workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;

        return workInProgress;
      }
      return null;
    }
    ...
    default:
      return null;
  }
}

unwindWorkОснованием для проверки того, что узел является границей ошибки, является то, что узел только чтоthrowExceptionКогда помечен тег effectTag для ShouldCapture. Если проверка прошла успешно, он в конечном итоге будет возвращен. А после возвращения? Будет присвоен узлу workInProgress, давайте посмотрим на общую логику обработки ошибок:

if ((completedWork.effectTag & Incomplete) === NoEffect) {

    // 正常流程
    ...

} else {
  // 验证节点是否是错误边界
  const next = unwindWork(completedWork, subtreeRenderLanes);

  if (next !== null) {
    // 如果找到了错误边界,删除与错误处理有关的effectTag,
    // 例如ShouldCapture、Incomplete,
    // 并将workInProgress指针指向next
    next.effectTag &= HostEffectMask;
    workInProgress = next;
    return;
  }

  // ...省略了React性能分析相关的代码

  if (returnFiber !== null) {
    // 将父Fiber的effect list清除,effectTag标记为Incomplete,
    // 便于它的父节点再completeWork的时候被unwindWork
    returnFiber.firstEffect = returnFiber.lastEffect = null;
    returnFiber.effectTag |= Incomplete;
  }
}

...
// 继续向上completeWork的过程
completedWork = returnFiber;

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

  if (next !== null) {
    next.effectTag &= HostEffectMask;
    workInProgress = next;
    return;
  }

Важно: Укажите узел workInProgress на границу ошибки, чтобы можно было повторить процесс обновления для границы ошибки.

В это время узел workInProgress имеет значение и выпрыгивает из completeUnitOfWork, затем продолжает самый внешний рабочий цикл:

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

В этот момент узел workInProgress, который является границей ошибки, будетЗатем он обрабатывается с помощью PerformUnitOfWork, а затем входит в beginWork, CompleteWork!

То есть он будет повторно обновлен один раз. Почему говорят, что он снова обновляется? Потому что при построении дерева workInProgress beginWork идет сверху вниз, и когда на него указывает указатель workInProgress, выполняется только beginWork. В это время, когда дочерний узел делает ошибку и завершает UnitOfWork вверх, обнаруживается, что это граница ошибки, и workInProgress снова указывает на нее, поэтому он снова начинает работу. Разница в том, что на этот раз узел содержит effectTag DidCapture. Так что процесс другой.

все еще помнюthrowExceptionStage Enqueue Error Обновление очереди обновления границы, представляющее ошибку? Он будет обработан, когда в этот раз beginWork вызовет processUpdateQueue. Это гарантируетgetDerivedStateFromErrorа такжеcomponentDidCatch, а затем создать новое состояние, которое представляет состояние этой ошибки.

Граница ошибки — это компонент класса, который будет выполняться на этапе beginWork.finishClassComponent, если будет установлено, что компонент имеет DidCapture, все его дочерние узлы будут выгружены, а затем будут повторно отображены новые дочерние узлы.Эти дочерние узлы могут быть альтернативным пользовательским интерфейсом, отображаемым при обработке ошибок.

Пример кода из ReactВведение в границы ошибок

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

В приведенном выше случае, когда в узле поддерева ErrorBoundary возникает ошибка, компонент вgetDerivedStateFromErrorа такжеcomponentDidCatchбудет запущен, а альтернативный пользовательский интерфейс в это время:

<h1>Something went wrong.</h1>

Сортировка процессов

Давайте воспользуемся диаграммой, чтобы разобраться с описанной выше обработкой ошибок.<Example/>Имеет возможности обработки ошибок.

  1              App
                  |
                  |
  2           <Example/>
                /
               /
  3 --->   <List/>--->span
            /
           /
  4       p ----> 'text'
         /
        /
  5    h1

1. Если<List/>ошибка обновления, то сначалаthrowExceptionОн будет помечен Incomplete effectTag, а затем начнется с его родительского узла, чтобы найти узел, который может обрабатывать ошибки.

2. найдено<Example/>, он может обработать ошибку, пометить ее тегом effectTag функции ShouldCapture (отметить ее), создать неправильное обновление иgetDerivedStateFromErrorположить полезную нагрузку,componentDidCatchВключите обратный вызов. , поставить в очередь<Example/>очередь обновлений.

3. От<List/>начать прямоcompleteUnitOfWork. Пойдет, так как у него есть IncompleteunwindWork, а затем дать его родительский узел<Example/>Нажмите Незавершенный,unwindWorkобнаруживает, что это не та граница, которую только что отметили, и продолжает движение вверхcompleteUnitOfWork.

4.<Example/>Неполный, введитеunwindWork, и это неправильный граничный узел, который только что был отмечен, удалите ShouldCapture и добавьте DidCapture и наведите указатель workInProgress на<Example/>

5.<Example/>Повторно введите beginWork, чтобы обработать updateQueue, согласовать дочерние узлы (выгрузить исходные дочерние узлы, отобразить резервный пользовательский интерфейс).

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

это вthrowExceptionЭто отражено в функции, которая должна найти границу ошибки от своего родительского узла вверх:

// 从当前节点的父节点开始向上找
let workInProgress = returnFiber;

do {
  ...
} while (workInProgress !== null);

Вернемся к completeWork, что он делает в общей обработке ошибок, так это обрабатывает узлы в пределах границы ошибки:

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

Выше мы проанализировали обработку ошибок только в общих сценариях, на самом деле, когда задача приостановлена ​​(Suspense), логика обработки ошибок также будет соблюдена, потому что значение ошибки throw в это время является доступным объектом, который будет подробно объяснил при анализе саспенса.

Суммировать

Давайте рассмотрим основные вещи, сделанные на этапе completeWork узла workInProgress:

  • Создание и монтирование реальных узлов DOM
  • Обработка свойств DOM
  • Сбор списка эффектов
  • обработка ошибок

Хотя на обработку ошибок уходит много места, все же необходимо сосредоточиться на обработке обычных узлов. Стадия completeWork находится после beginWork и перед фиксацией, которая играет связующую роль. Он получает узел волокна после diff, а затем подготавливает узел DOM и список эффектов. Поскольку фазу фиксации нельзя прервать, хорошая подготовка помогает фазе фиксации выполнять меньше работы.

Как только все узлы дерева workInProgress будут завершены, это означает, что дерево workInProgress было построено и все работы по обновлению завершены.Далее дерево войдет в стадию фиксации.В следующей статье мы проанализируем различные этапы этап фиксации Процесс.