源码篇 虚拟DOM
电梯Vue 源码源码篇 剖析 Vue2 双向绑定原理源码篇 使用及分析 Vue 全局 API源码篇 虚拟DOM源码篇 模板编译源码篇 实例方法源码篇 生命周期源码篇 Vue 的扩展机制设计Vue Router 4 源码源码篇 Vue Router 4 上篇源码篇 Vue Router 4 中篇源码篇 Vue Router 4 下篇源码拉取步骤可以看这篇文章有讲解下面直接进入正题源码篇 剖析 Vue 双向绑定原理-CSDN博客前言虚拟 DOM 常用于单页面应用中例如 Vue、React 等框架。因此在前端面试中经常会被问到什么是虚拟 DOM它是如何实现的本文我们将通过源码分析来揭开虚拟 DOM 的神秘面纱。正文在分析虚拟 DOM 的实现源码之前我们先搞懂这两个问题什么是虚拟 DOM为什么要有虚拟 DOM我们看看官方对于虚拟 DOM 的说法Vue3 官方文档 - 渲染机制翻译过来就是React 官方文档Virtual DOM and Reconciliation翻译过来就是什么是虚拟 DOM定义一句话总结虚拟 DOM 是用 JavaScript 对真实 DOM 的一种轻量级描述。在页面真正渲染到浏览器之前框架会先用 JS 对象在内存中构建一棵“虚拟的 DOM 树”。比如真实的 DOM 是div idapp h1Hello/h1 pWorld/p /div我们将组成该真实 DOM 节点的关键点通过下面这个 JS 对象表现出来const vnode { tag: div, props: { id: app }, children: [ { tag: h1, children: Hello }, { tag: p, children: World } ] }vnode 对象就是这个真实 DOM 节点的 虚拟 DOM 节点为什么要有虚拟 DOM原因1真实 DOM 操作“很慢”DOM 是浏览器提供的 API每次改动都要触发重排和重绘如果更新频繁性能会明显下降。打个比方操作真实 DOM 直接在纸上写字、擦掉、再写操作虚拟 DOM 在草稿纸上改最后一次性誊到纸上原因2虚拟 DOM 更高效的更新机制先根据数据生成虚拟 DOM当数据变化时生成新的虚拟 DOM对比新旧虚拟 DOMdiff 算法找出差异只更新必要的部分假设一个计数器组件let count 0 function render() { return { tag: button, children: 点击次数${count} } }如果没有虚拟 DOM每次 count 时都需要重建整个按钮元素document.body.innerHTML button点击次数${count}/button但如果有虚拟 DOM只需要更新文本节点不用重新创建整个按钮const oldVNode render() count const newVNode render() updateDOM(oldVNode, newVNode)原因3虚拟 DOM 是 JS 对象不依赖浏览器 DOM可以让框架具备跨平台能力渲染成 浏览器 DOMWeb渲染成 原生控件React Native渲染成 小程序、自定义渲染引擎首先找到源码位置如图src/core/vdomVNode 类在 Vue 中实际存在一个 VNode 类它是 Vue 虚拟 DOM 的“基础数据结构”抽象描述了真实 DOM 节点的所有必要信息源码位置src/core/vdom/vnode.tsexport default class VNode { // ...变量初始化...省略... constructor( tag, // 标签名如 div 或 span data, // 与该节点相关的数据属性、指令、事件等 children, // 子节点VNode 数组 text, // 文本节点内容 elm, // 对应的真实 DOM 节点渲染后会被赋值 context, // 当前 VNode 所属的组件实例 componentOptions, // 如果是组件节点这里存放组件的选项props、listeners、tag等 asyncFactory // 异步组件的工厂函数 ) { this.tag tag this.data data this.children children this.text text this.elm elm this.ns undefined // 命名空间如 SVG 或 MathML this.context context // 所属组件上下文 this.fnContext undefined // 函数式组件上下文 this.fnOptions undefined // 函数式组件配置 this.fnScopeId undefined // 函数式组件作用域ID this.key data data.key // key用于 diff 时优化复用 this.componentOptions componentOptions this.componentInstance undefined // 组件实例如果是组件VNode this.parent undefined // 父 VNode用于递归遍历 this.raw false // 是否为原始HTML如v-html this.isStatic false // 是否为静态节点v-once 优化 this.isRootInsert true // 是否作为根节点插入控制 transition 动画 this.isComment false // 是否为注释节点 this.isCloned false // 是否为克隆节点优化复用 this.isOnce false // 是否带有v-once this.asyncFactory asyncFactory // 异步组件工厂 this.asyncMeta undefined // 异步组件元信息 this.isAsyncPlaceholder false // 是否是异步组件的占位节点 } get child() { return this.componentInstance } }在这段源码中我们看到了几种类型的节点类型名称判断依据说明元素节点tag有值且不是组件普通 HTML 标签如div、span文本节点text有值、tag为空纯文本内容如Hello Vue注释节点isComment true对应 HTML 注释节点!-- xxx --组件节点componentOptions有值对应自定义组件my-button /会有componentInstance异步组件占位节点isAsyncPlaceholder true异步组件加载中时显示的占位符克隆节点isCloned true为优化 diff静态节点复用而克隆出的节点对应源码中的属性如下this.tag // 有值时表示标签节点元素或组件 this.text // 有值时表示文本节点 this.isComment // 是否是注释节点 this.componentOptions // 有值时表示组件节点 this.isAsyncPlaceholder // 异步组件占位节点 this.isCloned // 克隆节点下面我们一一进行说明1.元素节点有 tag 属性有 class、attributes 等 data 属性有描述子节点信息的 children 属性...2.文本节点有 text 属性用来表示具体的文本内容源码位置src/core/vdom/vnode.tsexport function createTextVNode(val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) }3.注释节点有 text 属性描述具体的注释信息isComment 属性判断是否是注释节点源码位置src/core/vdom/vnode.tsexport const createEmptyVNode (text: string ) { const node new VNode() node.text text node.isComment true return node }4.组件节点有 tag 属性并且不能是原生 DOM 标签它还有两个属性componentOptions组件的 option 选项普通 VNode 只需要标签、属性、子节点组件节点需要保存更多上下文信息。例如它对应哪个组件构造器它有哪些 props它的 render 函数是什么它的父 vnode 是谁它的生命周期钩子有哪些源码位置src/types/options.tsexport type ComponentOptions { // Vue 3 支持 Composition API 的 setup()在组件创建前调用用于返回响应式状态或渲染函数 setup?: (props: Recordstring, any, ctx: SetupContext) unknown [key: string]: any componentId?: string // 组件的响应式数据系统 data: object | Function | void props?: | string[] | Recordstring, Function | ArrayFunction | null | PropOptions propsData?: object computed?: { [key: string]: | Function | { get?: Function, set?: Function, cache?: boolean } } methods?: { [key: string]: Function } watch?: { [key: string]: Function | string } // 组件的渲染逻辑 el?: string | Element template?: string render: (h: () VNode) VNode renderError?: (h: () VNode, err: Error) VNode staticRenderFns?: Array() VNode // 生命周期钩子 beforeCreate?: Function created?: Function beforeMount?: Function mounted?: Function beforeUpdate?: Function updated?: Function activated?: Function deactivated?: Function beforeDestroy?: Function destroyed?: Function errorCaptured?: () boolean | void serverPrefetch?: Function renderTracked?(e: DebuggerEvent): void renderTriggerd?(e: DebuggerEvent): void // 当前组件的“私有资源”注册表这些资源只在该组件内有效 directives?: { [key: string]: object } components?: { [key: string]: Component } transitions?: { [key: string]: object } filters?: { [key: string]: Function } // 依赖注入系统 provide?: Recordstring | symbol, any | (() Recordstring | symbol, any) inject?: | { [key: string]: InjectKey | { from?: InjectKey; default?: any } } | Arraystring model?: { prop?: string, event?: string } parent?: Component mixins?: Arrayobject name?: string extends?: Component | object delimiters?: [string, string] comments?: boolean inheritAttrs?: boolean abstract?: any // 标识是否是组件 _isComponent?: true _propKeys?: Arraystring // 关联父级 vnode _parentVnode?: VNode _parentListeners?: object | null _renderChildren?: ArrayVNode | null _componentTag: string | null // 设置样式作用域 ID _scopeId: string | null // 保存构造器基类 _base: typeof Component }componentInstance当前组件节点对应的 Vue 实例注下面提到的 patch 会在后面单独分析当 Vue 的 patch 过程执行到“创建组件 VNode”时它会做以下事情从 componentOptions 中拿到组件构造器 Ctor执行 new Ctor(options)创建一个新的 Vue 实例组件实例把这个实例挂载到 VNode 上vnode.componentInstance new Ctor(options)再让这个实例去执行 mount()、渲染自己的模板等在 Vue 2 源码的 create-component.js 中有这样的逻辑// 创建组件实例 const component new Ctor(options) // 绑定回虚拟节点 vnode.componentInstance component // 调用 $mount() 让它真正挂载 component.$mount(hydrating ? vnode.elm : undefined, hydrating)5.异步组件占位节点isAsyncPlaceholder 标记的 VNode 本身不渲染实际内容它只在等待异步组件被加载完毕后被替换掉源码位置src/core/vdom/create-component.tssrc/core/vdom/helpers/resolve-async-component.tssrc/core/vdom/patch.ts这里我们只看简化版的关键部分在 createComponent 方法中return createAsyncPlaceholder(asyncFactory, data, context, children, tag)export function createAsyncPlaceholder( factory: Function, data: VNodeData | undefined, context: Component, children: ArrayVNode | undefined, tag?: string ): VNode { // 创建一个空的 VNode const node createEmptyVNode() // 保存异步工厂函数 node.asyncFactory factory // 保存上下文 node.asyncMeta { data, context, children, tag } return node }而 createEmptyVNode() 会返回一个空的 VNodeexport function createEmptyVNode(text?: string): VNode { const node new VNode() node.text text || node.isComment true // 空 VNode 实际是注释节点 return node }因此异步占位节点实际上是一个带 isComment true 的空节点。在 patch 阶段实际是这么判断异步占位节点的export function createPatchFunction(backend) { // ... if (isTrue(vnode.isComment) isDef(vnode.asyncFactory)) { vnode.isAsyncPlaceholder true return true } // ... }标记 vnode.isAsyncPlaceholder true 后异步组件未加载时VNode 树保持完整占位节点不会渲染实际 DOM加载完成后平滑替换。6.克隆节点该节点是为了Vue 对模板中静态节点做优化时使用会在下面单独分析源码位置src/core/vdom/vnode.ts// 输入一个已有的 VNode输出一个新的 VNode export function cloneVNode(vnode: VNode): VNode { // 通过 new VNode(...) 创建一个新的虚拟节点 const cloned new VNode( vnode.tag, vnode.data, vnode.children vnode.children.slice(), // 克隆子节点数组避免引用原数组 vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ) // 克隆其余属性 cloned.ns vnode.ns cloned.isStatic vnode.isStatic cloned.key vnode.key cloned.isComment vnode.isComment cloned.fnContext vnode.fnContext cloned.fnOptions vnode.fnOptions cloned.fnScopeId vnode.fnScopeId cloned.asyncMeta vnode.asyncMeta cloned.isCloned true return cloned }DOM-Diff一句话定义对比虚拟 DOM 树的差异找出需要更新的部分然后只更新必要的节点避免整体重渲染。而这个过程就叫做 patch这个过程主要做三件事1.创建 DOM判断 oldVnode 是否存在如果不存在说明是首次渲染 → 创建新 DOM当数据变化时判断新旧 vnode 是否相同节点sameVnode—— 判断依据新旧节点的 key 和 tag 分别都相同2.更新 DOM是同一个节点以新的VNode为准更新旧的oldVNode3.删除 DOM不是同一个节点直接删除旧节点创建新节点源码位置: /src/core/vdom/patch.js1.创建节点简化版代码如下// 根据给定的虚拟节点创建对应的真实 DOM 节点并将其挂载到父节点 parentElm 下 function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { // 获取 vnode 属性 const data vnode.data const children vnode.children const tag vnode.tag if (isDef(tag)) { // 创建普通元素节点 vnode.elm vnode.ns // 命名空间 ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) // 实际创建 DOM setScope(vnode) // 处理作用域样式 createChildren(vnode, children, insertedVnodeQueue) // 递归创建子节点 if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) // 调用 create 钩子 } insert(parentElm, vnode.elm, refElm) // 插入到父节点真正显示到页面 } else if (isTrue(vnode.isComment)) { // 处理注释节点 vnode.elm nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { // 处理文本节点 vnode.elm nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }2.更新节点function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { if (oldVnode vnode) return // 同节点直接返回 if (isDef(vnode.elm) isDef(ownerArray)) vnode ownerArray[index] cloneVNode(vnode) // 克隆 vnode防止修改原数组 const elm (vnode.elm oldVnode.elm) // 将旧 vnode 对应的真实 DOM 复用给新 vnode后续更新只修改属性、文本或子节点而不替换 DOM if (isTrue(oldVnode.isAsyncPlaceholder)) { // 处理异步占位节点 if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder true } return } if ( isTrue(vnode.isStatic) isTrue(oldVnode.isStatic) vnode.key oldVnode.key (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { // 如果是静态节点且 v-once直接复用旧 vnode 的组件实例避免重复渲染静态内容 vnode.componentInstance oldVnode.componentInstance return } let i const data vnode.data if (isDef(data) isDef((i data.hook)) isDef((i i.prepatch))) { i(oldVnode, vnode) } const oldCh oldVnode.children const ch vnode.children if (isDef(data) isPatchable(vnode)) { for (i 0; i cbs.update.length; i) cbs.update[i](oldVnode, vnode) if (isDef((i data.hook)) isDef((i i.update))) i(oldVnode, vnode) } // 这段代码单独分析 if (isUndef(vnode.text)) { // ... } else if (oldVnode.text ! vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef((i data.hook)) isDef((i i.postpatch))) i(oldVnode, vnode) } }我们单独分析一下下面这部分的代码const oldCh oldVnode.children const ch vnode.children if (isUndef(vnode.text)) { if (isDef(oldCh) isDef(ch)) { if (oldCh ! ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (__DEV__) checkDuplicateKeys(ch) if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ) addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ) } } else if (oldVnode.text ! vnode.text) { nodeOps.setTextContent(elm, vnode.text) }1.vnode.text 不存在 → 有子节点旧子节点 新子节点都存在 → updateChildren 执行 Diff新子节点存在但旧子节点不存在 → addVnodes 添加旧子节点存在但新子节点不存在 → removeVnodes 删除旧文本存在 → 清空文本2.vnode.text 存在 → 文本节点与旧文本比较不同则 setTextContent 更新总结来说patchVNode 方法流程如下在上面的流程图中可以看到如果新旧 VNode 里都包含了子节点那对于子节点的更新就调用了 updateChildren 方法 下面是整理后的代码源码位置 /src/core/vdom/patch.tsfunction updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx 0 let newStartIdx 0 let oldEndIdx oldCh.length - 1 let oldStartVnode oldCh[0] let oldEndVnode oldCh[oldEndIdx] let newEndIdx newCh.length - 1 let newStartVnode newCh[0] let newEndVnode newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm const canMove !removeOnly while (oldStartIdx oldEndIdx newStartIdx newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode oldCh[oldStartIdx] } else if (isUndef(oldEndVnode)) { oldEndVnode oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode oldCh[oldStartIdx] newStartVnode newCh[newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode oldCh[--oldEndIdx] newEndVnode newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode oldCh[oldStartIdx] newEndVnode newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode oldCh[--oldEndIdx] newStartVnode newCh[newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] undefined canMove nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode newCh[newStartIdx] } } if (oldStartIdx oldEndIdx) { refElm isUndef(newCh[newEndIdx 1]) ? null : newCh[newEndIdx 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }updateChildren() 采用了 双端比较double-end diff算法通过同时从新旧子节点的头尾两端进行对比提高 diff 效率基本变量说明oldStartIdx, oldEndIdx // 旧子节点数组的开始和结束索引 newStartIdx, newEndIdx // 新子节点数组的开始和结束索引 // 每次循环时会用下面这四个端点进行比对 oldStartVnode, newStartVnode // 当前从前向后比较的节点 oldEndVnode, newEndVnode // 当前从后向前比较的节点算法同时从两端开始比对如果 两端节点相同则直接调用 patchVnode() 更新如果不同则尝试各种交叉匹配头对尾、尾对头如果找不到匹配则尝试通过 key 定位旧节点若仍找不到就创建新的真实 DOM 元素插入最后根据剩余情况添加或移除多余的节点const canMove !removeOnlyremoveOnly 是一个特殊标志表示暂时只删除不移动节点当 canMove true 时表示允许进行节点移动操作while (oldStartIdx oldEndIdx newStartIdx newEndIdx) { ... }不断地在循环中进行sameVnode 判断是否相同patchVnode 更新insertBefore 进行移动有时还会把旧节点标记为 undefined表示已处理1.跳过空节点如果某些旧节点已经被处理成 undefined被复用、移动或替换后oldCh[idxInOld] undefined这个旧节点在 oldCh 数组中被标记为“空位”但索引还在。所以当循环指针继续移动时有可能会“指向一个已被置为 undefined 的位置”此时就跳过继续if (isUndef(oldStartVnode)) { oldStartVnode oldCh[oldStartIdx] } else if (isUndef(oldEndVnode)) { oldEndVnode oldCh[--oldEndIdx] }2.四种组合匹配策略最大化利用旧节点避免频繁创建新 DOMoldStartVnode vs newStartVnode如果相同表示前端节点未变patchVnode(oldStartVnode, newStartVnode) oldStartIdx newStartIdxoldEndVnode vs newEndVnode如果相同表示尾部节点未变patchVnode(oldEndVnode, newEndVnode) oldEndIdx-- newEndIdx--oldStartVnode vs newEndVnode旧前端节点匹配新后端节点旧前端节点被移到末尾patchVnode(oldStartVnode, newEndVnode) insertBefore(parentElm, oldStartVnode.elm, nextSibling(oldEndVnode.elm)) oldStartIdx newEndIdx--oldEndVnode vs newStartVnode旧后端节点匹配新前端节点旧后端节点被移到开头patchVnode(oldEndVnode, newStartVnode) insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndIdx-- newStartIdx3.四种组合都不匹配新节点的起始节点 newStartVnode 在旧节点列表中可能在“中间某个位置”也可能是“完全新增”的节点。因此Vue 会尝试“在旧节点中查找新节点”if (isUndef(oldKeyToIdx)) oldKeyToIdx createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] undefined canMove nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode newCh[newStartIdx]1.构建 key 到 index 的映射表if (isUndef(oldKeyToIdx)) oldKeyToIdx createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)生成一个 { key: index } 的映射表用来快速查找旧节点中相同 key 的节点比如旧子节点 oldCh [A(key1), B(key2), C(key3)] oldKeyToIdx { 1:0, 2:1, 3:2 }2.查找新节点在旧节点中的位置idxInOld isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)如果 newStartVnode 有 key就直接查表 oldKeyToIdx如果 没有 key就调用 findIdxInOld()通过 sameVnode 逐个对比寻找相同节点结果如果找到了 → idxInOld 是旧节点在 oldCh 中的索引如果没找到 → idxInOld 是 undefined3.如果没找到创建新节点if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) }说明这个新节点完全是“新增”的不在旧的虚拟节点树中调用 createElm() 创建新真实 DOM插入到当前 oldStartVnode 之前4.如果找到了旧节点可复用else { vnodeToMove oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] undefined canMove nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } }这段代码复用了上面的逻辑5.移动新索引指针newStartVnode newCh[newStartIdx]处理完当前新节点后移动指针准备进入下一轮比较至此虚拟 DOM 源码篇 over