Понимание исходного кода React I
Понимание исходного кода React II
Понимание исходного кода React III
Понимание исходного кода React IV
Исходный код React V (этот)
Понимание исходного кода React VI
Понимание исходного кода VII
Понимание исходного кода VIII
Понимание Исходный код React IX

В прошлый раз мы завершили верхнюю половину логики рендеринга компонентов класса, которая похожа на рендеринг простого компонента, хотя и отличается от нее, в следующих отношениях: 1) он создает один дополнительный ReactCompositeComponent для представления компонента класса (App); и 2) он вызывает App.render(), который запускает каскадные React.createElement() для создания ReactElement дерева.

На этот раз мы собираемся исследовать больше ветвей в нижней половине, исследуя, как ReactElement в дереве преобразуются в соответствующие ReactDOMComponent и, в конечном итоге, в реальные объекты DOM.

Файлы, использованные в этой статье:

renderers / dom / shared / ReactDOMComponent.js: предлагает один из методов, на которых фокусируется этот пост, _createInitialChildren

renderers / dom / client / utils / setTextContent.js: операция DOM, установка текста

renderers / dom / client / utils / DOMLazyTree.js: операция DOM, добавление дочернего элемента

renderers / shared / stack / reconciler / ReactMultiChild.js: промежуточный метод для traverseAllChildren, а также другой метод в фокусах, mountChildren()

shared / utils / traverseAllChildren.js: метод, который выполняет итерацию прямых вложенных ReactElements и создает их соответствующие ReactDOMComponents

Обозначения, используемые в стеке вызовов:
цикл
? условие

Я использую {} для ссылки на предыдущий пост, имеющий отношение к обсуждаемым методам (или логическому процессу).

Процесс, обсуждаемый в этом посте, в основном происходит в ReactDOMComponent[6].mountComponent(). Основная задача этого метода, который извлекает объект DOM из ReactDOMComponent[6], описана в {посте три}. Эта задача пронумерована как 0) для дальнейшего использования.

В этом посте мы собираемся обратиться к одному из методов, который мы специально упустили из виду в прошлый раз, _createInitialChildren, который используется для обработки недавно представленного ReactElement дерева как потомков компонента класса. Он использовался в нишевом кейсе, дочернем тексте, в {посте три * 7}, и срабатывала только боковая ветвь. Эта боковая ветка, а также весь метод будут полностью обсуждены в этом посте.

_createInitialChildren - наш главный герой сегодня; Пожалуйста, введите * 7 в третьем посте, если хотите проверить его роль в рендеринге простого компонента. Другой упущенный из виду метод _updateDOMProperties в {посте три * 6} будет обсуждаться в следующих статьях.

Чтобы быть более конкретным, этот метод 1) преобразует ReactElement в соответствующие им ReactDOMComonent; 2) (рекурсивно) вызывает ReactDOMComponent[*].mountComponent() для создания объектов DOM; и 3) добавляет их к корневому объекту DOM, который создается в 0).

Итак, сначала давайте повторим шаг 0) в контексте компонента класса.

`ReactDOMComponent[6].mountComponent() ` (перед `_createInitialChildren`)—create the DOM element[6]

подсказка для экономии времени: этот абзац предназначен для того, чтобы сообщение оставалось самодостаточным, детали процесса создания `ReactDOMComponent` были рассмотрены в { сообщении три })

Обозначенная структура данных:

Стек вызовов в действии:

...
|~mountComponentIntoNode()                                    |
  |-ReactReconciler.mountComponent()                          |
    |-ReactCompositeComponent[T].mountComponent()             |
      |-ReactCompositeComponent[T].performInitialMount()  upper half
        |-ReactReconciler.mountComponent()                    |
          |-ReactCompositeComponent[ins].mountComponent()     |
            |-this.performInitialMount()                      |
              |-this._renderValidatedComponent()              |
              |-instantiateReactComponent()                  _|_ 
                (we are here)                                 |
              |-ReactDOMComponent[6].mountComponent(          |
                  transaction, // scr: -----> not of interest |
                  hostParent,  // scr: -----> null            |
                  hostContainerInfo,// scr:---------------------> ReactDOMContainerInfo[ins]                                lower half
                  context      // scr: -----> not of interest |
                )                                             |
...

На этом шаге создается объект DOM с ReactDOMComponent[6] и настраиваются его атрибуты.

Напомним: он 1) инициализирует свойства ReactDOMComponent[6]; 2) создает элемент div DOM, используя document.createElement(); 3) создает двойную связь между ReactDOMComponent[6] и объектом DOM; 4) и 5) установить свойства и атрибуты вновь созданного объекта DOM; и 6) встраивает объект DOM в DOMLazyTree[1].

mountComponent: function (
  transaction,
  hostParent,
  hostContainerInfo,
  context
) {
// scr: --------------------------------------------------------> 1)
  this._rootNodeID = globalIdCounter++;
  this._domID = hostContainerInfo._idCounter++;
  this._hostParent = hostParent;
  this._hostContainerInfo = hostContainerInfo; // scr: ------------> ReactDOMContainerInfo[ins]
  var props = this._currentElement.props;
  switch (this._tag) { // scr: ---> no condition is met here
...
  }
... // scr: -----> sanity check
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
  var namespaceURI;
  var parentTag;
  if (hostParent != null) { // scr: -----> it is null
...
  } else if (hostContainerInfo._tag) {
    namespaceURI = hostContainerInfo._namespaceURI; // scr: -------> "http://www.w3.org/1999/xhtml"
    parentTag = hostContainerInfo._tag;        // scr: ------> "div"
  }
  if (namespaceURI == null || 
      namespaceURI === DOMNamespaces.svg && 
      parentTag === 'foreignobject'
  ) { // scr: -----> no
...
  }
  if (namespaceURI === DOMNamespaces.html) {
    if (this._tag === 'svg') {               // scr: -----> no
...
    } else if (this._tag === 'math') {       // scr: -----> no
...
    }
  }
  this._namespaceURI = namespaceURI;  // scr: ---------------------> "http://www.w3.org/1999/xhtml"
... // scr: ------> DEV code
  var mountImage;
  if (transaction.useCreateElement) { // scr: ---------------------> transaction related logic, we assume it is true
    var ownerDocument = hostContainerInfo._ownerDocument;
    var el;
    if (namespaceURI === DOMNamespaces.html) {
      if (this._tag === 'script') {         // scr: -----> no
...
      } else if (props.is) {                // scr: -----> no
...
      } else {
        // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
        // See discussion in https://github.com/facebook/react/pull/6896
        // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
// scr: --------------------------------------------------------> 2)
        // scr: ---------> HTML DOM API
        el = ownerDocument.createElement(this._currentElement.type);
      }
    } else { // scr: ------> no
...
    }
// scr: --------------------------------------------------------> 3)
    ReactDOMComponentTree.precacheNode(this, el); // scr: --------> doubly link (._hostNode & .internalInstanceKey)
    this._flags |= Flags.hasCachedChildNodes; // scr: ------------>
bit wise its flags
// scr: --------------------------------------------------------> 4)
    if (!this._hostParent) { // scr: ------> it is the root element
      DOMPropertyOperations.setAttributeForRoot(el); // scr: -----> data-reactroot
    }
// scr: --------------------------------------------------------> 5)
    this._updateDOMProperties( //*6
      null,
      props,
      transaction
    ); // scr: --------------------------> style:{ “color”: “blue” }
// scr: --------------------------------------------------------> 6)
    var lazyTree = DOMLazyTree(el); // scr: ------> DOMLazyTree[ins]
    this._createInitialChildren(transaction, props, context, lazyTree);
...
  } // if (transaction.useCreateElement)
  return mountImage;
}
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

ReactDOMComponent[6] получает свой узел DOM, его дочерние элементы следующие в строке

`ReactDOMComponent[6]._createInitialChildren () `—создание элементов DOM [2-5]

Обозначенная структура данных:

Как упоминалось ранее, этот метод использовался для создания строкового дочернего узла (‘hello world’) в {посте три}. Мы будем повторно использовать эту боковую ветвь в этом посте при рендеринге похожих узлов (то есть [3] и [5]), и мы назовем ветку {1}.

В случае компонента класса маршрут {2} выполняется при первом обращении к этому методу. Если быть точным, эта ветвь обрабатывает ReactElement дерево. Как уже упоминалось, он 1) преобразует ReactElements в ReactDOMComponents (a), генерирует узлы DOM с ReactDOMComponents (b) и 2) вставляет узлы DOM в корневой узел DOM, сгенерированный с ReactDOMComponent[6] на последнем шаге.

_createInitialChildren: function (
  transaction, // scr: not of interest
  props,       // scr: -------------------> ReactElement[6].props
  context,     // scr: not of interest
  lazyTree     // scr: -------------------> DOMLazyTree[ins]
)
{
  // Intentional use of != to avoid catching zero/false.
  // scr: it is named as 'dangerous', let's avoid touching it
  var innerHTML = props.dangerouslySetInnerHTML;
  if (innerHTML != null) { // scr: so no innerHTML
...
  } else {
    var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
    var childrenToUse = contentToUse != null ? null : props.children;
    // scr: some comments
    if (contentToUse != null) {
      // scr: some comments
      if (contentToUse !== '') { // scr: ----------------> route {1}
...// scr: DEV code
        DOMLazyTree.queueText(lazyTree, contentToUse);
      }
    } else if (childrenToUse != null) { // scr: ---------> route {2}
      var mountImages = this.mountChildren(childrenToUse, transaction, context);  // scr: --------------------------------> 1)
      for (var i = 0; i < mountImages.length; i++) { scr: ------> 2)
        DOMLazyTree.queueChild(lazyTree, mountImages[i]);
      }
    }
  }
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

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

Стек статических вызовов:

...                                            (outer recursion)
ReactDOMComponent[6].mountComponent()    <-------------------------|
    (we are here)                                                  |
  |-this._createInitialChildren()                                  |
  ?{1}                                                             |
    |-DOMLazyTree.queueText()                                      |
  ?{2}                                                             |
    |-this.mountChildren()        // scr: ---------------> 1)(a)   |
      |-this._reconcilerInstantiateChildren()                      |
        |-ReactChildReconciler.instantiateChildren()               |
          |-traverseAllChildren()                                  |
            |-traverseAllChildrenImpl()  <------|inner             |
              |↻traverseAllChildrenImpl() ------|recursion         |
                |-instantiateChild()                               |
                  |-instantiateReactComponent()                    |
      |↻ReactDOMComponent.mountComponent()      // scr: -> 1)(b)---|
    |↻DOMLazyTree.queueChild()    // scr: ---------------> 2)
...

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

`DOMLazyTree.queueText ()` и `DOMLazyTree.queueChild ()`

DOMLazyTree.queueText() имеет только одну эффективную строку в этом пошаговом руководстве:

function queueText(tree, text) {
  if (enableLazy) { // scr: NO, I mean, false
...
  } else {
    setTextContent(tree.node, text);
  }
}
queueText@renderers/dom/client/utils/DOMLazyTree.js
var setTextContent = function (node, text) {
  if (text) {
    var firstChild = node.firstChild;
  if (firstChild && firstChild === node.lastChild && firstChild.nodeType === 3) { // scr: false
...
    }
  }
  node.textContent = text; // scr: the only effective line
};
setTextContent@renderers/dom/client/utils/setTextContent.js

Node.textContent - это стандартное свойство DOM, которое представляет, ну, текстовое содержимое узла. Судя по всему, это конечная цель маршрута {1}.

DOMLazyTree.queueChild() также имеет одну строку:

function queueChild(parentTree, childTree) {
  if (enableLazy) { // scr: again, false
...
  } else {
    parentTree.node.appendChild(childTree.node);
  }
}
queueChild@renderers/dom/client/utils/DOMLazyTree.js

Здесь Node.appendChild() - еще один стандартный API DOM, который вставляет узел в другой как дочерний. Судя по всему, это последняя остановка маршрута {2}.

Теперь мы можем заменить эти два метода их соответствующей строкой сущности.

...                                          (outer recursion)
ReactDOMComponent[6].mountComponent()    <-------------------------|
  |-this._createInitialChildren()                                  |
  ?{1}                                                             |
    |-node.textContent = text;                                     |
  ?{2}                                                             |
    |-this.mountChildren()        // scr: ---------------> 1)(a)   |
      |-this._reconcilerInstantiateChildren()                      |
        |-ReactChildReconciler.instantiateChildren()               |
          |-traverseAllChildren()                                  |
            |-traverseAllChildrenImpl()  <------|inner             |
              |↻traverseAllChildrenImpl() ------|recursion         |
                |-instantiateChild()                               |
                  |-instantiateReactComponent()                    |
      |↻ReactDOMComponent.mountComponent() // scr: ------> 1)(b)---|
    |↻node.appendChild()                   // scr: ------> 2)
...

Экстраполируйте общую картину

Для этого мы начнем с уже известных нам методов.

Во-первых, instantiateReactComponent(), который создает экземпляр ReactDOMComponent из ReactElement (у нас сейчас нет составного ReactElement в дереве, поэтому все создаваемые компоненты ReactDOMComponent), который также является концом глубоко вложенной иерархии вызовов {два поста}

Во-вторых, ReactDOMComponent.mountComponent(), который инициализирует ReactDOMComponent, созданные на предыдущем шаге, и создает на их основе соответствующие узлы DOM. {Публикация три} & {начало}

Учитывая две вышеуказанные операции и {OG} (группу операций), теперь легче объяснить, как обрабатывается остальная часть ReactElement дерева.

Вот объяснение высокого уровня:

  1. когда внешняя рекурсия ReactDOMComponent.mountComponent() вызывается для нелистового узла, ветвь {2} будет использоваться для запуска {OG} для каждого из дочерних элементов компонента;
  2. когда внешняя рекурсия ReactDOMComponent.mountComponent() вызывается для листового узла, содержащего текст, будет действовать ветвь {1}, которая устанавливает node.textContent;
  3. когда внешняя рекурсия ReactDOMComponent.mountComponent() вызывается для листового узла, не содержащего текста, _createInitialChildren() вообще не будет вызываться.

Обратите внимание, что в этом процессе ReactDOMComponent.mountComponent() многократно используется для создания узла DOM для соответствующего экземпляра ReactDOMComponent, поэтому вам может потребоваться проверить его реализацию в начале этого текста, если его нет в вашем (мозговом) кеше.

Пришло время нарисовать стек вызовов в действии:

...
ReactDOMComponent[6].mountComponent()
  |-this._createInitialChildren()
    |-this.mountChildren() 
...           |↻instantiateReactComponent()[4,5]
      |-ReactDOMComponent[5].mountComponent()
        |-this._createInitialChildren()
          |-node.textContent = text; // scr: [5] done
      |-ReactDOMComponent[4].mountComponent()
        |-this._createInitialChildren()
          |-this.mountChildren() 
...                 |↻instantiateReactComponent()[2,3]
          |-ReactDOMComponent[2].mountComponent() // scr: [2] done
          |-ReactDOMComponent[3].mountComponent()
            |-this._createInitialChildren()
              |-node.textContent = text; // scr: [3] done
        |↻node[4].appendChild()[2,3] // scr: [4] done
    |↻node[6].appendChild()[4,5] // scr: [6] done
...

В этом стеке вызовов я опускаю глубокую вложенную иерархию вызовов, которая используется для создания экземпляра ReactDOMComponent из ReactElement, которую мы собираемся исследовать дальше.

Глубокий вложенный цикл instantiateReactComponent ()

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

Начиная с ReactDOMComponent._createInitialChildren:

...
var mountImages = this.mountChildren(
  childrenToUse, // scr:----------> ReactElement[6].props.children
  transaction,   // scr: not of interest
  context        // scr: not of interest
);
...

Далее мы рассмотрим реализацию ReactDOMComponent.mountChildren(). Как упоминалось ранее, он 1) создает экземпляры всех дочерних элементов ReactDOMComponents; и 2) инициализирует эти ReactDOMComponent вызовом ReactDOMComponent.mountComponent().

mountChildren: function (
  nestedChildren, // scr:----------> ReactElement[6].props.children
  transaction,    // scr: not of interest
  context         // scr: not of interest
) {
  
  // scr: ------------------------------------------------------> 1)
  var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
  this._renderedChildren = children;
  var mountImages = [];
  var index = 0;
  for (var name in children) {
    if (children.hasOwnProperty(name)) {
      var child = children[name];
      var selfDebugID = 0;
...// scr: DEV code
                                             (outer recursion)
// scr: --------------------------------------------------> 2)
      var mountImage = ReactReconciler.mountComponent(child, transaction, this, this._hostContainerInfo, context, selfDebugID);
      
      child._mountIndex = index++;
      mountImages.push(mountImage);
    }
  }
...// scr: DEV code
  return mountImages;
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

2) раньше назывался внешней рекурсией и является еще одним ReactReconciler.mountComponent() {пост два}, поэтому мы сосредотачиваемся на 1)

_reconcilerInstantiateChildren: function (
  nestedChildren, // scr:----------> ReactElement[6].props.children
  transaction,    // scr: not of interest
  context         // scr: not of interest
) {
...// scr: DEV code
  return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
},
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js

что является прямым призывом

instantiateChildren: function (
  nestedChildNodes, // scr: --------> ReactElement[6].props.children
  transaction,      // scr: not of interest
  context,          // scr: not of interest
  selfDebugID
) // 0 in production and for roots {
  if (nestedChildNodes == null) {
    return null;
  }
  var childInstances = {};
  if (process.env.NODE_ENV !== 'production') {
...// scr: DEV code
  } else {
    traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
  }
  return childInstances;
},
instantiateChildren@renderers/shared/stack/reconciler/ReactChildReconciler.js

что, опять же, прямой вызов traverseAllChildren(). Обратите внимание, что instantiateChild - это метод обратного вызова, который вызывается для каждого дочернего элемента.

function instantiateChild(
  childInstances, // scr: ---> the output parameter childInstances is passed all the way down here
  child,      // scr: --> a ReactElement
  name,       // scr: --> unique name for indexing in childInstances
  selfDebugID // scr: --> undefined
) {
... // scr: DEV code
  }
  if (child != null && keyUnique) {
    childInstances[name] = instantiateReactComponent(child, true);
  }
}
instantiateChild@renderers/shared/stack/reconciler/ReactChildReconciler.js

Он, в свою очередь, напрямую вызывает instantiateReactComponent() {post one}.

Переходим к traverseAllChildren()

function traverseAllChildren(
  children, // scr: ---------> ReactElement[6].props.children
  callback, // scr: ---------> instantiateChild
  traverseContext // scr: ---> output parameter, initialized as {}
) {
  if (children == null) {
    return 0;
  }
  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
traverseAllChildren@shared/utils/traverseAllChildren.js

еще один прямой звонок traverseAllChildrenImpl

function traverseAllChildrenImpl(
  children,  // scr: ---------> ReactElement[6].props.children
  nameSoFar, // scr: ---------> ''
  callback,  // scr: ---------> instantiateChild
  traverseContext // scr: ---> output parameter, initialized as {}
) {
  var type = typeof children;
  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }
// scr: -------------------------------------------------------> {a}
  if (children === null || type === 'string' || type === 'number' || type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE) {
    callback(traverseContext, children,
    // If it's the only child, treat the name as if it was wrapped in an array
    // so that it's consistent if the number of children grows.
    nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
    return 1;
  }
  var child;
  var nextName;
  var subtreeCount = 0; // Count of children found in the current subtree.
  var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
// scr: -------------------------------------------------------> {b}
  if (Array.isArray(children)) {
    for (var i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
    }
  } else {
... // scr: this branch will not be called here
  }
  return subtreeCount;
}
traverseAllChildrenImpl@shared/utils/traverseAllChildren.js

После этих однострочных вызовов другого метода traverseAllChildrenImpl() - настоящий рабочий дом. раньше этот метод также назывался «внутренней рекурсией».

Логика traverseAllChildrenImpl() довольно проста: когда он вызывается в первый раз (а тип параметра children - array), он вызывает себя для каждого ReactElement в массиве; когда он вызывается последовательно (children равно ReactElement), он вызывает вышеупомянутый обратный вызов, который внутренне полагается на instantiateReactComponent() {отправить один} для преобразования ReactElement в пустой и неинициализированный ReactDOMComonent.

Обратите внимание, что «внутренняя рекурсия» работает только с ПРЯМЫМИ дочерними элементами, в то время как «внешняя рекурсия» проходит по ВСЕМУ ReactElement дереву.

После того, как все ReactElements преобразованы в ReactDOMComonents, результат полностью возвращается к ReactDOMComponent.mountChildren() и завершает круг.

Чтобы лучше понять полный круг, вам, возможно, придется ссылаться на разные части головоломки вперед и назад, например, начало этого текста, где обсуждается ReactDOMComponent.mountComponent(); две операции DOM (Node.appendChild, Node.textContent), которые определяют нижнюю часть стека; обсуждение общей картины; а также этот раздел.

Наконец, после того, как все узлы DOM сгенерированы, логика возвращается к ReactReconciler.mountComponent(), и новое дерево узлов вставляется в назначенный контейнер div. {Публикация три}

...
|~mountComponentIntoNode()                                    |
  |-ReactReconciler.mountComponent()                          |
    |-ReactCompositeComponent[T].mountComponent()             |
      |-ReactCompositeComponent[T].performInitialMount()  upper half
        |-ReactReconciler.mountComponent()                    |
          |-ReactCompositeComponent[ins].mountComponent()     |
            |-this.performInitialMount()                      |
              |-this._renderValidatedComponent()              |
              |-instantiateReactComponent()                  _|_ 
              |-ReactDOMComponent[6].mountComponent(          |
                  transaction, // scr: -----> not of interest |
                  hostParent,  // scr: -----> null            |
                  hostContainerInfo,// scr:---------------------> ReactDOMContainerInfo[ins]                                    | 
                  context      // scr: -----> not of interest |
                )                                             |
                                                              |
... // the content of this section                        lower half
        |-_mountImageIntoNode()                  (HTML DOM specific)
            markup,    // scr: --> DOMLazyTree[ins]           |
            container, // scr: --> document.getElementById(‘root’)
            wrapperInstance, // scr:----> same                |
            shouldReuseMarkup, // scr:--> same                |
            transaction, // scr: -------> same                |
          )                                                  _|_

Надеюсь, вам понравилось это чтение. Если да, хлопайте в ладоши или подписывайтесь на меня на Medium. Спасибо, и я надеюсь увидеть вас в следующий раз.

Изначально опубликовано на holmeshe.me.