Vue2源码解析 patch(将vnode渲染成真实dom)

目录

1  patch介绍

1.1  新增节点

1.2  删除节点

1.3  更新节点

1.4  patch运行流程

 2  创建节点

 3  删除节点

 4  更新节点

4.1  静态节点

4.2  新虚拟节点有文本属性

4.3  新虚拟节点无文本属性

4.4  更新节点小结

 5  更新子节点

5.1  更新策略

5.2  优化策略

5.3  哪些节点是未处理过的

5.4  更新子节点小结

 6  总结


1  patch介绍

虚拟dom最核心的部分是patch,它可以将vnode渲染成真实的dom。

patch也可以叫作patching算法,通过它渲染真实dom时,对比新旧两个vnode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新。

patch是在现有dom上进行修改来达到渲染视图的目的,对现有dom进行修改需要做三件事:

        1.创建新增的节点;

        2.删除已经废弃的节点;

        3.修改需要更新的节点。

1.1  新增节点

为了考虑性能问题,所以vue用对比更新的方法。

新增节点的场景1:当oldVnode不存在而vnode存在时,就需要使用vnode生成真实dom元素并将其插入到视图中去,如下图所示。

 新增节点的场景2:当vnode和oldVnode完全不是同一个节点时(可得知vnode就是一个全新的节点,而oldVnode就是一个被废弃的节点),需要使用vnode生成真实的dom元素并将其插入到视图当中,如下图所示。

 使用vnode创建一个新dom节点,用它去替换oldVnode所对应的真实dom节点。

1.2  删除节点

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

删除节点的场景2:当oldVnode和vnode完全不是同一个节点时,在dom中需要使用vnode创建的新节点替换oldVnode所对应的旧节点,而替换过程是新创建的dom节点插入到旧节点的旁边,然后再将旧节点删除。

1.3  更新节点

更新节点的场景1:新旧两个节点是同一个节点,需要把这两个节点进行细致的对比,然后对oldVnode在视图中所对应的真实节点进行更新,案例如下图所示。

1.4  patch运行流程

 2  创建节点

事实上,只有三种类型的节点会被创建并插入到dom中:元素节点、注释节点、文本节点

创建元素节点的过程:

        1.判断vnode是否是元素节点,只需要判断它是否具有tag属性即可。如果一个vnode具有tag属性,就认为是元素属性。

        2.调用当前环境下的createElement方法创建真实的元素节点

        3.调用appendChild将元素插入到指定的父节点中;

        4.元素节点通常会有子节点(children),所以当一个元素节点被创建后,需要将它的子节点也创建出来并插入到这个刚创建出的节点下面;

        5.创建子节点的过程是一个递归过程。vnode中的children属性保存了当前节点的所有子虚拟节点,所以只需要将vnode中的children属性循环一遍,将每个子虚拟节点都执行一遍创建元素的逻辑,就可以实现我们想要的功能。

将虚拟dom创建真实dom,最后渲染到视图的过程,如下图所示:

 一个元素节点从创建到渲染视图的过程,如下图所示:

 除了元素节点之外,要创建的还有注释节点和文本节点。

注释节点,vnode中不存在tag属性,isComment属性为true,用document.createComment创建并插入到指定的父节点中;

文本节点,vnode中不存在tag属性,isComment属性不为true,用docuemnt.createTextNode创建并插入到指定的父节点中。

创建节点并渲染到视图的过程如下图所示:

 3  删除节点

vue源码中删除元素的代码如下逻辑: 

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

删除vnodes数组中从startIdx指定到endIdx指定位置的内容;removeNode用于删除视图中单个节点,而removeVnodes用于删除一组指定的节点。

removeNode的实现逻辑如下:

// 将当前元素从它的父节点中删除,其中nodeOps是对节点操作的封装
const nodeOps = {
  removeChild(node,child){
    node.removeChild(child)
  }
}
function removeNode(el){
  const parent = nodeOps.parentNode(el)
  if(isDef(parent)){
    nodeOps.removeNode(parent,el)
  }
}

为什么不直接使用parent.removeChild(child)删除节点,而是将这个节点操作封装成函数放在nodeOps里呢?

因为跨平台渲染时框架的渲染机制和dom解耦。只要把框架更新dom时的节点操作进行封装,就可以实现跨平台渲染,在不同平台下调用节点的操作。

 4  更新节点

4.1  静态节点

在更新节点时,首先需要判断新旧两个虚拟节点是否是静态节点,如果是,就不需要进行新操作,可以直接跳过更新节点的过程。

4.2  新虚拟节点有文本属性

当新虚拟节点有文本属性,并且和就虚拟节点的文本属性不一样时以新虚拟节点vnode为准来更新视图),我们可以直接把视图中的真实dom节点的内容修改成新虚拟节点的文本。

4.3  新虚拟节点无文本属性

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

1.有children的情况

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

如果旧虚拟节点也有children属性,那么对新旧两个虚拟节点的children进行一个更详细的对比并更新。

如果旧虚拟节点没有children属性,那么说明旧虚拟节点要么是一个空标签,要么是有文本的文本节点。如果是文本节点,那么先把文本清空让它变成空标签,然后将新虚拟节点(vnode)中的children挨个创建真实的dom元素节点并将其插入到视图中的dom节点下面。

2.无children的情况

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

4.4  更新节点小结

更新节点的逻辑如下图所示:

 在源码中,真实过程如下图所示:

 5  更新子节点

更新子节点大概可以分为4中操作:更新节点、新增节点、删除节点、移动节点位置。

例如,newChildren(新子节点列表)中有一个节点在oldChildren(旧子节点列表)中找不到相同的节点,这说明这个节点是因为状态更改而新增的节点,此时需要进行新增节点的操作。

例如,newChildren中的某个节点和oldChildren中的某个子节点是一个节点,但位置不同,这说明由于状态变化而位置发生了移动的节点,这时需要节点的移动操作。

对比两个子节点列表(children),首先需要做的事情是循环。循环newChildren(新子节点列表),每循环到一个新子节点,就去oldChildren(旧子节点列表)中找和当前节点相同的那个旧子节点。如果在oldChildren中找不到,说明当前子节点是由于状态变化而新增的节点,我们要进行创建节点并插入视图的操作;如果找到了,就做更新操作;如果找到的旧子节点的位置和新子节点不同,则需要移动节点等。

5.1  更新策略

1.创建子节点

新旧两个子节点列表是通过循环进行对比的,所以创建节点的操作是在循环体内执行的,其具体是在oldChildren(旧子节点列表)中寻找本次循环所指向的新子节点。

如果在oldChildren中没有找到与本次循环所指向的新子节点相同的节点,那么说明本次循环所指向的新节点是一个新增节点。对于新增节点,需要执行创建节点的操作,并将新创建的节点插入到oldChilren中所有未处理节点的前面。当节点成功插入dom后,这一轮循环旧结束了。

案例:下入中最上面是真实的dom节点,左下角的节点是新创建的虚拟节点,右下角的节点是虚拟节点。

上图中,表示已经对前两个子节点进行了更新,当前正在处理第三个子节点。当右下角的虚拟子节点中找不到与左下角的第三个节点相同的节点时,证明是新增节点,需要创建节点并插入到真实dom中,插入的位置是所有未处理节点的前面(如下图所示),也就是所指定的位置。

 为什么插入到所有已处理节点的后面不行吗?不行,如果这个新节点后面也是新节点那插入位置就不正确了。如下图所示:

 2.更新子节点

更新节点本质上当一个节点同时存在于newChildren和oldChildren中时需要执行的操作。

情况1:两个节点是同一个节点并且位置相同,只需要更新节点的操作即可。如下图所示:

情况2:oldChildren中子节点的位置和本次 循环所指向的新子节点的位置不一致时,除了对真实dom节点进行更新操作外,还需要对真实dom节点进行移动节点的操作。

3.移动子节点

移动子节点的场景1:发生在newChildren中的某个节点和oldChildren中的某个节点是同一个节点,但是位置不同,所以在真实的dom中需要将这个节点的位置以新虚拟节点的位置为准进行移动,如下图所示。

 通过Node.insertBefore(),可以成功地将一个已有节点移动到一个指定的位置。

但是,怎么知道应该把节点移动到哪里呢?

答:这个节点的位置是所有未处理节点的第一个节点。对比两个子节点列表是通过从左到右循环newChildren这个列表,然后每循环一个节点,就去oldChildren中寻找与这个节点相同的节点进行处理。也就是说,newChildren中当前被循环到的这个节点的左边都是被处理过的。如下图所示:

 4.删除子节点

删除子节点,本质上是删除那些oldChildren中存在但newChildren中不存在的节点

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

5.2  优化策略

通常情况下,并不是所有子节点的位置都会发生移动,一个列表中总有几个节点的位置是不变的,我们需要有快捷的方式(非循环的方式)来找到这些节点。

快速的查找节点的方式称为快捷查询,共有4种方式,分别是:

        1.新前与旧后

        2.新后与旧后

        3.新后与旧前

        4.新前与旧后

新前:newChildren中所有未处理的第一个节点;

新后:newChildren中所有未处理的最后一个节点;

旧前:oldChildren中所有未处理的第一个节点;

旧后:oldChildren中所有未处理的最后一个节点。

 1.新前与旧前

由于新前和旧前的位置相同,属于同一个节点,所以并不需要执行移动节点的操作,只需要更新节点即可。

如果不是同一个节点,没关系,一共有4种快捷查询方式,挨个试一次。如果都不行,最后再使用循环来查询节点。

2.新后与旧后 

当新前和旧前对比不是同一个节点,尝试用新后与旧后对比是否是同一个节点。如果是同一个节点,就将这两个节点进行对比并更新视图,如下图所示;不是同一个节点继续尝试新后和旧前对比。

 3.新后与旧前

对比新后与旧前是否是同一个节点,如果是,对比并更新视图。如下图所示:

新后与旧前是同一个节点,但是位置不同,所以除了更新节点外,还需要执行移动节点的操作。如下图所示:

当新后与旧前是同一个节点时,在真实dom种除了做更新操作外,还需要将节点移动到oldChildren中所有未处理节点的最后面

为什么是移动到oldChildren中所有未处理节点的最后面?因为更新节点是以新虚拟节点为准,子节点也不例外。

 当真实dom子节点左右两侧已经有节点被更新,只有中间这部分节点未处理时,新后这个节点是未处理节点中的最后一个节点,所以真的dom节点移动位置时,需要移动到oldChildren中所有未处理节点的最后面。

 4.新前和旧后

新前和旧后进行对比是否是同一个节点,是则进行更新节点的操作。如下图所示:

 加入是同一个节点,但是位置不相同,除了更新节点外,还需要节点的移动操作,将节点移动到oldChildren中所有未处理节点的最前面。如下图所示:

将节点移动到oldChildren中所有未处理节点的最前面的原因:当真实的dom节点中已经有节点被更新,并且更新到第二个节点时,我们发现oldChildren中对应的节点在第三个位置上,这时需要将旧后这个节点更新并移动到第二个位置上,所以只需要将节点移动到所有未处理节点的最前面,就能实现移动到第二个位置的目的。如下图所示:

5.3  哪些节点是未处理过的

怎么分辨哪些节点是处理过的,哪些节点是未处理过的呢?
我们的逻辑都是在循环体内处理的,所有只要让循环条件保证只有未处理的节点才能进入循环体内,就能达到忽略已处理的节点从而对未处理节点进行对比和更新等操作。

但由于前面的优化策略,节点是有可能会从后面对比的,对比成功就会进行更新处理,也就是说,循环体内的逻辑由于优化策略,不再是只处理所有未处理的节点的第一个,而是有可能会处理最后一个,这种情况下不能从前往后循环,而是应该从两边向中间循环。

那么,怎样实现两边向中间循环呢?

首先,准备4个变量:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx;

在循环体内,每处理一个节点,就将下标向指定的方向移动一个位置,通常情况下是对新旧两个节点进行更新操作,就相当于一次性处理两个节点,将新旧两个节点的下标都向指定方向移动一个位置。

当开始位置大于等于结束位置时,说明所有节点都遍历过了,则结束循环:

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
  // to do
}

通过上面的循环条件,就可以保证循环体内的节点都是未处理的。

5.4  更新子节点小结

更新子节点的整体流程如下图所示:

 

 6  总结

虚拟dom的最关键部分是:patch;

通过patch可以对比新旧虚拟dom,从而只针对发生了变化的节点进行更新视图的操作。

注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值