简介
说明
本文介绍vue的diff的原理。
vue的diff原理是基于虚拟DOM的。
diff原理简介
初始渲染
第一次渲染页面时,按此流程进行:模板=> 渲染函数=> 虚拟DOM=> 真实DOM
虚拟DOM是用JS对象来表示的,里边的每一个节点可以称为虚拟节点:Vnode。虚拟DOM可以理解为一个树状结构的对象。
页面改动
虚拟DOM状态变更的时候,重新构造一棵新的对象树,然后用新的树和旧的树进行比较(diff),记录两棵树差异。
把所记录的差异应用到真正的DOM树上(patch),视图就更新了。
diff原理详解
diff的两个特点
- 比较只会在同层级进行, 不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较
流程详述
采用深度优先遍历,把树形结构按照层级分解,只比较同级元素;当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁(两个重要函数patchVnode和updateChildren)。
- 判断根结点及变化后的节点是否是sameVnode。
- 如果不是的话,就会创建新的根结点并进行替换
- 如果是sameVnode,则进入patchVnode函数。
- patchVnode函数
- 如果两个节点是相等oldVnode === vnode则直接return
- 如果新节点是文本节点,则判断新旧文本节点是否一致,不一致(oldVnode.text !== vnode.text)则替换
- 如果新节点不是文本节点,则开始比较新旧节点的子节点oldCh和ch:
- 如果子节点都存在,则进行updateChildren计算(稍后讲)
- 如果只有新子节点存在,则如果旧节点有文本节点,则移除文本节点,然后将新子节点拆入
- 如果只有旧子节点存在,则移除所有子节点
- 如果均无子节点且旧节点是文本节点,则移除文本节点(此时新节点一定不是文本节点)
- updateChildren函数(做细致对比)
- start && oldStart对比
- end && oldEnd对比
- start && oldEnd对比
- end && oldStart 对比
- 生成map映射,(key:旧子节点上的key,value:旧子节点在自己点中的位置),根据key记录下老节点的位置(idxInOld)
- 如果找到了idxInOld,如果是相同节点则移动旧节点到新的对应的地方,否则虽然key相同但元素不同,当作新元素节点去创建
- 如果没有找到idxInOld,则创建节点
- 如果老节点先遍历完,则新节点比老节点多,将新节点多余的插入进去
- 如果新节点先遍历完,则就节点比新节点多,将旧节点多余的删除
diff中key的作用
diff算法比较后,会出现3种情况:
- 如果新虚拟DOM找到了与旧虚拟DOM相同的key,但新旧虚拟DOM内容没有变,新的虚拟DOM就不用创建真实DOM,直接复用之前旧虚拟DOM创建的真实DOM。
- 如果新虚拟DOM找到了与旧虚拟DOM相同的key,但新旧虚拟DOM内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
- 如果新虚拟DOM中未找到与旧虚拟DOM相同的key,则创建新的真实DOM,随后渲染到页面
最后总结,key正是diff算法的关键,通过这个key可以作为桥梁,连接起新旧两个虚拟DOM。
diff原理示意图
新旧节点的比较方法如下:
- 先找到不需要移动的相同节点,消耗最小
- 再找相同但是需要移动的节点,消耗第二小
- 最后找不到,才会去新建删除节点,保底处理
比如下图存在这两棵 需要比较的新旧节点树 和 一棵 需要修改的页面 DOM树
第一轮比较
因为父节点都是 1,所以开始比较他们的子节点。按照我们上面的比较逻辑,所以先找 相同 && 不需移动 的点。
毫无疑问,找到 2。
拿到比较结果,这里不用修改DOM,所以 DOM 保留在原地。
第二轮比较开始
此时没有 相同 && 不需移动 的节点 了。只能第二个方案:找相同的点。
找到 节点5,相同但是位置不同,所以需要移动。
拿到比较结果,页面 DOM 树需要移动DOM 了,不修改,原样移动
第三轮比较开始
此时相同节点也没得了,只能创建了。要根据 新Vnode 中没找到的节点去创建并且插入。
然后旧Vnode 中有些节点不存在 新VNode 中,所以要删除。
于是开始创建节点 6 和 9,并且删除节点 3 和 4