上次我们走完了从 setState()到单个 DOM 更新的流程, 并简单的分析了 diffing 算法这个分析显然不够, 因为 diffing 算法从一开始就是为应对更加复杂的情况而设计的
本篇我们会用两个例子进一步考察 diffing 算法更具体点, 我们来看这个算法如何处理 DOM 树的结构变化
注意, 本文用到的例子借鉴了官方文档这个文档也对 diffing 算法做了比较上层的描述所以如果您对本文的主题不太了解, 也可以先读一下
Photo by Luke Leung on Unsplash
例子一, diff 无 key 的节点
- 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;
babeled 后的版本:
- render() {
- return React.createElement(
- 'ul',
- null,
- this.state.data.map(function (val, i) {
- return React.createElement(
- 'li',
- null,
- ' ',
- val,
- ' '
- );
- })
- );
- }
新, 旧虚 DOM 树
我们已经知道了 render()函数会生成如下的虚 DOM 树{第四篇}(通过嵌套调用
- React.createElement()
- )
我们先忽略各 ReactElement 对应的 ReactDOMComponent
上面的图给出了在初始渲染过程中生成的旧虚 DOM 树和{上一篇}一样, setState()会在 5 秒钟后触发, 然后开始界面更新流程,
Figure-I
对这个数据结构有个印象, 我们跳过和{上一篇}重复的逻辑过程(大部分是 transaction 开始前的), 直接进入 diffing 算法,
- _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
步骤 1-5)和{上一篇}也一样, 这里就不赘述了
正如{第四篇}讨论过, 这个算法始于用
ReactCompositeComponent._renderValidatedComponent()
构件新的 DOM 树(Figure-I 右边的那个)
跟节点一样, 那就 diff 子节点吧
既然新旧 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()
调用只做了一件事, 遍历和更新它的直接子节点
我把{上一篇}的静态调用栈扩展一下, 以便引出下面的讨论:
- ... ___
- 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()
- Start recursing direct children
- _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
我把内容更新相关的代码折叠了, 以便集中注意在子 DOM 遍历上
1)如果条件满足(
lastChildren != null && nextChildren == null
)则删除所有子节点;
2)开始遍历
ReactMultiChild.updateChildren()
I 真正干活的
过了几个别名 (和其他一些各种划水的) 函数后, 这是第一个真正在做事的函数 I)它遍历虚 DOM 子节点, 比较新旧版本并且更新 ReactDOMComponent(我们简称这个为虚 DOM 操作);II) 将记录的操作固化到真实 DOM 中
这里
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()
- Virtual DOM operations
- _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 数组转成对象
- 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 和它的 对应 key(name)设置给目标对象中下面我们继续看
traverseAllChildren()
的函数体, 来了解 key 是怎么生成的
- ...
- 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
这个函数我们在{第五篇}中讨论过,(我把它拷过来)
当它第一次被调用时(这时参数 children 的类型是 array), 它会对这个数组中所有的 ReactElement 再递归调一次自己; 当它被后续调用时(参数 children 是 ReactElement), 它会调用前面提到的回调函数这个回调函数内部再......
把单个的 ReactElement 和它的 对应 key(name)设置给目标对象中(上面一段)
而 key 则是由 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
逻辑比较直观, 直接用数组的下标来作为 key(index.toString(36))也是因为我们现在讨论的是无 key 节点的情况
当前子调用栈,
- ...
- flattenChildren()
- |-traverseAllChildren()
- |-traverseAllChildrenImpl()
- |traverseAllChildrenImpl() // for direct each child
- |-flattenSingleChildIntoContext()
现在我们就有一个包含键值对的对象 nextChildren 可以被用于 diffprevChildren 了
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)
- },
所谓更新无非就是增, 删, 改
这个函数会遍历 nextChildren, 然后
1)如果 pre 和 next 的类型相同(由
shouldUpdateReactComponent()
判定), 遍历回
ReactReconciler.receiveComponent()
并更新子节点 {上一篇}的内容, 具体来说, 这个分支会更新
以及
2)如果 pre 和 next 的类型不同, 或者对应的 pre 根本就不存在, 则重新加载 (remount) 虚 DOM 节点;
在{第五篇}中, 这个虚 DOM 对应的 li 节点是在加载 (mounting) 过程中被创建的
3)如果 next 中不存在 pre, 则卸载(un-mount)pre 虚 DOM
更新节点内容的操作是封装在遍历
ReactReconciler.receiveComponent()
中的{上一篇}, 然而对实际 DOM 树的操作则需要等处理逻辑返回至
- ReactMultiChild.updateChildren()
- ReactMultiChild.updateChildren()
- IImatipulate real DOMs
- ...
- 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);
那含 key 节点有什么区别呢?
Diffing 含 key 节点
例子二,
- ...
- render() {
- return (
- <ul>
- {
- this.state.data.map(function(val, i) {
- return <li key={val}>{ val }</li>;
- })
- }
- </ul>
- );
- }
- ...
整个逻辑在
ReactDOMComponent.flattenChildren()
之前, 是和无 key 节点一模一样的而在这个将数组转化为包含键值对信息的对象的函数中, 节点所包含的 key 会被显示使用,
- 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()
), 新旧两颗树则通过 key 而对其了,
从而
ReactReconciler.receiveComponent()
的递归调用中不会对 (key:one 和 two) 生成任何实际的 DOM 操作, 因为它们的内容是相同的所以这次唯一需要的 DOM 操作是
parentNode.insertBefore(tree.node, referenceNode);
用来将 key 为 new 的节点添加到 DOM 树中
比之前省了两次
打包带走
- 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 树的结构, 为啥这里不需要给节点添加 key 呢? 答案在代码里
结语(对, 暂时就写到这里了)
有目的的读代码就像查找算法, 如果数组排过序了, 理论上会比无序数组快 O(n) - O(log n)这个连载旨在将 React 代码理顺, 以便下次你读它时能享受到 O(log n)的速度
好了, 这次先写到这如果您觉得这篇不错, 可以点赞或关注这个专栏
来源: https://juejin.im/entry/5a991820518825558722fa62