1. 前言
diff并不是React独创,只是React针对diff算法进行了优化。在React中diff算法和Virtual Dom的完美结合可让咱们不须要关心页面的变化致使的性能问题。由于diff会帮助咱们计算出Virtual Dom变化的部分,而后只对该部分进行原生DOM操做,而非从新渲染整个页面,从而保证每次操做更新后页面的高效渲染。node
2. 详解diff
React将Virtual Dom转为真实DOM树的最少操做的过程叫作调和(reconciliation)。diff即是调和的具体实现。算法
3. diff策略
- 策略一: Web UI中DOM节点跨层级的移动操做特别少,能够忽略不计。
- 策略二: 拥有相同类的两个组件将会生成类似的树形结构,拥有不一样类的两个组件将会生成不一样的树形结构。
- 策略三: 对于同一层级的一组节点,它们能够经过惟一id进行区分。 基于以上策略,React分别对tree diff、component diff、element diff进行算法优化。
4. tree diff 对于树的比较
基于策略一( Web UI中DOM节点跨层级的移动操做特别少,能够忽略不计)。React对树的算法进行简洁明了的优化,既对树进行分层比较,两棵树只会对同一层次的节点进行比较。bash
既然DOM节点跨层级的移动操做能够忽略不计,针对这一现象,React经过updateDepth对Virtual DOM树进行层级控制,只会对相同层级的DOM进行比较,即同一个父节点下的全部子节点,当发现节点已经不存在时,则删除该节点以及其子节点,不会用于进一步比较,这样只需对树进行一次遍历,便可以完成整个DOM树比较。源码分析
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth ++
var errorThrown = true
try {
this._updateChildren(nextNestedChildrenElements, transaction, context)
} finally {
updateDepth --
if(!updateDepth) {
if(errorThrown) {
clearQueue()
}else {
processQueue()
}
}
}
}
复制代码
以下出现DOM节点跨层级移动操做,A节点整个被移动到了D节点下,因为React只会简单的考虑同层级节点的位置变化,对于不一样层级的节点只有建立和删除。当根节点发现子节点中A消失,就会直接销毁A;当D发现多了一个子节点A,则会建立新的A和其子节点,此时diff执行状况:createA--->createB--->crateC--->createA:性能
能够发现跨层级移动时,会出现从新建立整个树的操做,这种比较影响性能,因此不推荐进行DOM节点跨节点操做。
5. component diff 对于组件的比较
React是基于组件构建应用的,对于组件间的比较所采起的策略也是很是简洁的、高效的。优化
- 若是是同一类型的组件,按照原策略继续比较Virtual DOM树便可。
- 若是不是同一类型组件,则将该组件判断为dirtycomponent,从而替换整个组件下的全部子组件。
- 对于同一类型的组件,有可能其Virtual DOM没有任何变化,若是可以确切知道这点,那么就能够节省大量的diff运算时间。所以React 容许用户经过shouldComponentUpdate来判断该组件是否须要进行diff算法分析。
6. element diff
当节点处于同一层级时,diff提供了3种节点操做,分别为INSERT_MARKUP(插入)、MOVE_EXISTING(移动)、REMOVE_NODE(删除):ui
- INSERT_MARKUP:新的组件类型不在旧集合里,即全新的节点,须要对新节点执行插入操做。
- MOVE_EXISTING:旧集合中有新组件类型,且element是可更新的类型,generateComponentChildren已调用receiveComponent,这种状况下prevChild=nextChild,就须要作移动操做,能够复用之前的DOM节点。
- REMOVE_NODE:旧组件类型,在新集合里也有,但对应的element不一样则不能直接复用和更新,须要执行删除操做,或者旧组件不在新集合里,也须要执行删除操做。 相关代码以下:
function makeInsertMarkup(markup, afterNode) {
return {
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
content: markup,
fromIndex: null,
fromNode: null,
toIndexL toIndex,
afterNode: afterNode
}
}
function makeMove(child, afterNode, toIndex) {
return {
type: ReactMultiChildUpdateTypes.MOVE)EXISTING,
content: null,
fromIndex: child._mountIndex,
fromNode: ReactReconciler.getNativeNode(child),
toIndex: toIndex,
afterNode: afterNode
}
}
function makeRemove(child, node) {
return {
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
content: null,
fromIndex: child._mountIndex,
fromNode: node,
toIndex: null,
afterNode: null
}
}
复制代码
以下图:旧集合中包含节点A,B,C,D。更新后集合中包含节点B,A,D,C。此时新旧集合进行diff差别化对比,发现B!=A,则建立并插入B至新集合,删除就集合A。以此类推,建立并插入A,D,C。删除B,C,D。this
React发现这类操做繁琐冗余,由于这些都是相同的节点,只是因为位置发生变化,致使须要进行烦杂低效的删除,建立操做,其实只要对这些节点进行位置移动便可。spa
针对这一现象,React提出优化策略:容许开发者对同一层级的同组子节点,添加惟一的key进行区别。3d
新旧集合所包含的节点以下图所示,进行diff差别化对比后,经过key发现新旧集合中的节点都是相同的节点,所以无需进行节点的删除和建立,只须要将旧集合中节点的位置进行移动,更为新集合中节点的位置,此时React给出的diff结果为:B,D不作任何操做,A,C进行移动便可。
源码分析一波: 首先,对新集合中的节点进行循环遍历,经过惟一的key判断新旧集合中是否存在相同的节点,若是存在相同的节点,则进行移动操做,但在移动前须要将当前节点在旧集合中的位置与lastIndex进行比较。不然不执行该操做。这是一种顺序优化手段,lastIndex一直更新,表示访问过的节点在旧集合中最右边的位置(即最大的位置)。若是新集合中当前访问的节点比lastIndex大,说明当前访问节点在旧集合中就比上一个节点位置靠后,则该节点不会影响其余节点的位置,所以不用添加到差别队列中,即不执行移动操做
以上面的那个图为例子:
- 重新集合中取得B, 判断旧集合中是否存在相同节点B,此时发现存在节点B,接着经过对比节点位置判断是否进行移动操做。B在旧集合中的位置B._mountIndex = 1,此时lastIndex = 0,不知足child._mountIndex < lastIndex的条件,所以不对B进行移动。更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中prevChild._mountIndex表示B在旧集合中的位置,则lastIndex = 1。并将B的位置更新为新集合中的位置prevChild._mountIndex = nextIdex,此时新集合中**B._mountIndex = 0, nextIndex ++**进入下一个节点判断。
- 重新集合中取得A, 而后判断旧集合中是否存在A相同节点A,此时发现存在节点A。接着经过对比节点位置是否进行移动操做。A在旧集合中的位置A._mountIndex=0,此时lastIndex=1,知足child._mountIndex < lastIndex条件,所以对A进行移动操做enqueueMove(this, child._mountIndex, toIndex),其中toIndex其实就是nextIndex,表示A须要移动的位置。更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),则lastIndex = 1。并将A的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中A._mountIndex = 1, nextIndex++ 进入下一个节点的判断。
- 重新集合中取得 D,而后判断旧集合中是否存在相同节点 D,此时发现存在节点 D,接着经过对比节点位置判断是否进行移动操做。D 在旧集合中的位置D._mountIndex = 3,此时 lastIndex = 1 ,不知足 child._mountIndex < lastIndex 的条件,所以不对 D 进行移动操做。更新 lastIndex=Math.max(prevChild._mountIndex, lastIndex) ,则 lastIndex = 3 ,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex ,此时新集合中 D._mountIndex = 2 , nextIndex++ 进入下一个节点的判断。
- 重新集合中取得 C,而后判断旧集合中是否存在相同节点 C,此时发现存在节点 C,接着 经过对比节点位置判断是否进行移动操做。C 在旧集合中的位置 C._mountIndex = 2 ,此 时 lastIndex = 3 ,知足 child._mountIndex < lastIndex 的条件,所以对 C 进行移动操做 enqueueMove(this, child._mountIndex, toIndex) 。更新 lastIndex = Math.max(prevChild. _mountIndex, lastIndex) ,则 lastIndex = 3 ,并将 C 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex ,此时新集合中 A._mountIndex = 3 , nextIndex++ 进 入下一个节点的判断。因为 C 已是最后一个节点,所以 diff 操做到此完成。
若是有新增的节点和删除的节点diff如何处理呢?(如下都是复制的,码不动字了....)
- 重新集合中取得B,而后判断旧集合中存在是否相同节点 B,能够发现存在节点 B。因为 B 在旧集合中的位置 B._mountIndex = 1 ,此时 lastIndex = 0 ,所以不对 B 进行移动操做。 更新 lastIndex = 1 ,并将 B 的位置更新为新集合中的位置 B._mountIndex = 0 , nextIndex++ 进入下一个节点的判断。
- 重新集合中取得 E,而后判断旧集合中是否存在相同节点 E,能够发现不存在,此时能够 建立新节点 E。更新 lastIndex = 1 ,并将 E 的位置更新为新集合中的位置, nextIndex++ 进入下一个节点的判断。
- 重新集合中取得 C,而后判断旧集合中是否存在相同节点 C,此时能够发现存在节点 C。 因为 C 在旧集合中的位置 C._mountIndex = 2 , lastIndex = 1 ,此时 C._mountIndex > lastIndex ,所以不对 C 进行移动操做。更新 lastIndex = 2 ,并将 C 的位置更新为新集 合中的位置, nextIndex++ 进入下一个节点的判断。
- 重新集合中取得 A,而后判断旧集合中是否存在相同节点 A,此时发现存在节点 A。因为 A 在旧集合中的位置 A._mountIndex = 0 , lastIndex = 2 ,此时 A._mountIndex < lastIndex , 所以对 A 进行移动操做。更新 lastIndex = 2 ,并将 A 的位置更新为新集合中的位置, nextIndex++ 进入下一个节点的判断。
- 当完成新集合中全部节点的差别化对比后,还须要对旧集合进行循环遍历,判断是否存 在新集合中没有但旧集合中仍存在的节点,此时发现存在这样的节点 D,所以删除节点 D, 到此 diff 操做所有完成。
这一篇读的有点乱...稍微总结一下下:
- React 经过diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
- React 经过分层求异的策略,对 tree diff 进行算法优化;
- React 经过相同类生成类似树形结构,不一样类生成不一样树形结构的策略,对 component diff 进行算法优化;
- React 经过设置惟一 key的策略,对 element diff 进行算法优化;