0%

Vue2.x的patch方法

作用

将VNode渲染成真实的DOM

核心概念

patch方法

patch可以理解成打补丁的意思,在patch方法中采用diff算法比较新旧节点,一边比较一边给真实的DOM打补丁。

diff算法

diff算法用来比较新旧节点,比较只会在同层级比较,不会跨层级比较,这个是相对于传统diff算法的一个很大提升。

过程分析
  • 判断两节点是否值得比较(sameVNode),值得比较则执行patchVNode方法。这个方法做了如下事情:

    1. 找到当前oldVNode对应的真实节点,称为el;

    2. 判断VNode和oldVNode是否指向同一个对象,如果是,直接返回;

    3. 如果两者都是文本节点且不相等,将el的文本节点设置为VNode的文本节点;

    4. 如果oldVNode有子节点而VNode没有,删除el的子节点;

    5. 如果oldVNode没有子节点而VNode有,则将VNode的子节点生成真实节点,添加到el;

    6. 如果两者都有子节点,则通过updateChildren函数比较子节点。由于updateChildren方法是diff中相对复杂的一部分,拎出来单独分析一下。

  • 不值得比较则用VNode替换oldVNode。具体流程如下:

    1. 找到当前oldVNode对应的真实节点以及该节点的父节点;
    2. 根据VNode生成新节点;
    3. 将新节点添加到父节点;
    4. 移除旧节点。
updateChildren方法

updateChildren方法用来给新旧VNode都有子节点的情况打补丁。可以将新旧VNode理解成两个数组,以旧数组为基础,通过删除、移动、插入的方式,将旧数组的值转换成新数组的值。

⚠️这里的值指的不是两个数组所在的存储地址,只是数组项的值。而且每次删除、移动、插入的时候都会操作真实DOM。

匹配过程

旧数组:[oldS,…,oldE]

新数组:[S, …, E]

其中oldS和oldE是待匹配旧节点的开始子节点和结束子节点,S和E是待匹配新节点的开始子节点和结束子节点。这里的oldS、oldE、S和E在diff过程中是会变的,可以理解成一个变量或者一个代号。因为在diff过程中,如果新旧子节点已经匹配过了,会从待匹配的新旧节点列表中移除。

可以分为5个匹配方式来对比是否是sameVNode,来按顺序完成匹配:

  1. 旧首新首对比:匹配成功会将旧结束子节点移动到真实DOM中目前可供选择位置的最前,因为不能影响到已经patch好的子节点位置,然后移除待匹配的新旧节点列表中的开始子节点;

  2. 旧尾旧尾对比:匹配成功会将旧开始子节点移动到真实DOM中目前可供选择位置的最后,因为不能影响到已经patch好的子节点位置,然后移除待匹配的新旧节点列表中的结束子节点;

  3. 旧首新尾对比:匹配成功会将旧开始子节点移动到真实DOM中目前可供选择位置的最后,因为不能影响到已经patch好的子节点位置,然后移除待匹配的旧节点列表中的开始子节点和新节点列表中的结束子节点;

  4. 旧尾新首对比:匹配成功会将旧结束子节点移动到真实DOM中目前可供选择位置的最前,因为不能影响到已经patch好的子节点位置,然后移除待匹配的旧节点列表中的结束子节点和新节点列表中的开始子节点;

  5. 如果上面四种没有匹配成功会分成两种情况:

    • 如果新旧子节点都存在key,会根据旧节点的key生成一张hash表,用S的key与hash表做匹配,判断是否时为sameVNode。
      1. 匹配成功:将匹配成功的节点移动到真实DOM中目前可供选择位置的最前,因为不能影响到已经patch好的子节点位置,然后移除待匹配旧节点中已经匹配到S的key的节点和待匹配新节点中的S;
      2. 匹配失败:将S生成真实DOM节点,插入到目前可供选择位置的最前,也就是oldS的位置,然后然后移除待匹配新节点中的S。
    • 如果没有key,将直接将遍历待匹配旧节点列表。
      1. 匹配成功:将匹配成功的节点移动到真实DOM中目前可供选择位置的最前,因为不能影响到已经patch好的子节点位置,然后移除待匹配旧节点中已经匹配到S的节点和待匹配新节点中的S;
      2. 匹配失败:将S生成真实DOM节点,插入到目前可供选择位置的最前,也就是oldS的位置,然后然后移除待匹配新节点中的S。

    ⚠️ 常见场景解析:

    • v-for的时候设置key,就是为了如果首首、尾尾、首尾、尾首这四种匹配方式没匹配到的时候,可以使用key来更快地寻找可复用的节点,而不是只能用遍历的方式。

    • 不能用index索引来做key的原因也是这个,因为用index索引来做key,并不能找到想要复用的旧节点,甚至可能会导致一个子节点都复用不了,起到负面作用。

      举个🌰:一个数组长度是偶数的数组用index做key,被反序后,会给每一个子节点匹配到一个错误的不可复用的子节点,使diff的效率比没加key更低。

  6. 循环上面的过程,不断的将待匹配节点列表向内部收缩。当待匹配的新旧节点列表有一个先被清空的话,将执行下面判断。

    ⚠️ 这里说的待匹配列表只是为了帮助理解来提出的一个概念,并没有一个真实的变量来存储这个待匹配列表。真实在数组中描述待匹配列表,是通过移动开始节点和结束节点的指针位置来实现。

    1. 待匹配的旧节点列表被清空,也就是oldStartIdx > oldEndIdx,说明旧节点都被patch了,还有新节点没被处理到,批量新增待匹配的新节点;
    2. 待匹配的新节点列表被清空,也就是newStartIdx > newEndIdx,说明新节点都被patch了,还有多余的旧节点没被处理到,批量删除待匹配的旧节点。
目的

完成这一些复杂的匹配,就是为了更快更多地复用DOM的旧节点,因为DOM节点的创建开销是很大的。其实更快和更多是矛盾的,Vue找到了一个平衡点,比较只会在同层级进行, 不会跨层级。

调用时机

首次渲染

父子组件在加载的时候,生命周期执行的顺序为:

父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted。

下面从首次渲染的流程角度,来分析为什么是这个顺序。

首次渲染时,父组件render生成父组件的VDOM,然后会执行父组件的patch方法,目的是将VDOM渲染成DOM。

在执行父组件patch方法解析到子组件VNode的时候,子组件VNode是被父组件的render函数生成的组件类型的VNode。在生成组件类型的组件过程中,完成了构造子类构造函数、安装组件钩子函数和实例化VNode,生成的子组件是没有children的,这个时候不知道子组件的细节。

patch到子组件VNode时,会触发子组件生命周期中的init hook,在init hook中开始对子组件进行挂载,将子组件当作一个新的Vue实例,重新进行初始化、render生成VDOM,然后执行子组件的patch。

解析子组件VNode这个过程,以深度优先的算法,将所有的子孙组件挂载到根实例上。有子组件就先完成子组件的完整挂载过程,等子组件完成挂载后再回到父组件完成父组件的挂载。从这个逻辑来理解,上面的生命周期执行顺序就很好理解了。

首次渲染patch过程比较简单,就是简单的插入,因为旧节点为空。

完成patch之后 ,将更新后的VNode数据赋值给oldVNode。

数据更新的时候

数据更新触发渲染watcher更新的时候,会重新生成render函数,生成新的VNode,供patch方法为旧DOM打补丁使用。即使数据更新发生在很多组件上,每次执行渲染watcher更新,重新patch的时候关注的都只是当前组件。

⚠️ 每个组件都有自己的渲染watcher。

数据更新时patch过程就会相对复杂,会更充分使用到diff算法的细节。oldVNode就是上次渲染时的VNode,VNode就是本次重新生成的VNode,通过diff算法,一边一边比较一边给真实的DOM打补丁。

完成patch之后 ,将更新后的VNode数据赋值给oldVNode。