Vue源码之patch

664 阅读8分钟

patch

抛出问题

  1. 何时触发

  2. 新旧节点不是相同节点怎么更新渲染

  3. 新旧节点是相同节点怎么更新渲染

  4. 父组件值变化影响子组件需要子组件重新渲染如何触发子组件的重新渲染

  5. diff算法比较逻辑

何时触发

当触发值改变时,或触发set,然后触发dep.notify(),最终执行updateComponent也就是vm._update(vm._render(), hydrating),首先获取到新的vnode然后执行_update:

可以看到第一次初始化渲染时把旧的vnode赋值给了vm._vnode,后面再次渲染时把首次渲染的vnode赋值给了preVnode,最后调用了vm.patch(prevVnode, vnode) 传入新旧vnode。也就调用了patch函数。这⾥执⾏ patch 的逻辑和⾸次渲染是不⼀样的,因为 oldVnode 不为空,并且它和 vnode 都是VNode 类型,接下来会通过 sameVNode(oldVnode, vnode) 判断它们是否是相同的 VNode 来决定⾛不同的更新逻辑。



sameVnode 的逻辑⾮常简单,如果两个 vnode 的 key 不相等,则是不同的;否则继续判断对于同步组件,则判断isComment 、 data 、 input 类型等是否相同,对于异步组件,则判断asyncFactory 是否相同。

新旧节点不是相同节点怎么更新渲染

首先createElm以当前旧节点为参考节点,创建新的节点,并插⼊到 DOM 中,然后找到当前 vnode 的⽗的占位符节点,先执⾏各个 module 的 destroy的钩⼦函数,如果当前占位符是⼀个可挂载的节点,则执⾏ module 的 create 钩⼦函数最后把 oldVnode 从当前 DOM 树中删除。对于新旧节点不同的情况,就是创建新节点 -> 更新占位符节点 -> 删除旧节点。

新旧节点是相同节点怎么更新渲染

新旧节点相同,它会调⽤ patchVNode ⽅法。

比较两个vnode包括三种类型的操作:属性更新,文本更新,子节点更新。

规则如下:

  1. 新老vode都无子节点,进行文本替换,一般就是遍历到了子节点的尽头。
  2. 老vode无子节点而新vode有子节点,先清空老vode的文本节点,然后为其增加新节点。
  3. 老vode有子节点而新vode无子节点,移除老vode的所有子节点。
  4. 新老vode均有子节点,则对子节点进行diff操作,调用的是updateChildren。

父组件值变化影响子组件需要子组件重新渲染如何触发子组件的重新渲染

上面图中知道当更新的 vnode 是⼀个组件 vnode 的时候,会执⾏ prepatch 的⽅法,他定义在create-component.js中。

prepatch ⽅法就是拿到新的 vnode 的组件配置以及组件实例,去执⾏ updateChildComponent。

updateChildComponent 的逻辑也⾮常简单,由于更新了 vnode ,那么 vnode 对应的实例 vm的⼀系列属性也会发⽣变化,包括占位符 vm.$vnode 的更新、 slot 的更新, listeners 的更新, props 的更新等等,然后就顺理成章的触发了子组件的set最终执行子组件updateComponent也就是vm._update(vm._render(), hydrating),首先获取到新的vnode然后执行子组件的_update。

diff算法比较逻辑

updateChildren(深度优先 同级比较)

  • 抛出问题:两个节点进行比较传统怎么写,我们会写一个双循环进行比较。缺点就是循环次数太多了。

  • 减少双循环次数优化:
    前后节点两两比较大概率可以找到相同的节点,怎么确认是否同一个节点,使用key能高效的确认节点是否是相同节点。

  • 创建四个指针 oldStartIdx newStartIdx oldEndIdx newEndIdx

  • 比较顺序:

    1. 老开头与新开头比(oldStartIdx与newStartIdx)

    2. 老结尾与新结尾比(oldEndIdx与newEndIdx)

    3. 老开头与新结尾比(oldStartIdx与newEndIdx)

    4. 老结尾与新开始比(oldStartIdx与newStartIdx)

    5. 实在找不到:双循环

  • 开始条件:
    老开头指针<=老结尾指针 && 新开头指针<=新结尾指针

  • 比较处理:

    1. 老开头与新开头比对成功:深度优先继续patchVnode比较子vode,各开始指针+1,子节点比对完再跳出来继续同级比较。

    2. 老结尾与新结尾比对成功:深度优先继续patchVnode比较子vode,各结束指针-1,子节点比对完再跳出来继续同级比较。

    3. 老开头与新结尾比对成功:深度优先继续patchVnode比较子vode,将老节点移动到末尾,开始指针+1,结束指针-1,子节点比对完再跳出来继续同级比较。

    4. 老结尾与新开始比对成功:深度优先继续patchVnode比较子vode,将老节点移动到首,开始指针+1,结束指针-1,子节点比对完再跳出来继续同级比较。

    5. 实在找不到:进行双循环比对查找在老的孩子数组中的索引,找到创建新元素追加,除了深度优先继续patchVnode比较子vode,还要移动到队首。

  • 结束处理
    当不满足开始条件时跳出比对逻辑,此时存在三种情况:

    1. 新老vode没有多余的子节点:老vode添加null。
    2. oldStartIdx > oldEndIdx 新vode子节点比老vode子节点多并且确实存在新节点 添加节点。
    3. newStartIdx > newEndIdx 新vode子节点比老vode子节点少 删除节点。

总结

组件更新的过程核⼼就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。新旧节点不同的更新流程是创建新节点->更新⽗占位符节点->删除旧节点;⽽新旧节点相同的更新流程是去获取它们的 children,根据不同情况做不同的更新逻辑。最复杂的情况是新旧节点相同且它们都存在⼦节点,那么会执⾏ updateChildren 逻辑,这块⼉可以借助画图的⽅式配合理解。

1.当值发生改变触发set最终触发vm._update(vm._render()),首先_render获取新的vnode,接着调用vm._update也就是调用patch传入新旧节点,此时的旧节点是上次创建的vnode节点。接着会通过sameVnode函数来新旧节点是否为相同节点,sameVnode首先判断新旧节点的key是否相同,接着判断tag标签是否相同,是否同时是注释节点,是否data同时定义,是否相同input类型。如果不是相同节点那么就很简单,createElm创建新的节点,如果节点存在parent那么更新父占位符节点,最后删除旧节点。如果是相同节点执行patchVnode函数。

2.patchVnode首先根据旧节点获取挂载节点。其次获取新节点的data判断data中是否有hook和prepatch如果有说明这个vnode就是组件vnode就会执行prepatch方法,prepatch最终会执行updateChildComponent触发组件的重新渲染会执行到组件的patch对比中。如果不是组件vnode就会去获取新老节点的children。新老vnode都无子节点,进行文本替换,一般就是遍历到了子节点的尽头。老vnode无子节点而新vnode有子节点,先清空老vnode的文本节点,然后为其增加新节点。老vnode有子节点而新vode无子节点,移除老vode的所有子节点新老vode均有子节点,则对子节点进行diff操作,调用的是updateChildren。

3.updateChildren首先创建四个指针 oldStartIdx newStartIdx oldEndIdx newEndIdx,对比开始条件老开头指针<=老结尾指针 && 新开头指针<=新结尾指针,对比操作:老开头与新开头比对成功:深度优先继续patchVnode比较子vode,各开始指针+1,子节点比对完再跳出来继续同级比较。老结尾与新结尾比对成功:深度优先继续patchVnode比较子vode,各结束指针-1,子节点比对完再跳出来继续同级比较。老开头与新结尾比对成功:深度优先继续patchVnode比较子vode,将老节点移动到末尾,开始指针+1,结束指针-1,子节点比对完再跳出来继续同级比较。 老结尾与新开始比对成功:深度优先继续patchVnode比较子vode,将老节点移动到首,开始指针+1,结束指针-1,子节点比对完再跳出来继续同级比较。实在找不到:进行双循环比对查找在老的孩子数组中的索引,找到创建新元素追加,除了深度优先继续patchVnode比较子vode,还要移动到队首。结束处理:新老vode没有多余的子节点:老vode添加null,新vode子节点比老vode子节点多并且确实存在新节点添加节点,新vode子节点比老vode子节点少删除节点。