乐趣区

谈谈React中Diff算法的策略及实现

1、什么是 Diff 算法

传统 Diff:diff 算法即差异查找算法;对于 Html DOM 结构即为 tree 的差异查找算法;而对于计算两颗树的差异时间复杂度为 O(n^3), 显然成本太高,React 不可能采用这种传统算法;

React Diff:

之前说过,React 采用虚拟 DOM 技术实现对真实 DOM 的映射,即 React Diff 算法的差异查找实质是对两个 JavaScript 对象的差异查找;
基于三个策略:

Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。(tree diff)
拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结(component diff)
对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。(element diff)

2、React Diff 算法解读

首先需要明确,只有在 React 更新阶段才会有 Diff 算法的运用;
React 更新机制:

React Diff 算法优化策略图:

React 更新阶段会对 ReactElement 类型判断而进行不同的操作;ReactElement 类型包含三种即:文本、Dom、组件;

每个类型的元素更新处理方式:

自定义元素的更新,主要是更新 render 出的节点,做甩手掌柜交给 render 出的节点的对应 component 去管理更新。
text 节点的更新很简单,直接更新文案。

浏览器基本元素的更新,分为两块:

更新属性,对比出前后属性的不同,局部更新。并且处理特殊属性,比如事件绑定。
子节点的更新,子节点更新主要是找出差异对象,找差异对象的时候也会使用上面的 shouldUpdateReactComponent 来判断,如果是可以直接更新的就会递归调用子节点的更新, 这样也会递归查找差异对象。不可直接更新的删除之前的对象或添加新的对象。之后根据差异对象操作 dom 元素(位置变动,删除,添加等)。

事实上 Diff 算法只被调用于 React 更新阶段的 DOM 元素更新过程;为什么这么说?

1、如果为更新文本类型,内容不同就直接更新替换,并不会调用复杂的 Diff 算法:
ReactDOMTextComponent.prototype.receiveComponent(nextText, transaction) {
// 与之前保存的字符串比较
if (nextText !== this._currentElement) {
this._currentElement = nextText;
var nextStringText = ” + nextText;
if (nextStringText !== this._stringText) {
this._stringText = nextStringText;
var commentNodes = this.getHostNode();
// 替换文本元素
DOMChildrenOperations.replaceDelimitedText(
commentNodes[0],
commentNodes[1],
nextStringText
);
}
}
}
2、对于自定义组件元素:
class Tab extends Component {
constructor(props) {
super(props);
this.state = {
index: 1,
}
}
shouldComponentUpdate() {
….
}
render() {
return (
<div>
<p>item1</p>
<p>item1</p>
</div>
)
}

}

需要明确的是,何为组件,可以说组件只不过是一段 Html 结构的包装容器,并且具备管理这段 Html 结构的状态等能力;
如上述 Tab 组件:它的实质内容就是 render 函数返回的 Html 结构,而我们所说的 Tab 类就是这段 Html 结构的包装容器(可以理解为一个包装盒子);
在 React 渲染机制图中可以看到,自定义组件的最后结合 React Diff 优化策略一(不同类的两个组件具备不同的结构)

3、基本元素:
ReactDOMComponent.prototype.receiveComponent = function(nextElement, transaction, context) {
var prevElement = this._currentElement;
this._currentElement = nextElement;
this.updateComponent(transaction, prevElement, nextElement, context);
}

ReactDOMComponent.prototype.updateComponent = function(transaction, prevElement, nextElement, context) {
// 需要单独的更新属性
this._updateDOMProperties(lastProps, nextProps, transaction, isCustomComponentTag);
// 再更新子节点
this._updateDOMChildren(
lastProps,
nextProps,
transaction,
context
);

// ……
}
在 this._updateDOMChildren 方法内部才调用了 diff 算法。

3、React 中 Diff 算法的实现
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var mountImages = [];

// 获取新的子元素数组
var nextChildren = this._reconcilerUpdateChildren(
prevChildren,
nextNestedChildrenElements,
mountImages,
removedNodes,
transaction,
context
);

if (!nextChildren && !prevChildren) {
return;
}

var updates = null;
var name;
var nextIndex = 0;
var lastIndex = 0;
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) {
// 同一个引用,说明是使用的同一个 component, 所以我们需要做移动的操作
// 移动已有的子节点
// NOTICE:这里根据 nextIndex, lastIndex 决定是否移动
updates = enqueue(
updates,
this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
);

// 更新 lastIndex
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 更新 component 的.mountIndex 属性
prevChild._mountIndex = nextIndex;

} else {
if (prevChild) {
// 更新 lastIndex
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
}

// 添加新的子节点在指定的位置上
updates = enqueue(
updates,
this._mountChildAtIndex(
nextChild,
mountImages[nextMountIndex],
lastPlacedNode,
nextIndex,
transaction,
context
)
);

nextMountIndex++;
}

// 更新 nextIndex
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
}

// 移除掉不存在的旧子节点,和旧子节点和新子节点不同的旧子节点
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(
updates,
this._unmountChild(prevChildren[name], removedNodes[name])
);
}
}
}

5、基于中 Diff 的开发建议

基于 tree diff:

开发组件时,注意保持 DOM 结构的稳定;即,尽可能少地动态操作 DOM 结构,尤其是移动操作。
当节点数过大或者页面更新次数过多时,页面卡顿的现象会比较明显。
这时可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

基于 component diff:

注意使用 shouldComponentUpdate() 来减少组件不必要的更新。
对于类似的结构应该尽量封装成组件,既减少代码量,又能减少 component diff 的性能消耗。

基于 element diff:
对于列表结构,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

接下来手动实现一个简单的 Diff 算法即将更新,敬请期待~~~

“积跬步、行千里”—— 持续更新中~,喜欢留下个赞哦!

往期经典好文:

团队合作必备的 Git 操作
谈谈 Js 前端规范化
从 React 渲染流程分析 Diff 算法

相关专栏推荐:
React 学习之路

退出移动版