首先要说diff算法以前,仍是稍微解释一下虚拟DOM,虽然大部分人都知道虚拟DOM的概念了。javascript
首先,不少人没有意识到一个问题,现代前端框架为咱们解决了什么? 我认为前端现代框架解决的是忽略对DOM的操做,让前端人员注重于维护状态。前端
对于视图更新以往的解决方式是,不关心任何状态,只须要将全部DOM删掉,而后从新生成一份DOM,可是这种访问DOM的方式会形成至关多的性能浪费。vue
而虚拟DOM在框架中的任务就是经过状态生成相应结构的虚拟节点树,使用新生成的虚拟节点树和上次的虚拟节点树进行对比,当某个状态发生变化时,而后只更新和渲染不一样的部分。java
在Vue1.0中采用了极高的细粒度,每个绑定一个对应的watcher实例,进行观察状态的变化,可是当状态越多的节点被使用时,会有一些内存开销以及一些依赖追踪的开销。node
在Vue2.0之后,引入了虚拟DOM,为单个组件设置一个watcher实例,即便组件内有10个节点,里面状态发生变化时,只会通知到组件,而后组件内部经过虚拟DOM进行对比和渲染。算法
虚拟DOM的生成数组
咱们来结合以前《Vue不看源码懂原理》系列——Vue模板编译中提到的模板渲染结合起来看。Vue经过编译将模板转换AST,以后将AST转换成渲染函数,执行渲染函数能够获得一个虚拟节点树(vnode),再拿新生成的虚拟节点树去和旧的虚拟节点树(oldVnode)对比,找出更新部分节点,最后作渲染。 前端框架
科普完毕,接下来一段用来解释上述中的vnode与oldVnode对比的过程,也就是你们常说的diff算法。app
diff算法解决的问题是否是暴力修改DOM,而是经过对象对DOM进行更新替换,通常包括三种主要逻辑:框架
前两种都相对比较简单,一个一个来讲起。
新增节点最多见的场景就是,当oldVnode不存在这个节点,而vnode存在时,那么表明它是一个全新的节点,须要使用vnode中的新节点去生成真实DOM插入到页面的DOM中。
拿元素节点举例,首先判断它是否带有tag属性,若是有那么它就是一个元素节点,调用对应的appendChild方法,将该节点插入到指定的父节点中。须要注意的是建立子节点是一个递归过程,就像你遍历一个对象树同样,咱们须要将vnode的chaildren属性进行循环一遍,为每一个父节点的子节点也执行一遍建立节点的逻辑。
删除节点的场景比较简单,即上面说到的当存在一个被废弃的节点时,咱们除了要插入新的替换节点,也要删除以前的DOM节点。
删除节点的实现逻辑以下:
function removeVnodes(vnodes,startIdx,endIdx){
for(;startIdx<=endIdx;++startIdx){
const ch = vnodes[startIdx]
if(isDef(ch)){
removeVnode(ch.elm) // 删除单个节点方法
}
}
}
复制代码
删除节点的逻辑就是删除vnodes数组中从startIdx指定位置到endIdx的内容便可。
Vue更新子节点的策略基于在比对子节点数组的时候,将接收的参数oldVnode的子节点构成的数组和nvnode的子节点构成的数组进行比较。
两个数组做比较只须要一个双层循环就搞定了,例如如今对oldVnode数组的第一个元素作判断,我要拿着这个元素去和vnode里面的元素一个个比过去,假设在对比到vnode中第三个元素的时候发现连个元素同样,则表示oldVNode数组的第一个元素的位置发生了变化,在新数组中它变到了第三的位置。此时咱们能够知道ldVnode数组的第一个元素位置变成了第三。 上面这种方式惟一存在的问题是效率过低。假设oldVnode和vnode有100个子元素,当咱们在比较oldVnode的最后一个元素的时候,发现它和vnode中的最后一个元素相同,这其实浪费了不少的计算资源。 所以vue对子节点更新进行了策略优化,Vue为oldVnode和vnode分别添加了一对游标,默认指向数组的第一个和最后一个元素,它实现的是一种从两边向中间查找的一种方式,全量查找至少在时间复杂度上减小了一倍。
- 若是oldStartIdx指向的元素为undefined则oldStartIdx右移,一样的若是oldEndIdx指向的元素不存在则oldEndIdx左移。这个操做的目的是快速去掉vnode左右两端的无效数据。为何会出现元素值为undefined呢?往下看就知道了。
- 若是oldStartIdx和newStartIdx是相同元素则对其调用patchVnode。oldStartIdx和newStartIdx都向右移动。 一样的,若是newEndIdx和oldEndIdx是相同元素对其调用patchVNode。newEndIdx和oldEndIdx都向左移动。咱们认为不少时候节点变化先后它的子节点数组的首尾元素还是相同元素。
- 若是oldStartIdx和newEndIdx是相同元素则对其调用patchVnode,oldStartIdx右移,newEndIdx左移。若是oldEndIdx和newStartIdx是相同元素则对其调用patchVnode,oldEndIdx左移,newStartIdx右移。
diff的核心是递归比较子节点
正常Diff两个树的时间复杂度是O(n * 3),但实际状况下咱们不多会进行跨层级的移动DOM,因此Vue将Diff进行了优化,从O(n * 3)> -> O(n),只有当新旧children都为多个子节点时才须要用核心的Diff算法进行同层级比较。 Vue2的核心Diff算法采用了双端比较的算法,同时重新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操做。相比React的Diff算法,一样状况下能够减小移动节点次数,减小没必要要的性能损耗,更加的优雅。
key在虚拟DOM的做用
新旧 children 中的节点只有顺序是不一样的时候,最佳的操做应该是经过移动元素的位置来达到更新的目的。 须要在新旧 children 的节点中保存映射关系,以便可以在旧children的节点中找到可复用的节点。key也就是children中节点的惟一标识。
到这呢就算把Vue中的节点更新过程就简单讲述一遍,可能有些逻辑讲述的通常,文笔有限。
最好能够结合上一篇一块儿看《Vue不看源码懂原理》系列——Vue模板编译
感谢点赞鼓励,
也欢迎讨论。