深入浅出Vue.js阅读——虚拟DOM——patch

patch

1. patch介绍

  patch的目的其实是修改DOM节点,也可以理解为视图渲染。patch不是暴力替换节点,而是在现有的DOM上进行修改来达到渲染视图的目的。对现有DOM修改需要做三件事:

  • 创建新增的节点
  • 删除已经废除的节点
  • 修改需要更新的节点

1. 新增节点

  新增节点,本质上是为了使用JavaScript的计算成本来换取DOM的操作成本。只有那些因为状态的改变而新增的节点在DOM中并不存在时,我们才需要创建一个节点并插入到DOM中。
新增节点的场景:

  • oldVnode不存在而vnode存在
  • vnode和oldVnode完全不是听一个节点

2. 删除节点

  当一个节点只在oldVnode中存在时,需要把它从DOM中删除。
  在渲染视图时,需要以vnode为标准,所以vnode中不存在的节点都属于被废弃的节点,而被废弃的节点需要从DOM中删除。

3. 更新节点

  在删除和新增节点的操作中,如果两个虚拟节点是完全不同的,需要以新节点为标准渲染视图。
  当新旧节点是同一个节点时,需要对这两个节点进行比较细致的对比,然后对oldVnode在视图中所对应的真实节点进行更新。

4. 小结

  当oldVnode不存在时,直接使用vnode渲染视图。
  当oldVnode和vnode都存在但不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素。
  当oldVnode和vnode是同一个节点时,使用更详细的对比操作对真实的DOM节点进行更新。

在这里插入图片描述

2. 创建节点

  只有三种类型的节点会被插入到DOM中:

  • 元素节点
    元素节点具有tag属性。创建元素节点的步骤:
    1. 调用当前环境下的createElement方法来创建真实的元素。
    2. 将它的子节点也创建出来,并 插入到这个刚创建出的节点下面。
      创建子节点是一个递归过程。vnode中的children属性上保存了当前节点的所有子虚拟节点:
      1. 将vnode中的children属性都循环一遍
      2. 将每个子虚拟节点都执行一遍创建元素的逻辑
    3. 调用当前环境下的appendChild方法,将元素插入到指定的父节点中。
      在这里插入图片描述
  • 注释节点:
    注释节点有一个唯一的标识属性isComment,如果是注释节点,调用当前环境下的ceateComment方法来创建真实的注释节点并将其插入到指定的父节点中。
  • 文本节点:
    文本节点只有一个text属性。如果是文本节点,调用当前环境下的createTextNode方法来创建真实的文本节点并将其插入到指定的父节点中。

创建节点并渲染到视图的全过程:
在这里插入图片描述

3. 删除节点

删除实现逻辑如下:

function removeNodes(vnodes,startIdx,endIdx){
    for(;startIdx<=endIdx;startIdx++){
        const ch = vnodes[startIdx];
        if(isDef(ch)){
            removeNode(ch.elm)
        }
    }
}

删除vnode数组中从startIdx指定的位置到endIdx指定位置的内容。
removeNode用于删除视图中的单个节点,而removeVnodes用于删除一组指定的节点。逻辑如下:

const nodeOps = {
    removeChild(){
        nodeOps.removeChild(child)
    }
}

function removeNode(el){
    const parent = nodeOps.parentNode(el);
    if(isDef(parent)){
        nodeOps.removeChild(parent,el);
    }
}

将当前元素从它的父节点中删除,其中nodeOps是对节点操作的封装。

4. 更新节点

  更新节点不是很暴力的使用新节点覆盖旧节点,而是通过对比找出两个新旧节点不一样的地方,针对那些不一样的地方进行更新。

1. 静态节点

  静态节点指的是那些一旦渲染到界面上之后,无论日后状态如何变化,都不会发生任何变化的节点。
  更新节点时,首先要判断新旧两个节点是否是静态节点,如果是,就不需要进行更新操作,可以直接跳过更新节点的操作。

2. 新虚拟节点有文本属性

  当新虚拟节点有文本属性,并且和旧虚拟节点的文本属性不一样时,我们可以直接把视图中真实的DOM节点的内容改成新虚拟节点的文本。

3. 新虚拟节点无文本属性

  如果新创建的虚拟节点没有text属性,那么它就是一个元素节点。元素节点通常会有子节点,也就是children属性,但也有可能没有子节点,所以存在两种不同的情况:

1. 有children的情况

  当新创建的虚拟节点有children属性时,其实还会有两种情况,那就是要看旧虚拟节点是否有children属性。

  • 旧虚拟节点有children属性
    对新旧两个虚拟节点进行更详细的对比并更新。
  • 旧虚拟节点没有children属性
    说明旧虚拟节点要么是空标签,要么是有文本的文本节点。 如果是文本节点,先把文本清空变成空标签,然后将新虚拟节点中的children挨个创建成真实的DOM元素并将其插入到视图中的DOM节点下面。
2. 无children的情况

  当新创建的虚拟节点既没有text属性也没有children属性时,这说明这个新创建的节点是一个空节点,它下面既没有文本也没有子节点,这时如果旧虚拟节点中有子节点就删除子节点,有文本就删文本。有什么删什么,最后达到视图中是空标签的目的。

4.小结

更新节点的逻辑:
在这里插入图片描述
在源码中,真实的实现过程如下:
在这里插入图片描述

5. 更新子节点

  更新子节点大概可以分为四种操作:更新节点、删除节点、新增节点、移动节点位置》更新节点更多的是在讨论什么情况下需要更新操作,什么情况下新增节点等。

1.更新策略

1. 创建子节点

关注问题:
  什么情况下需要创建节点?把创建的节点插入到真实的DOM节点中的哪个位置?
  创建节点的操作是在循环体内执行的,其具体实现实在oldChildren(旧子节点列表)中寻找本次循环所指向的新子节点。
  如果在oldChildren中没有找到与本次循环所指向的新子节点相同的节点,那么说明本次循环所指向的新子节点是一个新增节点。对于新增节点,我们需要执行创建节点的操作,并将新创建的节点插入到oldChildren中所有未处理节点(未处理就是没有进行任何更新操作的节点)的前面。当节点成功插入DOM后,这一轮循环就结束了。

2. 更新子节点

  更新节点本质上是一个节点同时存在于newChildrenoldChildren中时需要执行的操作。

3. 移动子节点

  移动节点通常发生在newChildren中的某个节点和oldChildren中的某个节点是同一个节点,但是位置不同,所以在真实的DOM中需要将这个节点的位置以新虚拟节点的位置为基准进行移动。
  通过Node.insetBefore()方法,我们可以成功的将一个已有节点移动到一个指定的位置。

4. 删除子节点

  删除子节点,本质上删除那些oldChildren中存在但newChildren中不存在的节点
  当newChildren中的所有节点都被循环了一遍后,也就是循环结束后,如果oldChildren中还有剩余的没有被处理的节点,那么这些节点就是被废弃、需要删除的节点。

2. 优化策略

问题场景:
  只修改了列表中某个数据的内容,而没有新增数据或者删除数据,这种情况下newChildrenoldChildren中所有节点的位置都是相同的,这时节点的位置就是可以预测的,不需要循环也可以知道oldChildren中的哪个节点和被寻找的新子节点是同一个节点。
解决办法:
  只需要尝试使用相同位置的两个节点来对比是否是同一个节点:如果恰巧是同一个节点,直接就可以进入更多节点的操作;如果尝试失败了,再用循环的方式来查找节点。
  这样做可以很大程度的避免循环oldChildren来查找节点,从而使执行速度得到很大的提升。
  我们把这种很快速查找节点的方式成为快捷查找,那么他们共有四种查找方式:

  • 新前与旧前
  • 新后与旧后
  • 新后与旧前
  • 新前与旧后
    解释:
    新前newChildren中所有未处理的第一个子节点
    新后newChildren中所有未处理的最后一个节点
    旧前oldChidren中所有未处理的第一个子节点
    旧后oldChildren中所有未处理的最后一个节点
  1. 新前与旧前
    如果“新前”和"旧前"的位置相同,所以并不需要执行移动节点的操作,只需要更新节点即可。
  2. 新后与旧前
    如果“新后”与”旧前“这两个节点的位置相同,所以只需要执行更新节点的操作即可,不需要执行移动节点的操作。
  3. 新后与旧前
    当”新后“与”旧前“是同一个节点时,在真实DOM中除了做更新操作外,还需要将节点移动到oldChildren中所有未处理节点的最后面。
  4. 新前与旧后
    当”新前“与”旧后“是同一个节点时,在真实DOM中除了做更新操作外,还需要将节点移动到oldChildren中所有未处理节点的最前面。

3. 哪些节点是未处理过的

  由于优化策略,节点是有可能从后面对比的,对比成功就会进行更新处理,也就是说,我们的循环体内的逻辑由于优化策略,不再只是处理所有未处理过的节点的第一个,而是有可能会处理最后一个,这种情况下就不能从前向后循环,而应该是从中间向两边循环。
如何实现两边向中间循环?
  先准备四个变量:

  • oldStratIdx:oldChildren的开始位置的下标;
  • oldEndIdx:oldChildren的结束位置的下标;
  • newStartIdx:newChildren的开始位置下标;
  • newEndIdx:newChildren的结束位置下标;
      在循环体内,每处理一个节点,就将下标向指定的方向移动一个位置,通常情况下是对新旧两个节点进行更新操作,就相当于一次处理两个节点,将新旧两个节点的下标都向指定方向移动一个位置。
      开始位置所表示的节点被处理后,就向后移动一个位置;结束位置的节点被处理后,则向前移动一个位置。
      也就是说,oldStartIdxnewStartIdx只能向后移动,而oldEndIdxnewEndIdx只能向前移动。
      当开始位置大于结束位置时,说明所有节点都遍历过了,则结束循环:
while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){
	//do something
}

  这个循环条件是无论newChildren或者oldChildren,只要他们两个中有一个循环完毕,就会退出循环。那么,当新子节点和旧子节点的节点数量不一致时,会导致循环结束后仍然有未处理的节点,也就是说这个循环将无法覆盖所有节点。
  如果是oldChildren先循环完毕,这个时候newChildren中还有剩余的节点,说明这些节点都是需要新增的节点,直接把这个节点插入到DOM中就行了。
  如果是newChildren先循环完毕,这个时候oldChildren中还有剩余的节点,说明这些节点都是被废弃的节点,是应该被删除的节点。
  找到newChildren中所剩余的节点:因为oldChildren先被循环完毕,所以此时newStartIdx肯定小于newEndIdx的,那么在newChildren中,下标在newStartIdxnewEndIdx之间的所有点都是未处理过的节点。
  找到oldChildren中所剩余的节点:因为newChildren先被循环完毕,所以oldStartIdx小于oldEndIdx,那么在oldChildren中,下标在oldStartIdxoldEndIdx之间的所有节点都是未处理过的节点。

4. 小结

  在新增节点中,详细讨论了什么情况下需要创建子节点,以及把创建的子节点插入到什么位置。
  更新子节点的过程,如果在oldChildren中可以找到与新子节点相同的节点,就需要更新它们。
  如果在oldChildren中找到节点的位置和新子节点的位置不一样,需要将DOM中的节点移动到新子节点所在的位置。
  删除节点的操作发生在循环结束之后。当循环结束之后,oldChildren中所有未处理的节点都是需要被删除的节点。
更新子节点的整体流程:
在这里插入图片描述
oldStartVnode:oldChildren中所有未处理的第一个节点,与前文中提到的"旧前"是同一个节点。
oldEndVnode:oldChildren中所有未处理的最后一个节点,与前文中提到的"旧后"是同一个节点。
newStartVnode:newChildren中所有未处理的第一个节点,与前文中提到的"新前"是同一个节点。
newEndVnode:newChildren中所有未处理的最后一个节点,与前文中提到的"新后"是同一个节点。
  在上图中,有一部分逻辑是建立keyindex索引的对应关系。在Vue.js的模板中,渲染列表时可以为节点设置一个属性key,这个属性可以标识一个节点的唯一ID。
  前面提到过,在更新子节点时,需要在oldChildren中循环去找一个节点。但是如果我们在渲染列表时,为子节点设置了属性key,那么在建立keyindex索引的对应的关系时,就生成了一个key对应着一个节点下标这样一个对象。也就是说,如果在节点上设置了属性key,那么在oldChildren中找相同节点时,可以直接通过key拿到下标,从而获取节点。这样我们根本不需要通过循环来查找节点。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值