对比两颗树上所有的节点,传统的方式使用依次比较两棵树的每一个节点,这样的时间复杂度是 O(n^3)。
比如:当前有三个节点,比较完树上的每一个节点需要的时间是 O(n^3)。其中 n 是节点个数。
- patch(oldVnode, newVnode)
- 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化
- diff 过程只进行同层级比较,时间复杂度 O(n)
100, 100
100, 1000000
patch
-
功能:
- 传入新旧 VNode,对比差异,把差异渲染到 DOM
- 返回新的 VNode,作为下一次 patch() 的 oldVnode
-
执行过程:
- 首先执行模块中的钩子函数
pre
- 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
- 调用 patchVnode(),找节点的差异并更新 DOM
- 如果 oldVnode 是 DOM 元素
- 把 DOM 元素转换成 oldVnode
- 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
- 把刚创建的 DOM 元素插入到 parent 中
- 移除老节点
- 触发用户设置的
create
钩子函数
- 首先执行模块中的钩子函数
-
源码位置:src/snabbdom.ts
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 保存新插入节点的队列,为了触发钩子函数 const insertedVnodeQueue: VNodeQueue = []; // 执行模块的 pre 钩子函数 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm if (!isVnode(oldVnode)) { // 把 DOM 元素转换成空的 VNode oldVnode = emptyNodeAt(oldVnode); } // 如果新旧节点是相同节点(key 和 sel 相同) if (sameVnode(oldVnode, vnode)) { // 找节点的差异并更新 DOM patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 如果新旧节点不同,vnode 创建对应的 DOM // 获取当前的 DOM 元素 elm = oldVnode.elm!; parent = api.parentNode(elm); // 触发 init/create 钩子函数,创建 DOM createElm(vnode, insertedVnodeQueue); if (parent !== null) { // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中 api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); // 移除老节点 removeVnodes(parent, [oldVnode], 0, 0); } } // 执行用户设置的 insert 钩子函数 for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]); } // 执行模块的 post 钩子函数 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 返回 vnode return vnode; };
patchVnode
-
功能:
- patchVnode(oldVnode, vnode, insertedVnodeQueue)
- 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
-
执行过程:
- 首先执行用户设置的 prepatch 钩子函数
- 执行 create 钩子函数
- 首先执行模块的 create 钩子函数
- 然后执行用户设置的 create 钩子函数
- 如果 vnode.text 未定义
- 如果
oldVnode.children
和vnode.children
都有值- 调用
updateChildren()
- 使用 diff 算法对比子节点,更新子节点
- 调用
- 如果
vnode.children
有值,oldVnode.children
无值- 清空 DOM 元素
- 调用
addVnodes()
,批量添加子节点
- 如果
oldVnode.children
有值,vnode.children
无值- 调用
removeVnodes()
,批量移除子节点
- 调用
- 如果 oldVnode.text 有值
- 清空 DOM 元素的内容
- 如果
- 如果设置了
vnode.text
并且和和oldVnode.text
不等- 如果老节点有子节点,全部移除
- 设置 DOM 元素的
textContent
为vnode.text
- 最后执行用户设置的 postpatch 钩子函数
-
源码位置:src/snabbdom.ts
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { const hook = vnode.data?.hook; // 首先执行用户设置的 prepatch 钩子函数 hook?.prepatch?.(oldVnode, vnode); const elm = vnode.elm = oldVnode.elm!; let oldCh = oldVnode.children as VNode[]; let ch = vnode.children as VNode[]; // 如果新老 vnode 相同返回 if (oldVnode === vnode) return; if (vnode.data !== undefined) { // 执行模块的 update 钩子函数 for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); // 执行用户设置的 update 钩子函数 vnode.data.hook?.update?.(oldVnode, vnode); } // 如果 vnode.text 未定义 if (isUndef(vnode.text)) { // 如果新老节点都有 children if (isDef(oldCh) && isDef(ch)) { // 使用 diff 算法对比子节点,更新子节点 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { // 如果新节点有 children,老节点没有 children // 如果老节点有text,清空dom 元素的内容 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); // 批量添加子节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 如果老节点有children,新节点没有children // 批量移除子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { // 如果老节点有 text,清空 DOM 元素 api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { // 如果没有设置 vnode.text if (isDef(oldCh)) { // 如果老节点有 children,移除 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } // 设置 DOM 元素的 textContent 为 vnode.text api.setTextContent(elm, vnode.text!); } // 最后执行用户设置的 postpatch 钩子函数 hook?.postpatch?.(oldVnode, vnode); }
updateChildren
-
功能:
- diff 算法的核心,对比新旧节点的 children,更新 DOM
-
执行过程:
- 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
- 在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
- 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为 O(n)
- 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
- 在对开始和结束节点比较的时候,总共有四种情况
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
- 开始节点和结束节点比较,这两种情况类似
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
- 调用 patchVnode() 对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
- oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
- 调用 patchVnode() 对比和更新节点
- 把 oldStartVnode 对应的 DOM 元素,移动到右边 - 更新索引
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
- 调用 patchVnode() 对比和更新节点
- 把 oldEndVnode 对应的 DOM 元素,移动到左边
- 更新索引
- 如果不是以上四种情况
- 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
- 如果没有找到,说明 newStartNode 是新节点
- 创建新节点对应的 DOM 元素,插入到 DOM 树中
- 如果找到了
- 判断新节点和找到的老节点的 sel 选择器是否相同
- 如果不相同,说明节点被修改了
- 重新创建对应的 DOM 元素,插入到 DOM 树中
- 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
- 循环结束
- 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
- 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除