【译】了解React源代码-UI更新(DOM树)9
【译】了解React源代码-初始渲染(简单组件)1
【译】了解React源代码-初始渲染(简单组件)2
【译】了解React源代码-初始渲染(简单组件)3
【译】了解React源代码-初始渲染(类组件)4
【译】了解React源代码-初始渲染(类组件)5
【译】了解React源代码-UI更新(事务)6
【译】了解React源代码-UI更新(事务)7
【译】了解React源代码-UI更新(单个DOM)8
【译】了解React源代码-UI更新(DOM树)9
上一次,我们经历了从setState()
到更新单个DOM的过程。 我们还分析了差异算法,因为该算法是为比更新单个DOM节点复杂得多的任务而设计的,因此远远不够完善。
这次我们将使用两个示例更深入地研究差异算法。 更具体地说,我们看一下该算法如何处理变异的DOM树。
注意,本文中使用的示例均来自官方文档,该文档还提供了差异算法的高级描述。 如果您对主题不太熟悉,则可能需要先阅读。
示例1.无密钥节点的差异
class App extends Component {
constructor(props) {
super(props);
this.state = {
data : ['one', 'two'],
};
this.timer = setInterval(
() => this.tick(),
5000
);
}
tick() {
this.setState({
data: ['new', 'one', 'two'],
});
}
render() {
return (
<ul>
{
this.state.data.map(function(val, i) {
return <li>{ val }</li>;
})
}
</ul>
);
}
}
export default App;
转义版本的render()
,
render() {
return React.createElement(
'ul',
null,
this.state.data.map(function (val, i) {
return React.createElement(
'li',
null,
' ',
val,
' '
);
})
);
}
新旧虚拟DOM树
我们知道虚拟DOM树render()
方法的结果是{第四篇}(对React.createElement()
的嵌套调用)
We ignore the
ReactElement
’s corresponding controllers (i.e.,ReactDOMComponent
) for simplicity.
为了简单起见,我们忽略了ReactElement
的相应控制器(即ReactDOMComponent
)。
上图给出了由初始渲染生成的旧虚拟DOM树。 与{上一篇}中一样,setState()
会在5秒钟后触发,从而启动更新过程,
考虑到这种数据结构,我们跳过与{上一篇}相同的逻辑过程(大部分在transaction之前),然后直接转到差异算法,
_updateRenderedComponent: function (transaction, context) {
var prevComponentInstance = this._renderedComponent; // scr: -> 1)
// scr: ------------------------------------------------------> 2)
var prevRenderedElement = prevComponentInstance._currentElement;
// scr: create a new DOM tree
var nextRenderedElement = this._renderValidatedComponent();
var debugID = 0;
// scr: DEV code
...
if (shouldUpdateReactComponent( // scr: ----------------------> 3)
prevRenderedElement,
nextRenderedElement)
) {
ReactReconciler.receiveComponent( // scr: ------------------> 5)
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
);
} else { // scr: ---------------------------------------------> 4)
// scr: code that is not applicable this time
...
}
},
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js
The steps 1–5) are also identical to {last post}.
步骤1-5)也与{上一篇}相同。
,该算法首先创建新的DOM树({Figure-I}中右边的那个)和ReactCompositeComponent._renderValidatedComponent()
。 {第四篇}
根节点是相同的,因此可以“区分”其直接子节点
由于ReactElement [1]
的类型相同(“ ul”
),因此逻辑转到{上一篇}中的5)。
receiveComponent: function (nextElement, transaction, context) {
var prevElement = this._currentElement;
this._currentElement = nextElement;
this.updateComponent(transaction,
prevElement,
nextElement,
context);
},
updateComponent: function(
transaction,
prevElement,
nextElement,
context
) {
var lastProps = prevElement.props;
var nextProps = this._currentElement.props;
// scr: code that is not applicable this time
...
// scr: ------------------------------------------------------> 1)
this._updateDOMProperties(lastProps, nextProps, transaction);
// scr: ------------------------------------------------------> 2)
this._updateDOMChildren(lastProps, nextProps, transaction, context);
// scr: code that is not applicable this time
...
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
在{上一篇}步骤1)中更新DOM节点属性; 和2)更新其内容。
但是对于根节点(ReactElement [1]
),整个ReactDOMComponent.updateComponent()
方法调用的唯一目的是递归和更新ReactElement [1]
的直接子对象,因为节点的属性及其内容均未更改。
我还将{上一篇}的静态调用堆栈作为线索进行扩展:
... ___
ReactReconciler.receiveComponent() <----------------| |
|-ReactDOMComponent.receiveComponent() | |
|-this.updateComponent() | |
|-this._updateDOMProperties() | |
|-CSSPropertyOperations.setValueForStyles() | |
|-this._updateDOMChildren() | |
|-this.updateTextContent() | diffing
|-this._updateDOMChildren() (the focus this time)| |
|-this.updateChildren() | |
|=this._updateChildren() | |
|-this._reconcilerUpdateChildren() | |
|-this.flattenChildren() | |
|-ReactChildReconciler.updateChildren() ---| |
---
如前所述,递归从ReactDOMComponent._updateDOMChildren()
开始。 在以下各节中,我们将遵循层次结构,一次执行一个函数,然后进入堆栈的底部。
ReactDOMComponent._updateDOMChildren()-开始递归直接子级
_updateDOMChildren: function (
lastProps, nextProps, transaction, context
) {
// scr: code for content updating
...
var nextChildren = nextContent != null ? null : nextProps.children;
if (lastChildren != null && nextChildren == null) { // scr: --> 1)
this.updateChildren(null, transaction, context);
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
// scr: code for content updating
...
}
if (nextContent != null) {
if (lastContent !== nextContent) {
// scr: code for content updating
...
} else if (nextHtml != null) {
// scr: code for content updating
...
} else if (nextChildren != null) {
// scr: DEV code
...
// scr: --------------------------------------------------> 2)
this.updateChildren(nextChildren, transaction, context);
}
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
I fold up the content updating related code so we can focus on DOM children recursing
我将内容更新相关的代码折叠起来,以便我们专注于DOM子代递归
1)仅在必要时删除子级(lastChildren!= null && nextChildren == null
);
2)开始递归。
ReactMultiChild.updateChildren()I —实际工作的马
在使用别名方法或具有很少(预处理)操作的方法之后,我们来研究以下工作:I)递归虚拟DOM子代,比较它们的新/旧版本,并相应地修改ReactDOMComponent
的名称( 为简单起见我们将其命名为虚拟DOM操作); 和II)将操作提交给实际的DOM。
the role of this
ReactMultiChild.updateChildren()
is similar to that ofmountComponentIntoNode()
in initial rendering {post two}
此ReactMultiChild.updateChildren()
的角色在初始呈现中类似于mountComponentIntoNode()
的角色{第二篇}
updateChildren: function (
nextNestedChildrenElements,
transaction,
context
) {
// Hook used by React ART
this._updateChildren(nextNestedChildrenElements, transaction, context);
},
_updateChildren: function (
nextNestedChildrenElements,
transaction,
context
) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var mountImages = [];
var nextChildren = this._reconcilerUpdateChildren( // scr: ---> I)
prevChildren, // scr: ------------------> i)
nextNestedChildrenElements, // scr: ----> ii)
mountImages,
removedNodes,
transaction,
context
);
if (!nextChildren && !prevChildren) {
return;
}
// scr: -----------------------------------------------------> II)
var updates = null;
var name;
// `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
var nextIndex = 0;
var lastIndex = 0;
// `nextMountIndex` will increment for each newly mounted child.
var nextMountIndex = 0;
var lastPlacedNode = null;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// The `removedNodes` loop below will actually remove the child.
}
// The child must be instantiated before it's mounted.
updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context));
nextMountIndex++;
}
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
}
// Remove children that are no longer present.
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
}
}
if (updates) {
processQueue(this, updates);
}
this._renderedChildren = nextChildren;
// scr: DEV code
...
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
我们首先看一下虚拟DOM操作,I)。 请注意,负责方法ReactDOMComponent._reconcilerUpdateChildren()
的两个输入参数是:i)prevChildren
,即ReactDOMComponent._renderedChildren
,在初始渲染中将其设置为其子ReactDOMComponents
的对象{第五篇}; ii)nextNestedChildrenElements
,即从ReactDOMComponent._updateDOMChildren()
传递的nextProps.children
。
ReactDOMComponent._reconcilerUpdateChildren()—虚拟DOM操作
_reconcilerUpdateChildren: function (
prevChildren,
nextNestedChildrenElements,
mountImages,
removedNodes,
transaction,
context
) {
var nextChildren;
var selfDebugID = 0;
// scr: DEV code
...
nextChildren = flattenChildren( // scr: -----------------> 1)
nextNestedChildrenElements,
selfDebugID);
ReactChildReconciler.updateChildren( // scr: -----------------> 2)
prevChildren,
nextChildren,
mountImages,
removedNodes,
transaction,
this,
this._hostContainerInfo,
context, selfDebugID);
return nextChildren;
},
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
在2)可以遍历和比较虚拟DOM之前,此方法1)调用
flattenChildren()—将ReactElement数组转换为对象(map)
function flattenChildren(children, selfDebugID) {
if (children == null) {
return children;
}
var result = {};
// scr: DEV code
...
{
traverseAllChildren(children, flattenSingleChildIntoContext, result);
}
return result;
}
flattenChildren@shared/utils/flattenChildren.js
在这里,我们需要注意传递给traverseAllChildren()
的回调
function flattenSingleChildIntoContext(
traverseContext,
child,
name,
selfDebugID
) {
// We found a component instance.
if (traverseContext && typeof traverseContext === 'object') {
var result = traverseContext;
var keyUnique = result[name] === undefined;
// scr: DEV code
...
if (keyUnique && child != null) {
result[name] = child;
}
}
}
flattenSingleChildIntoContext@shared/utils/flattenChildren.js
,该回调将单个ReactElement
及其关联的键(name
)设置在对象(map)中。 接下来,我们查看traverseAllChildren()
方法主体,以特别了解如何生成密钥。
...
var SEPARATOR = '.';
...
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
traverseAllChildren@shared/utils/traverseAllChildren.js
function traverseAllChildrenImpl(
children,
nameSoFar, // scr: -------- ''
callback,
traverseContext
) {
var type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}
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;
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: code that is not applicable
...
}
return subtreeCount;
}
traverseAllChildrenImpl@shared/utils/traverseAllChildren.js
如前所述,我们在{第五篇}中描述了该方法,
when it is called the first time (and the type of
children
parameter isarray
), it calls itself for everyReactElement
within the array; when it is called successively (children
isReactElement
), invokes the aforementioned callback that…
第一次调用时(children
参数的类型是array
),它将为数组中的每个ReactElement
调用自身; 当它被连续调用时(children
为ReactElement
),将调用上述回调……
“将单个ReactElement
及其相关的键(name
)设置在对象中”。
密钥是使用getComponentKey()
生成的,
function getComponentKey(component, index) {
if (component && typeof component === 'object' && component.key != null) {
// Explicit key
return KeyEscapeUtils.escape(component.key);
}
// Implicit key determined by the index in the set
return index.toString(36);
}
getComponentKey@shared/utils/traverseAllChildren.js
如果未在“无密钥节点”中显式设置密钥,则该方法基本上使用数组的索引作为对象中的密钥(index.toString(36)
)。
flattenChildren()
的静态(子)调用栈,
...
flattenChildren()
|-traverseAllChildren()
|-traverseAllChildrenImpl()
|↻traverseAllChildrenImpl() // for direct each child
|-flattenSingleChildIntoContext()
现在我们有一个键值对象nextChildren
与prevChildren
进行了“区分”。
ReactChildReconciler.updateChildren()—操作虚拟DOM树
updateChildren: function(
prevChildren,
nextChildren,
mountImages,
removedNodes,
transaction,
hostParent,
hostContainerInfo,
context,
selfDebugID, // 0 in production and for roots
) {
if (!nextChildren && !prevChildren) {
return;
}
var name;
var prevChild;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
prevChild = prevChildren && prevChildren[name];
var prevElement = prevChild && prevChild._currentElement;
var nextElement = nextChildren[name];
if ( // scr: -----------------------------------------------> 1)
prevChild != null &&
shouldUpdateReactComponent(prevElement, nextElement)
) {
ReactReconciler.receiveComponent(
prevChild,
nextElement,
transaction,
context,
);
nextChildren[name] = prevChild; // scr: --------------> end 1)
} else {
if (prevChild) { // scr: ---------------------------------> 2)
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
ReactReconciler.unmountComponent(prevChild, false);
}
// The child must be instantiated before it's mounted.
var nextChildInstance = instantiateReactComponent(nextElement, true);
nextChildren[name] = nextChildInstance;
// Creating mount image now ensures refs are resolved in right order
// (see https://github.com/facebook/react/pull/7101 for explanation).
var nextChildMountImage = ReactReconciler.mountComponent(
nextChildInstance,
transaction,
hostParent,
hostContainerInfo,
context,
selfDebugID,
);
mountImages.push(nextChildMountImage);
} // scr: ----------------------------------------------> end 2)
}
// scr: ------------------------------------------------------> 3)
// Unmount children that are no longer present.
for (name in prevChildren) {
if (
prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren.hasOwnProperty(name))
) {
prevChild = prevChildren[name];
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
ReactReconciler.unmountComponent(prevChild, false);
}
} // scr: ------------------------------------------------> end 3)
},
updating is nothing more than modifying, adding, and deleting
更新无非就是修改,添加和删除
此方法遍历nextChildren
,并且
1)如果相应的“ pre”和“ next”节点的类型相同(由shouldUpdateReactComponent()
判断),则递归回到ReactReconciler.receiveComponent()
以像{上一篇}中那样修改关联的DOM节点的内容。 {上一篇}),其逻辑分支适用于
和
因为比较是基于对方的索引(也是key);
2)如果“ pre”和“ next”节点的类型不同,或者相应的“ pre”节点根本不存在,则重新安装虚拟DOM;
As in {post five}, the virtual DOM’s corresponding
li
node has been created in the mounting process;
与{第五篇}中一样,虚拟DOM的相应li
节点已在安装过程中创建;
3)如果“下一个”虚拟DOM不存在,则卸载它们。
内容更新操作封装在ReactReconciler.receiveComponent()
{上一篇}的递归中,而真正的DOM树上的操作是在逻辑处理返回到ReactMultiChild.updateChildren()
中时进行的。
ReactMultiChild.updateChildren()II —构造真实的DOM
...
var updates = null;
var name;
// `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
var nextIndex = 0;
var lastIndex = 0;
// `nextMountIndex` will increment for each newly mounted child.
var nextMountIndex = 0;
var lastPlacedNode = null;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
// scr: --------------------------------------------------> III)
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
updates = enqueue(
updates,
this.moveChild(
prevChild,
lastPlacedNode,
nextIndex,
lastIndex
)
);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex; // scr: ---------> end III)
} else { // scr: ------------------------------------------> IV)
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// The `removedNodes` loop below will actually remove the child.
}
// The child must be instantiated before it's mounted.
updates = enqueue(
updates,
this._mountChildAtIndex(
nextChild,
mountImages[nextMountIndex],
lastPlacedNode,
nextIndex,
transaction,
context
)
);
nextMountIndex++;
} // scr: ---------------------------------------------> end IV)
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
}
// Remove children that are no longer present.
for (name in removedNodes) { // scr: -------------------------> V)
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(
updates,
this._unmountChild(
prevChildren[name],
removedNodes[name]
)
);
}
} // scr: ------------------------------------------------> end V)
if (updates) {
processQueue(this, updates); // scr: ----------------------> VI)
}
this._renderedChildren = nextChildren;
// scr: DEV code
...
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
此逻辑块迭代nextChildren
,并在必要时对其进行循环。
III)标记节点的位置已更改;
IV)标记一个新添加的节点;
V)标记一个已删除的节点;
VI)将更改提交到DOM树{上一篇}
此处适用的分支是IV),将ReactElement [4]
关联的节点添加到DOM树。
_mountChildAtIndex: function (
child,
mountImage,
afterNode,
index,
transaction,
context
) {
child._mountIndex = index;
return this.createChild(child, afterNode, mountImage);
},
createChild: function (child, afterNode, mountImage) {
return makeInsertMarkup(mountImage, afterNode, child._mountIndex);
},
function makeInsertMarkup(markup, afterNode, toIndex) {
// NOTE: Null values reduce hidden classes.
return {
type: 'INSERT_MARKUP',
content: markup,
fromIndex: null,
fromNode: null,
toIndex: toIndex,
afterNode: afterNode
};
}
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
在VI中)
processUpdates: function(parentNode, updates) {
// scr: DEV code
...
for (var k = 0; k < updates.length; k++) {
var update = updates[k];
switch (update.type) {
case 'INSERT_MARKUP':
insertLazyTreeChildAt(
parentNode,
update.content,
getNodeAfter(parentNode, update.afterNode),
);
break;
// scr: code that is not applicable
...
function insertLazyTreeChildAt(
parentNode,
childTree,
referenceNode
) {
DOMLazyTree.insertTreeBefore(
parentNode,
childTree,
referenceNode
);
}
DOMChildrenOperations@renderers/dom/client/utils/DOMChildrenOperations.js
因此,此堆栈中的最后一张卡是DOMLazyTree.insertTreeBefore()
。 从{第三篇}我们已经知道,此方法调用HTML DOM API
parentNode.insertBefore(tree.node, referenceNode);
那什么时候发生
用键区分节点
示例2
...
render() {
return (
<ul>
{
this.state.data.map(function(val, i) {
return <li key={val}>{ val }</li>;
})
}
</ul>
);
}
...
过程逻辑与ReactDOMComponent.flattenChildren()
之前的无键节点中的逻辑相同,在无键节点中,将使用指定的键而不是数组索引来建立键值对象,
function getComponentKey(component, index) {
if (component && typeof component === 'object' &&
component.key != null) {
// Explicit key
return KeyEscapeUtils.escape(component.key);
}
// code that is not applicable
...
}
getComponentKey@shared/utils/traverseAllChildren.js
因此,在ReactChildReconciler.updateChildren()
中,可以更好地对齐两个虚拟DOM树的比较,
并且通过比较具有相同内容且仅具有相同内容的节点(键:一和二),递归ReactReconciler.receiveComponent()不会引发任何DOM操作。 必要的DOM操作,即
parentNode.insertBefore(tree.node, referenceNode);
对ReactMultiChild.updateChildren()
中的节点(key:new
)执行。
其结果是,keys可以抽出一些不必要的DOM操作用于突变DOM树。
Take home
class App extends Component {
constructor(props) {
super(props);
this.state = {
mutate: false,
};
this.timer = setInterval(
() => this.tick(),
5000
);
}
tick() {
this.setState({
mutate: true,
});
}
render() {
return (
<ul>
{ this.state.mutate &&
<li>New</li>
}
<li>One</li>
<li>Two</li>
</ul>
);
}
}
export default App;
上面的代码还更改了DOM树结构。 您能回答为什么这里不需要按键吗?
—尾注—
有目的地读取源代码就像搜索数组一样,从理论上讲,对数组进行排序后,它的读取速度为O(n)-O(log n)
。 本系列旨在为您整理React代码库,因此只要有目的,您就可以享受O(log n)
。
(原文链接)Understanding The React Source Code - UI Updating (DOM Tree) IX