持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情
当响应式数据被修改的时候,触发set方法,执行Dep.notify通知相关的Watcher去调用vm._update()进行更新视图。在update方法中会将新旧的VNode对象进行__patch__,patch的核心就是diff算法。本文将重点讲解Vue2中的diff算法。Vue2的diff算法是全量更新的,在Vue3中是增加了静态标记等优化,这个会在后续进行总结。
传送门:vue2diff算法源码地址
patch
/src/core/vdom/patch.ts
patch是将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图。
首先先看patch代码的核心部分,其余大家自行查阅源码。 分为两种情况:
- 当
oldValue不存在时,则会创建新节点。 - 当
oldValue已经存在,则会调用sameVnode判断新旧Vnode是否同一节点- 如果是同一节点,则进入pathVnode,更新比对过程。
- 不是同一节点,则直接创建新的节点、更新父占位符节点、删除旧节点。(具体不在本文详解)
sameVnode
那么如何判断新旧Vnode是否为同一节点呢,可以看看sameVnode方法:
当两个VNode的tag(节点的标签名)、key、isComment(是否为注释节点)都相同, 并且同时定义或未定义data的时候,且如果标签为input则type必须相同时,才可判断这两个VNode则算同一节点。才会进入pathVnode。
pathVnode
我们分段拆解,pathVnode大致有以下步骤:
- 两个VNode节点相同则直接返回
- 静态节点处理:如果新旧VNode都是静态的,同时它们的key相同,并且新的VNode是clone或者是标记了v-once属性,那么只需要替换elm以及componentInstance即可,直接跳过更新比对过程。
-
当新节点没有text文本时:
- 新旧节点均有子节点,则对新旧子节点进行对比,调用updateChildren,也是diff的核心。
- 如果新节点有子节点,旧节点没有子节点,则判断旧节点是否有文本内容,如果有则清空旧节点的文本内容,再为当前DOM节点创建新增子节点
- 如果新节点没有子节点,旧节点有子节点,则删除旧节点的子节点。
- 新旧节点均无子节点,则判断旧节点是否有文本内容,有则清空。
-
当新老节点都是文本节点时,则判断新旧文本内容是否相同进行文本更新即可。
updateChildren
updateChildren 也就是平常说的diff算法,今天就来一探他的真面目。 在新旧两组节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢,当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。 在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。
首先旧始节点,旧尾节点和新始节点,新尾节点两两对比,一共四种方法。分别为:
- 旧始节点(oldStartVnode)与新始节点(newStartVnode)对比
- 旧尾节点(oldEndVnode)与新尾节点(newEndVnode)对比
- 旧始节点(oldStartVnode)与新尾节点(newEndVnode)对比
- 旧尾节点(oldEndVnode)与新始节点(newStartVnode)对比
具体源码分析如下:
当旧始节点和新始节点(oldStartVnode, newStartVnode)或者旧尾节点和新尾节点(oldEndVnode, newEndVnode)满足sameVnode时,直接将该VNode节点进行patchVnode即可。
当旧始节点和新尾节点(oldStartVnode,newEndVnode)满足sameVnode,说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
当旧尾节点和新始节点(oldEndVnode,newStartVnode)满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。
如果以上4种情况都没找到,则从新数组的第一个节点去旧数组中去查找,找到了就进行递归更新,没找到则创建新节点。
当循环结束,即当旧节点的所有子节点先遍历完(oldStartIdx < oldEndIdx) 或者新节点的所有子节点先遍历完(newStartIdx > newEndIdx),则会进行以下操作
总结
比对新老两个虚拟DOM,当旧不存在时,则会创建新节点。当旧节点已经存在,则会判断新旧节点是否同一节点。如果是同一节点,则进入pathVnode进行对比。
如果新旧节点相同则直接返回。
如果不同,则判断新旧节点的文本节点。
当新节点没有文本内容时,如果新节点有子节点,旧节点没有子节点,则判断旧节点是否有文本内容,如果有则清空旧节点的文本内容,并新增子节点。如果新节点没有子节点,而旧节点有子节点,则删除旧子节点。如果新旧节点均无孩子,则判断旧节点是否有文本内容,有则清空。新旧节点均有子节点,则对新旧子节点进行对比。
新旧孩子节点对比会先先对旧始节点,旧尾节点和新始节点,新尾节点两两对比,一共四种方法,当旧新节点的开始节点或者新旧节点的为节点为同一节点时,则会进行patchVnode对比。当旧始节点和新尾节点为同一节点时,进行patchVnode对比的同时还需要将真实DOM节点移动到旧尾节点的后面。当旧尾节点和新始节点为同一节点时,进行patchVnode对比的同时真实的DOM节点移动到了旧始节点的前面。
如果以上4种情况都没找到,则从新数组的第一个节点去旧数组中去查找,找到了就进行递归更新,没找到则创建新节点。
当循环结束,当旧节点的所有子节点先遍历完,则为需要将是新增的节点插入到真实DOM节点中去,
当新节点的所有子节点先遍历完,则需要将多余的旧节点从真实的DOM节点中移除。