Понимание исходного кода 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: метод, который выполняет итерацию прямых вложенных ReactElement
s и создает их соответствующие ReactDOMComponent
s
Обозначения, используемые в стеке вызовов: ↻
цикл ?
условие
Я использую {} для ссылки на предыдущий пост, имеющий отношение к обсуждаемым методам (или логическому процессу).
Процесс, обсуждаемый в этом посте, в основном происходит в 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) преобразует ReactElement
s в ReactDOMComponent
s (a), генерирует узлы DOM с ReactDOMComponent
s (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
дерева.
Вот объяснение высокого уровня:
- когда внешняя рекурсия
ReactDOMComponent.mountComponent()
вызывается для нелистового узла, ветвь {2} будет использоваться для запуска {OG} для каждого из дочерних элементов компонента; - когда внешняя рекурсия
ReactDOMComponent.mountComponent()
вызывается для листового узла, содержащего текст, будет действовать ветвь {1}, которая устанавливаетnode.textContent
; - когда внешняя рекурсия
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) создает экземпляры всех дочерних элементов ReactDOMComponent
s; и 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
дереву.
После того, как все ReactElement
s преобразованы в ReactDOMComonent
s, результат полностью возвращается к 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.