在 React 源码分析 - 组件更新与事务中的流程图的最后:
蓝色框框的部分分别是 Diff 算法的核心代码 updateChildren 以及 processUpdates, 通过 Diff 算法获取了组件更新的 updates 队列之后一次性进行更新
Diff 算法的代码(先别着急下面会具体解释算法的主要步骤):
- _updateChildren: function (nextNestedChildrenElements, transaction, context) {
- var prevChildren = this._renderedChildren;
- var removedNodes = {};
- var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, removedNodes, transaction, context);
- if (!nextChildren && !prevChildren) {
- return;
- }
- var updates = null;
- var name;
- var lastIndex = 0;
- var nextIndex = 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) {
- lastIndex = Math.max(prevChild._mountIndex, lastIndex);
- }
- updates = enqueue(updates, this._mountChildAtIndex(nextChild, lastPlacedNode, nextIndex, transaction, context));
- }
- nextIndex++;
- lastPlacedNode = ReactReconciler.getNativeNode(nextChild);
- }
- 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;
- }
深入 React 技术栈这本书对 Diff 算法的解释比较好其实只要记住几个原则以及在具体的计算 updates 队列的时候的算法优化的点就好了
传统的 diff 算法的复杂度是 O(n^3), 想要具体的了解可以去看 "A Survey on Tree Edit Distance and Related Problems"
这种复杂度在实际中应用会爆炸的, 虽然现在的电脑的 CPU 很强, 但一个页面也不能这样任性~
对此 React 的做法是给出合理的假设和方法来让整个 diff 过程合理简化
DOM 节点跨层级的移动操作的场景是很少见的, 可以忽略不计(合理, 可以通过组件的设计来尽量保证 DOM 结构的稳定, 必要时可以通过 CSS 的方法来进行 DOM 在展示上的调整, 因为创建删除以及移动 DOM 的操作是能少则少, 浏览器的每个 DOM 节点都是一个大对象, 有着很多的方法和属性)
同一类的两个组件将会生成相似的树形结构, 不同类的两个组件将会生成不同的树形结构 (合理, 本身组件就有提高页面的可复用性的作用, 也就是将结构功能类似的页面结构(或者说相似的 DOM 树形结构) 抽象成一类组件, 所以合理的组件抽象就应该满足这条假设)
对于同一层级的一组节点可以通过设置唯一的 key 来进行区分, 从而做到 diff 的进一步优化(这个不算是一个假设而是一个提高性能的方法)
对于同一类的两个组件, 有可能其 Virtual Dom 是没有任何变化的因此 React 允许开发者通过 shouldComponentUpdate()来判断组件是否需要进行 diff 算法分析 (合理, 开发者本身对页面的理解来进一步进行 diff 的优化, 当然这有可能会因为开发者错误的使用 shouldComponentUpdate() 判断错误了是否需要更新, 从而得到了错误的结果..... 但是这怪 sei ???, 写 bug 了还不老实)
基于上面的几条, 在具体的 Diff 过程中 React 只进行分层比较, 新旧的树之间只比较同一个层次的节点节点的操作分为 3 种: 插入移动和删除
节点移动操作判断的过程, 引用深入 React 技术栈中的话:
首先对新集合的节点进行循环遍历, for (name in nextChildren), 通过唯一 key 可以判断新老集合中是否存在相同的节点, if (prevChild === nextChild), 如果存在相同节点, 则进行移动操作, 但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较, if (child._mountIndex < lastIndex), 则进行节点移动操作, 否则不执行该操作这是一种顺序优化手段, lastIndex 一直在更新, 表示访问过的节点在老集合中最右的位置(即最大的位置), 如果新集合中当前访问的节点比 lastIndex 大, 说明当前访问节点在老集合中就比上一个节点位置靠后, 则该节点不会影响其他节点的位置, 因此不用添加到差异队列中, 即不执行移动操作, 只有当访问的节点比 lastIndex 小时, 才需要进行移动操作
需要注意的是这是一种顺序优化手段, lastIndex 一直在更新, 表示访问过的节点在老集合中最右的位置(即最大的位置), 如果新集合中当前访问的节点比 lastIndex 大, 说明当前访问节点在老集合中就比上一个节点位置靠后, 则该节点不会影响其他节点的位置, 因此不用添加到差异队列中, 即不执行移动操作这句话意思是如果一个节点在旧集合中的位置已经在你之前进行判断的最后一个节点的背后, 那么这个节点已经在被 diff 过的节点的后面了和之前的 diff 过的节点在顺序上就已经是正确的了, 不需要移动了, 反之的节点需要被移动
另外需要知道的是如果没有给 key 赋值, React 会默认使用的是遍历过程中的 index 值这里的 index 值指的是节点遍历的顺序号, 效果等同于有些小伙伴用列表数组的 index 来当做 key 这样其实是不好的, 因为节点的 key 和节点的位置有关系和节点本身没关系, 也就是如果我一个列表有 10 个节点, 按照遍历的顺序 key 为 1 到 10, 然后我在列表的最开始增加了一个节点, 这个时候按照列表遍历的顺序来设置 key, 则原来的 10 个节点的 key 都变了, 而且新旧节点的 key 错误的对上了, 要知道 key 在 React 中时对一个组件的身份识别的标示, 错误或者重复的 key 会造成 React 错误的结果......so.......key 需要是一个和节点本身有联系的唯一标示
react 的作者之一 Paul OShannessy 有提到:
Key is not really about performance, its more about identity (which in turn leads to better performance). Randomly assigned and changing values do not form an identity
对于新增和删除节点的操作简单来说:
新增节点就是创建新的节点放在顺序遍历到的位置上
删除节点则是在该层次遍历结束后, 对旧集合进行循环遍历, 判断是否在新集合中没有, 没有的话, 则删除节点
当然上面说的移动新增和删除节点的操作, 不是马上执行的, 而是收集到 updates 数组中, 然后用 processUpdates 方法一次性进行具体的 DOM 的的更新
- processUpdates: function (parentNode, updates) {
- for (var k = 0; k < updates.length; k++) {
- var update = updates[k];
- switch (update.type) {
- case ReactMultiChildUpdateTypes.INSERT_MARKUP:
- insertLazyTreeChildAt(parentNode, update.content, getNodeAfter(parentNode, update.afterNode));
- break;
- case ReactMultiChildUpdateTypes.MOVE_EXISTING:
- moveChild(parentNode, update.fromNode, getNodeAfter(parentNode, update.afterNode));
- break;
- case ReactMultiChildUpdateTypes.SET_MARKUP:
- setInnerhtml(parentNode, update.content);
- break;
- case ReactMultiChildUpdateTypes.TEXT_CONTENT:
- setTextContent(parentNode, update.content);
- break;
- case ReactMultiChildUpdateTypes.REMOVE_NODE:
- removeChild(parentNode, update.fromNode);
- break;
- }
- }
- }
其中的节点的具体的操作就是到具体的浏览器的 DOM 的节点的操作了, 举个栗子
- function insertLazyTreeChildAt(parentNode, childTree, referenceNode) {
- DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
- }
- var insertTreeBefore = createMicrosoftUnsafeLocalFunction(function (parentNode, tree, referenceNode) {
- if (tree.node.nodeType === 11) {
- insertTreeChildren(tree);
- parentNode.insertBefore(tree.node, referenceNode);
- } else {
- parentNode.insertBefore(tree.node, referenceNode);
- insertTreeChildren(tree);
- }
- });
Node.insertBefore()就是浏览器 DOM 操作的 API 了
想要跟着具体的 Diff 的过程来理解的话, 推荐单步调试或者看深入 React 技术栈中的栗子, 这里我就不画了..... 画图很累的..... 网上也非常多类似的搜一下就好了
参考资料:
React 源码剖析系列 - 不可思议的 react diff
深入 React 技术栈
来源: https://juejin.im/post/5aa163df518825557b4c4f0a