React и DOM-вещи — новый алгоритм узла

React.js

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

Эта статья является второй статьей, подробно объясняющей операции React DOM. Содержание статьи происходит на этапе фиксации.

Операция вставки DOM-узлов — это stateNode на файберном узле.Для файберного узла родного типа DOM stateNode хранит DOM-узел. Операция вставки узлов на этапе фиксации заключается в следовании дереву волокон для вставки узлов DOM в реальное дерево DOM.

commitPlacementэто запись для вставки узла,

function commitMutationEffectsImpl(
  fiber: Fiber,
  root: FiberRoot,
  renderPriorityLevel,
) {

  ...

  switch (primaryEffectTag) {
    case Placement: {
      // 插入操作
      commitPlacement(fiber);
      fiber.effectTag &= ~Placement;
      break;
    }

    ...

  }
}

Мы называем узел волокна, который необходимо вставить в целевой узел,commitPlacementФункция функции заключается в следующем:

  1. Найдите родительский узел на уровне DOM целевого узла (родительского)
  2. В соответствии с типом целевого узла найдите соответствующий родитель
  3. Если узел DOM, соответствующий целевому узлу, в настоящее время имеет только текстовое содержимое, что-то вроде<div>hello</div>, и содержит effectTag ContentReset (сброс содержимого), затем установите текстовое содержимое перед вставкой узла
  4. Найдите базовый узел
  5. выполнить вставку
function commitPlacement(finishedWork: Fiber): void {
  ...

  // 找到目标节点DOM层面的父节点(parent)
  const parentFiber = getHostParentFiber(finishedWork);

  // 根据目标节点类型,改变parent
  let parent;
  let isContainer;
  const parentStateNode = parentFiber.stateNode;
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case FundamentalComponent:
      if (enableFundamentalAPI) {
        parent = parentStateNode.instance;
        isContainer = false;
      }
  }
  if (parentFiber.effectTag & ContentReset) {
    // 插入之前重设文字内容
    resetTextContent(parent);
    // 删除ContentReset的effectTag
    parentFiber.effectTag &= ~ContentReset;
  }

  // 找到基准节点
  const before = getHostSibling(finishedWork);

  // 执行插入操作
  if (isContainer) {
    // 在外部DOM节点上插入
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    // 直接在父节点插入
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

Здесь нужно прояснить, куда вставляется узел DOM, т.В соответствии с типом целевого узла найдите соответствующий родитель.

еслиHostRootилиHostPortalТип узлов, во-первых, у них нет соответствующих узлов DOM, а во-вторых, они будут рендерить дочерние узлы DOM в соответствующие внешние узлы (containerInfo) при фактическом рендеринге. Итак, когда тип узла волокна - эти два, узел вставляется в этот внешний узел, а именно:

// 将parent赋值为fiber上的containerInfo
parent = parentStateNode.containerInfo

...

// 插入到外部节点(containerInfo)中
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent)

еслиHostComponent, затем вставьте непосредственно в его родительский узел DOM, то есть

// 直接在父节点插入
insertOrAppendPlacementNode(finishedWork, before, parent);

Мы видим, что когда вставка действительно выполняется,beforeУчаствовать, так что это делает?

Найдите опорные узлы

Когда React вставляет узел, есть два случая: есть ли у вновь вставленного узла DOM уже родственный узел в том месте, где он вставлен, если нет, выполнитьparentInstance.appendChild(child), да, звонитеparentInstance.insertBefore(child, beforeChild). этоbeforeChildчто упомянуто вышеbefore, который является базовым узлом вновь вставленного узла DOM, с помощью которого новый узел может быть вставлен в правильную позицию, когда родительский узел DOM уже имеет дочерние узлы. Представьте, что у вас уже есть дочерние узлыparentInstance.appendChild(child)Чтобы вставить, вставляет ли это новый узел в конце? Это явно неправильно. Поэтому очень важно найти позицию перед,before(базовый узел) черезgetHostSiblingфункция найти.

Проиллюстрируем на примереgetHostSiblingПринцип:

p — это вновь сгенерированный узел DOM. a — это существующий и неизменный узел DOM. Их положение в дереве волокон следующее: p нужно вставить в дерево DOM, и мы можем вывести окончательную форму дерева DOM на основе этого дерева волокон.

                    Fiber                    DOM

                   div#root                  div#root
                      |                         |
                    <App/>                     div
                      |                       /   \
                     div                     p     a
                    /   
                   /      
 Placement  -->   p ----> <Child/>
                             |
                             a

Можно видеть, что в дереве волокон a является родственным узлом родительского узла p, тогда как в дереве DOM p и a являются одноуровневыми узлами, и, наконец, p вставляется перед a.

Давайте выведем процесс в соответствии с приведенной выше схемой:

p имеет родственные узлы<Child/>, у него есть дочерний узел a, a является родным узлом DOM, а a уже существует в дереве DOM, то в качестве результата возвращается a, а p вставляется перед a.

В качестве другого примера, p также является недавно вставленным узлом, а h1 существует в дереве DOM как существующий узел.

                      Fiber           DOM

                     div#root         div#root
                        |                |
                      <App/>            div
                        |               /  \
                       div             p   h1
                      /   
                     /      
               <Child1/>--><Child2/>
                  |            |
                  |            |
 Placement  --->  p            h1

p не имеет родственных узлов, найдите его<Child1/>, у которого есть родственные узлы<Child2/>,<Child2/>Не родной узел DOM, найдите<Child2/>Дочерний узел находит h1, h1 является родным узлом DOM, а h1 уже существует в дереве DOM, затем в качестве результата возвращается h1, а p вставляется перед h1.

После двух примеров процесс поиска getHostSibling родственного узла DOM вновь вставленного узла можно резюмировать следующим образом:

  1. Приоритет отдается поиску одноуровневых узлов на одном уровне и фильтрации собственных компонентов DOM.
  2. Если его нельзя отфильтровать, найдите дочерние узлы узла того же уровня и отфильтруйте собственные компоненты DOM.
  3. Повторяйте процесс поиска одноуровневых узлов, а затем поиск дочерних узлов до тех пор, пока не будут найдены одноуровневые узлы.
  4. Найдите родительский узел вверх и повторите первые три шага для родительского узла.
  5. Пока родной DOM-узел не отфильтрован, если DOM-узел не является тем узлом, который нужно вставить, то он возвращается как результат, то есть находитсяbefore(базовый узел), новый узел необходимо вставить перед ним.

Существуют следующие правила:

需要插入的节点Если на том же уровне есть узел волокна и он является собственным узлом DOM, он должен быть вставлен перед этим узлом. Если родственный узел не является собственным узлом DOM, то он и дочерние узлы родственного узла在DOM层面是兄弟节点Отношение.

需要插入的节点Если нет родственного узла, то это дочерний узел родственного узла родительского узла.在DOM层面是兄弟节点Отношение.

Базовый узел и целевой узел являются братьями и сестрами в дереве DOM, и их положение должно быть после целевого узла.

Затем взгляните на его исходный код в соответствии с процессом, описанным выше:

function getHostSibling(fiber: Fiber): ?Instance {

  let node: Fiber = fiber;
  siblings: while (true) {

    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        // 代码执行到这里说明没有兄弟节点,并且新节点的父节点为DOM节点,
        // 那么它将作为唯一的节点,插入父节点
        return null;
      }
      // 如果父节点不为null且不是原生DOM节点,那么继续往上找
      node = node.return;
    }

    // 首先从兄弟节点里找基准节点
    node.sibling.return = node.return;
    node = node.sibling;

    // 如果node不是以下三种类型的节点,说明肯定不是基准节点,
    // 因为基准节点的要求是DOM节点
    // 会一直循环到node为dom类型的fiber为止。
    // 一旦进入循环,此时的node必然不是最开始是传进来的fiber
    while (
      node.tag !== HostComponent &&
      node.tag !== HostText &&
      node.tag !== DehydratedFragment
    ) {
      if (node.effectTag & Placement) {
        // 如果这个节点也要被插入,继续siblings循环,找它的基准节点
        continue siblings;
      }
      if (node.child === null || node.tag === HostPortal) {
        // node无子节点,或者遇到了HostPortal节点,继续siblings循环,
        // 找它的基准节点。
        // 注意,此时会再进入siblings循环,循环的开始,也就是上边的代码
        // 会判断这个节点有没有siblings,没有就向上找,有就从siblings里找。
        continue siblings;
      } else {
        // 过滤不出来原生DOM节点,但它有子节点,就继续往下找。
        node.child.return = node;
        node = node.child;
      }
    }

    if (!(node.effectTag & Placement)) {
      // 过滤出原生DOM节点了,并且这个节点不需要动,
      // stateNode就作为基准节点返回
      return node.stateNode;
    }
  }
}

В этот момент опорный узел найден, и далее выполняется операция вставки.

вставить узел

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

Мы можем вставить узел в родительский узелinsertOrAppendPlacementNodeНа основе функций, чтобы разобраться в процессе вставки.

                    Fiber
                   div#root
                      |
                    <App/>
                      |
                    div#App
                      |
Placement  -->     <Child/>
                    /
                   /
                  p ------> span ----- h1
                             |
                             a

теперь к<Child/>вставить вdiv#App, реальный процесс вставки состоит в том, чтобы сначала найти div#App в качестве родителя, а затем найти<Child/>Есть ли родственный узел, затем вызовитеinsertOrAppendPlacementNodeдля выполнения операции вставки. Давайте посмотрим, как это называется:

insertOrAppendPlacementNode(finishedWork, before, parent);

Есть три параметра:

  • FinishedWork: узел волокна, который нужно вставить, в настоящее время он<Child/>
  • до:<Child/>родственный узел, который в этом сценарии равен нулю
  • родитель:<Child/>Родительский узел , то естьdiv#App

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

Если это не собственный узел DOM, он<Child/>В него такую ​​штуку вставить нельзя, что делать? вниз, вырезать из своего ребенка, снова позвонитьinsertOrAppendPlacementNode, то есть вызывает себя рекурсивно, вставляя всех потомков в родителя. В примере p будет вставлен в parent.

В настоящее время<Child/>Все дочерние узлы p были вставлены. В это время снова будет найден диапазон одноуровневых узлов p, и он будет вставлен. Затем будет обнаружено, что у диапазона есть родственный узел h1, и h1 также будет быть вставлен.

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

Давайте посмотрим на описанный выше процессinsertOrAppendPlacementNodeисходный код

function insertOrAppendPlacementNode(
  node: Fiber,
  before: ?Instance,
  parent: Instance,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
    // 如果是原生DOM节点,直接进行插入操作
    const stateNode = isHost ? node.stateNode : node.stateNode.instance;
    if (before) {
      // 插入到基准节点之前
      insertBefore(parent, stateNode, before);
    } else {
      // 插入到父节点之下
      appendChild(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // HostPortal节点什么都不做
  } else {
    // 不是原生DOM节点,找它的子节点
    const child = node.child;
    if (child !== null) {
      // 对子节点进行插入操作
      insertOrAppendPlacementNode(child, before, parent);
      // 然后找兄弟节点
      let sibling = child.sibling;
      while (sibling !== null) {
        // 插入兄弟节点
        insertOrAppendPlacementNode(sibling, before, parent);
        // 继续检查兄弟节点
        sibling = sibling.sibling;
      }
    }
  }
}

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

Предполагая, что целевой узел не является собственным узлом DOM и имеет существующие одноуровневые узлы DOM (то есть ссылочный узел до span):

  • Есть дочерние узлы, и дочерние узлы вставляются в div. Окончательная форма DOM - это дерево DOM справа. Хотя p не тот же уровень, что и диапазон в дереве волокон, он находится на уровне DOM, поэтому его нужно вставить перед диапазоном Это значение перед в этом сценарии
                    Fiber                  DOM
                     div                     div
                      |                      / \
Placement  -->     <Child/>----> span       /   \
                      |                    p    span
                      |
                      p
  • Дочерние узлы вставляются, и в окончательном дереве DOM p, a и span являются одноуровневыми узлами, а p и a должны быть вставлены перед span по очереди, поэтому в этом сценарии также требуется before.
                    Fiber                  DOM
                     div                     div
                      |                      /|\
Placement  -->     <Child/>----> span       / | \
                      |                    p  a  span
                      |
                      p ----> a

Суммировать

Объединив проблемы фактической вставки узлов, нетрудно обобщить характеристики процесса вставки узлов на этапе фиксации:

  1. Найдите правильную позицию для вставки узла DOM
  2. Избегайте избыточной вставки узлов DOM

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

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

Таким образом, вы можете видеть, что в окончательном списке эффектов самый нижний узел находится вверху:

Выше приведен процесс вставки узлов DOM в соответствии с деревом волокон.