DIFF 算法在执行时有三个维度,分别是 Tree DIFF、Component DIFF 和 Element DIFF,执行时按顺序依次执行,它们的差异仅仅因为 DIFF 粒度不同、执行先后顺序不同。
Tree DIFF 是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁。如图所示,左边是旧属,右边是新属,第一层是 R 组件,一模一样,不会发生变化;第二层进入 Component DIFF,同一类型组件继续比较下去,发现 A 组件没有,所以直接删掉 A、B、C 组件;继续第三层,重新创建 A、B、C 组件。
- 计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面。
- 通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n 是树的节点数,这个有多可怕呢?——如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异。
-
要知道渲染真实 DOM 的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实 dom 上会引起整个 dom 树的重绘和重排,有没有可能我们只更新我们修改的那一小块 dom 而不要更新整个 dom 呢?diff 算法能够帮助我们。
-
我们先根据真实 DOM 生成一颗 virtual DOM,当 virtual DOM 某个节点的数据改变后会生成一个新的 Vnode,然后 Vnode 和 oldVnode 作对比,发现有不一样的地方就直接修改在真实的 DOM 上,然后使 oldVnode 的值为 Vnode。
-
diff 的过程就是调用名为 patch 的函数,比较新旧节点,一边比较一边给真实的 DOM 打补丁。
- virtual DOM 是将真实的 DOM 的数据抽取出来,以对象的形式模拟树形结构。比如 dom 是这样的:
<div>
<p>123</p>
</div>
- 对应的 virtual DOM(伪代码):
var Vnode = {
tag: "div",
children: [{ tag: "p", text: "123" }]
};
- 在采取 diff 算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。
<div>
<p>123</p>
</div>
<div>
<span>456</span>
</div>
- 上面的代码会分别比较同一层的两个 div 以及第二层的 p 和 span,但是不会拿 div 和 span 作比较。在别处看到的一张很形象的图:
- 当数据发生改变时,set 方法会让调用 Dep.notify 通知所有订阅者 Watcher,订阅者就会调用 patch 给真实的 DOM 打补丁,更新相应的视图。
- 可以标识组件的唯一性,为了更好地区别各个组件 key 的作用主要是为了高效的更新虚拟 DOM。另外 vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
var MyComponent = React.createClass({
render: function() {
if (this.props.first) {
return (
<div className="first">
<span>A Span</span>
</div>
);
} else {
return (
<div className="second">
<p>A Paragraph</p>
</div>
);
}
}
});
- React 要从这个表现形式当中尝试找到前一个渲染结果到后一个的最小步数. 比如, 当我们挂载了
<MyComponent first="{true}" />
, 然后用
<MyComponent first="{false}" />
替换, 然后又取消挂载, 这样一个过程的 DOM 的指令是这样的:
-
从没有到第一步
-
创建节点:
<div className="first"><span>A Span</span></div>
-
-
第一步到第二步
-
替换属性: className="first" 到 className="second"
-
替换节点:
<span>A Span</span> 到 <p>A Paragraph</p>
-
-
第二步到没有
-
删除节点:
<div className="second"><p>A Paragraph</p></div>
-
- 策略一(tree diff): Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
- 策略二(component diff): 拥有相同类的两个组件 生成相似的树形结构, 拥有不同类的两个组件 生成不同的树形结构。
- 策略三(element diff): 对于同一层级的一组子节点,通过唯一 id 区分。
- 同一类型的两个组件,按原策略(层级比较)继续比较 Virtual DOM 树即可。
- 同一类型的两个组件,组件 A 变化为组件 B 时,可能 Virtual DOM 没有任何变化,如果知道这点(变换的过程中,Virtual DOM 没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate() 来判断是否需要 判断计算。
- 不同类型的组件,将一个(将被改变的)组件判断为 dirty component(脏组件),从而替换 整个组件的所有节点
- 注意:如果组件 D 和组件 G 的结构相似,但是 React 判断是 不同类型的组件,则不会比较其结构,而是删除 组件 D 及其子节点,创建组件 G 及其子节点。
- 插入:组件 C 不在集合(A,B)中,需要插入
- 删除:(1)组件 D 在集合(A,B,D)中,但 D 的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。 (2)组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。 移动:组件 D 已经在集合(A,B,C,D)里了,且集合更新时,D 没有发生更新,只是位置改变,如新集合(A,D,B,C),D 在第二个,无须像传统 diff,让旧集合的第二个 B 和新集合的第二个 D 比较,并且删除第二个位置的 B,再在第二个位置插入 D,而是 (对同一层级的同组子节点) 添加唯一 key 进行区分,移动即可。
- 答:diff 只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。
- 看着上图的 B,React 先从新中取得 B,然后判断旧中是否存在相同节点 B,当发现存在节点 B 后,就去判断是否移动 B。 B 在旧 中的 index=1,它的 lastIndex=0,不满足 index < lastIndex 的条件,因此 B 不做移动操作。此时,一个操作是,lastIndex=(index,lastIndex)中的较大数=1. 注意:lastIndex 有点像浮标,或者说是一个 map 的索引,一开始默认值是 0,它会与 map 中的元素进行比较,比较完后,会改变自己的值的(取 index 和 lastIndex 的较大数)。
- 看着 A,A 在旧的 index=0,此时的 lastIndex=1(因为先前与新的 B 比较过了),满足 index < lastIndex,因此,对 A 进行移动操作,此时 lastIndex=max(index,lastIndex)=1。
- 看着 D,同(1),不移动,由于 D 在旧的 index=3,比较时,lastIndex=1,所以改变 lastIndex=max(index,lastIndex)=3
- 看着 C,同(2),移动,C 在旧的 index=2,满足 index < lastIndex(lastIndex=3),所以移动。 由于 C 已经是最后一个节点,所以 diff 操作结束。
- B 不移动,不赘述,更新 l astIndex=1
- 新集合取得 E,发现旧不存在,故在 lastIndex=1 的位置 创建 E,更新 lastIndex=1
- 新集合取得 C,C 不移动,更新 lastIndex=2
- 新集合取得 A,A 移动,同上,更新 lastIndex=2
- 新集合对比后,再对旧集合遍历。判断 新集合 没有,但 旧集合 有的元素(如 D,新集合没有,旧集合有),发现 D,删除 D,diff 操作结束。
- 看图的 D,此时 D 不移动,但它的 index 是最大的,导致更新 lastIndex=3,从而使得其他元素 A,B,C 的 index < lastIndex,导致 A,B,C 都要去移动。
- 理想情况是只移动 D,不移动 A,B,C。因此,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响 React 的渲染性能。
- 尽量不要跨层级的修改 dom
- 设置 key 可以最大化的利用节点
- diff 的效率并不是每种情况下都是最优的