本节简单看一下 组件更新 patch 过程
通过前面几节我们已经了解组件创建、组件挂载,以及响应式对象的依赖收集、派发通知的过程,接下来再来看一下数据更新时组件更新的过程。
在数据更新时会在 vm._update() 函数内会进行区分首次渲染或数据更新,我们现在分析数据更新过程。此时的 prevVnode、vnode 都存在并且是 VNode 类型,且 prevVnode 是上次页面渲染生成的虚拟 DOM 对象,而vnode 是本次页面渲染生成的虚拟 DOM 对象。然后在 patch() 函数内先通过 sameVnode(prevVnode,vnode) 函数比较两个虚拟对象,最后在以本次页面渲染的 vnode 为标准的情况下对 prevVnode 进行小的改动,由此完成 vnode 对页面的渲染,这就是 diff 算法。
首先判断新旧 vnode 的属性 key 是否相同,然后又判断同步组件的属性 tag、isComment、data、input 类型是否相等,最后对于异步组件判断 asyncFactory 是否为同一个异步组件工厂函数。
一、新旧节点不同
若新 vnode 与旧 prevVnode 对象不同,呢么逻辑上是比较简单的,本质上是替换已经存在的节点。
首先通过 createElm(vnode) 函数生成新 DOM 节点树并插入到父节点内。
然后从父节点内删除旧节点,在此过程中会执行 vnode.data.hook.destory() 函数,在此函数内又执行 vm.$destory() 函数销毁组件。在该 vm.$destory() 函数内会先后执行声明周期钩子: beforeDestory、destroyed .
二、新旧节点相同
当 vnode、oldvnode 对象是相同节点时,在 patchVnode() 函数内就需要遍历 children 节点进行不同的处理。首先 vnode 是否为文本节点即为判断 vnode.text 属性是否存在,若存在则直接替换文本内容。若不是文本节点,则分为以下情况:
- 如果都有
children属性且不相同,则使用updateChildren()函数进行更细致的分类。 - 只有
vnode.children存在,表示旧的节点不需要了,呢么直接将vnode创建为真实DOM对象节点,然后插入到父节点内。 - 只有
oldvnode.children存在,表示更新的是空节点,呢么直接删除oldvnode节点。 - 只有
oldvnode.text存在,则直接清除文本节点的内容。
重点看 updateChildren() 函数的过程:
- 新开始节点与旧开始节点对比,如果相同则表示
DOM位置是对的不用改,将新旧节点的开始坐标向后移动一位。 - 新结束节点与旧结束节点对比,如果相同则表示
ODM位置是对的不用修改,将新旧节点的结束坐标向前移动一位。 - 旧开始节点与新结束节点对比,若匹配,则位置不对需要更新
DOM节点,旧开始节点对应的真实DOM插入到最后一位,旧开始节点下标后移一位,新结束节点下标前移一位 - 旧结束节点与新开始节点对比,如果匹配,则位置不对需要更新
DOM节点,旧结束节点对应的真实DOM插入到旧开始节点对应的真实DOM节点的前面,旧结束节点下标前移一位,新开始节点下标后移一位 - 依据
key值查找
- 如果
vnode.key在已有的key内,则说明是已存在的节点,只是位置不对,直接移动位置 - 如果
vnode.key不在已有的key内,则说明是新增节点,则直接创建新的DOM节点,插入到旧开始节点对应的DOM前面。
- 以新的
vnode为标准,如果新的vnode节点列表处理完,旧的列表还有没有处理的节点,则直接删除旧的没有处理的节点。如果旧的节点处理完新的节点还有,则直接创建真实DOM节点插入。
为什么 v-for 里建议为每一项绑定 key属性,而且最好具有唯一性,而不建议使用 index 索引?
在 diff 过程比对内部做更新子节点时,会根据 oldvnode 内没有处理的节点得到一个 key 值和下标对应的对象集合,为的就是当处理 vnode 每一个节点时,能快速查找该节点是否是已有的节点,从而提高整个 diff 比对的性能。如果是一个动态列表,key 值最好能保持唯一性,但像轮播图那种不会变更的列表,使用 index 也是没有问题。