每次动画前初始化
可以看到,并没有什么用。依然会有一个减小的动画。如果将 setTimeout 置回到 0,只是看不到这个缩减到 0 过程,而是缩减到目标值的这一过程。并且对于第二个元素,因为前后 props 并没有发生变化,连缩小的过程也没有。
React 的 diff 机制对于树的差异检测,按照这个论文中描述的算法实现,其时间复杂度为 O(n3) 。而页面中 DOM 节点很容易上千,这样一次渲染需要 diff 的操作超过十亿,显然不可行。所以 React 在进行 diff 时作了两个假设前提:
如果父元素不同,其子节点产生不同的树。
开发者可通过为元素指定 key 来标识元素的唯一性,提高 React 差异检测时的效率。
基于这两点假设,在进行 diff 时可以少很多工作量,
diff 过程中,如果发现原来某个位置的元素其类型变化了,则无需继续遍历其子元素,直接认为该元素连同所有子节点都需要被替换掉。
diff 过程中,如果元素的 key 与上一次渲染时没发生变化,则判定为不需要重新渲染,进而也无需往下继续遍历其子元素。
这样假设之后,React 的 diff 算法做到了时间复杂度为 O(n)。
DOM 节点的 diff区分为节点类型变化与没变化两种情况,
对于前后再次渲染中,同一位置元素类型变化的情况,如前文所述,对该元素及其子节点整个更新。比如由 <section> 变成 <div>,该位置的 <section> 及其所有子节点将整个销毁,其中的状态也丢弃掉,创建 <div> 及相应子元素替换在该位置。
对于类型没变的情况则比较元素的属性,得出差异后只更新相应属性,比如 className。样式有更新也只计算出变化的样式属性然后只更新该属性。
组件节点的 diff对于自己写的组件,类型变化时同 DOM 节点一样,将整个组件实例销毁,其中各状态将丢失,所有子节点也都销毁,这些组件的 componentWillUnmount() 生命周期函数将被触发。然后实例化新类型的组件替换在该位置,新实例化的组件其 componentWillMount() 及 componentDidMount() 生命周期函数将顺次触发。
如果该位置组件类型没变,说明只需要根据变化的 props 更新组件即可,无需重新实例化新的组件。组件实例中的状态将在两次渲染中被保留复用,组件的 componentWillReceiveProps() 及 componentWillUpdate() 生命周期函数将触发。
子节点的遍历及 key 属性上面描述了节点对比后的处理。对于节点内子节点,递归遍历时,应用相同的逻辑。考察下面的示例代码:
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> + <li>third</li> </ul>
React 在遍历 <ul> 的子节点时,能够将前两个 <li> 元素匹配,保持不动,然后将新增的 <li>third</li> 附加在列表最后,完成更新。
如果新插入的元素不在列表最后,而是在最前面或中间,事情就开始发生变化。
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> + <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul>
这时 React 简单地按位置来对比更的模式就变得不那么智能了。由前文所述,
在进行第一个子元素 <li> 的对比时,发现其内容由 <li>Duke</li> 变为了 <li>Connecticut</li>,于是将该位置的元素更新。
继续对比,发现原来第二个位置的 <li>Villanova</li> 变为了 <li>Duke</li>,执行更新操作。
再继续发现需要新增 <li>Villanova</li> 元素。
这是 React 真实的流程,并不是我们一眼就能看出来的那个样子,只需要在列表开头插入那个新增的元素,将其他子元素保留即可。
所以,对于这样的列表类型,如果元素频繁变动,势必导致更新的效率会很低。问题的根本在于 React 不能识别前后两次渲染哪些元素其实是同一个,而是根据其在组件树中的位置来进行 diff 的。如果我们手动为元素指定一个唯一标识,这个标识在前后再次渲染时如果不变的话,这样就相当于告诉 React 它们是同一个元素,而不是按照其所在列表中的位置来进行 diff。