Vue 源码解析 Diff
Vdom
VNode
1 | export default class VNode { |
节点类型
注释节点
createEmptyVNode + isComment
1 | export const createEmptyVNode = (text: string = "") => { |
文本节点
createTextVNode
1 | export function createTextVNode(val: string | number) { |
元素节点
tag attributes
组件节点
组件节点除了有元素节点具有的属性之外,它还有两个特有的属性
- componentOptions: 组件的 option 选项,如组件的 props 等
- componentInstance: 当前组件节点对应的 Vue 实例
函数式组件节点
函数式组件节点相较于组件节点,它又有两个特有的属性
- fnContext: 函数式组件对应的 Vue 实例
- fnOptions: 组件的 option 选项
克隆节点
cloneVNode + isCloned
1 | export function cloneVNode(vnode: VNode): VNode { |
Dom-Diff
patch 过程,主要有三个过程,创建节点,删除节点,更新节点
创建节点
新的 VNode 中有而旧的 oldVNode 中没有,就在旧的 oldVNode 中创建。判断三类节点
- 元素节点: 判断该 VNode 节点是否有 tag 标签 + createElement
- 注释节点: isComment + createComment
- 文本节点: createTextNode
1 | function createElm(vnode, parentElm, refElm) { |
删除节点
新的 VNode 中没有而旧的 oldVNode 中有,就从旧的 oldVNode 中删除
1 | function removeNode(el) { |
更新节点
新的 VNode 和旧的 oldVNode 中都有,就以新的 VNode 为准,更新旧的 oldVNode。分为几种情况
VNode / oldVNode 均为静态节点
isStatic,跳过,无需处理
VNode 为文本节点
VNode 为元素节点
1 | function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { |
更新子节点
双层循环遍历 newChildren 和 oldChildren: 每循环外层 newChildren 数组里的一个子节点,就去内层 oldChildren 数组里找看有没有与之相同的子节点。分为几种情况
创建子节点
newChildren 里有,而 oldChildren 里没有: 创建节点,创建好之后再把它插入到 DOM 中合适的位置
合适的位置是所有未处理节点之前,而并非所有已处理节点之后
下图节点顺序插入出错
删除子节点
newChildren 里没有,而 oldChildren 里有
更新子节点
newChildren 里有,且 oldChildren 里有,且位置相同
移动子节点
newChildren 里有,且 oldChildren 里有,但位置不同: 以 newChildren 里子节点的位置为基准,调整 oldChildren 里该节点的位置,使之与在 newChildren 里的位置相同
- 以 newChildren 里子节点的位置为基准,调整 oldChildren 里该节点的位置
所有未处理节点之前就是要移动的目的位置
diff 对比过程
对比顺序: 4 指针双向遍历
- newStartIdx: newChildren 数组里开始位置的下标
- newEndIdx: newChildren 数组里结束位置的下标
- oldStartIdx: oldChildren 数组里开始位置的下标
- oldEndIdx: oldChildren 数组里结束位置的下标
- 在循环的时候,每处理一个节点,newStartIdx 和 oldStartIdx 往后 +1 (只会加),newEndIdx 和 oldEndIdx 往前 -1 (只会减),当开始位置大于结束位置时,表示所有节点都已经遍历过了
优化前策略
- 如果 oldStartVnode 不存在,则直接跳过,将 oldStartIdx 加 1,比对下一个
- 如果 oldEndVnode 不存在,则直接跳过,将 oldEndIdx 减 1,比对前一个
- 如果新前与旧前节点相同,就把两个节点进行 patch 更新,同时 oldStartIdx 和 newStartIdx 都加 1,后移一个位置
- 如果新后与旧后节点相同,就把两个节点进行 patch 更新,同时 oldEndIdx 和 newEndIdx 都减 1,前移一个位置
- 如果新后与旧前节点相同,先把两个节点进行 patch 更新,然后把旧前节点移动到 oldChilren 中所有未处理节点之后,最后把 oldStartIdx 加 1,后移一个位置,newEndIdx 减 1,前移一个位置
- 如果新前与旧后节点相同,先把两个节点进行 patch 更新,然后把旧后节点移动到 oldChilren 中所有未处理节点之前,最后把 newStartIdx 加 1,后移一个位置,oldEndIdx 减 1,前移一个位置
- 如果不属于以上四种情况,就进行常规的循环比对 patch
- 如果在循环中,oldStartIdx 大于 oldEndIdx 了,表示 oldChildren 比 newChildren 先循环完毕,那么 newChildren 里面剩余的节点都是需要新增的节点,把[newStartIdx, newEndIdx]之间的所有节点都插入到 DOM 中
- 如果在循环中,newStartIdx 大于 newEndIdx 了,表示 newChildren 比 oldChildren 先循环完毕,那么 oldChildren 里面剩余的节点都是需要删除的节点,把[oldStartIdx, oldEndIdx]之间的所有节点都删除
优化后策略
- 新前比旧前: 相同则进入更新节点流程,不同则进入下一步
- 新后比旧后: 相同则进入更新节点流程,不同则进入下一步
- 新后比旧前: 相同则进入更新节点流程,更新完后再将旧前移动到 oldVNode 数组中所有未处理节点之后,不同则进入下一步
- 新前比旧后: 相同则进入更新节点流程,更新完后再将旧后移动到 oldVNode 数组中所有未处理节点之前,不同则通过之前的循环方式查找
1 | // 循环更新子节点 |