当前位置: 首页 > news >正文

源码篇 虚拟DOM

电梯

Vue 源码

源码篇 剖析 Vue2 双向绑定原理

源码篇 使用及分析 Vue 全局 API

源码篇 虚拟DOM

持续更新中...

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 id="app"><h1>Hello</h1><p>World</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
  • 对比新旧虚拟 DOM(diff 算法)
  • 找出差异,只更新必要的部分

假设一个计数器组件:

let count = 0function 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

可以让框架具备跨平台能力:

  • 渲染成 浏览器 DOM(Web)
  • 渲染成 原生控件(React Native)
  • 渲染成 小程序、自定义渲染引擎

首先找到源码位置如图:src/core/vdom

VNode 类

在 Vue 中实际存在一个 VNode 类,它是 Vue 虚拟 DOM 的“基础数据结构”,抽象描述了真实 DOM 节点的所有必要信息:

源码位置:src/core/vdom/vnode.ts

export default class VNode {// ...变量初始化...省略...constructor(tag,                // 标签名,如 'div' 或 'span'data,               // 与该节点相关的数据(属性、指令、事件等)children,           // 子节点(VNode 数组)text,               // 文本节点内容elm,                // 对应的真实 DOM 节点(渲染后会被赋值)context,            // 当前 VNode 所属的组件实例componentOptions,   // 如果是组件节点,这里存放组件的选项(props、listeners、tag等)asyncFactory        // 异步组件的工厂函数) {this.tag = tagthis.data = datathis.children = childrenthis.text = textthis.elm = elmthis.ns = undefined               // 命名空间(如 SVG 或 MathML)this.context = context            // 所属组件上下文this.fnContext = undefined        // 函数式组件上下文this.fnOptions = undefined        // 函数式组件配置this.fnScopeId = undefined        // 函数式组件作用域IDthis.key = data && data.key       // key,用于 diff 时优化复用this.componentOptions = componentOptionsthis.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-oncethis.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.ts

export function createTextVNode(val: string | number) {return new VNode(undefined, undefined, undefined, String(val))
}

3.注释节点

有 text 属性描述具体的注释信息,isComment 属性判断是否是注释节点

源码位置:src/core/vdom/vnode.ts

export const createEmptyVNode = (text: string = '') => {const node = new VNode()node.text = textnode.isComment = truereturn node
}

4.组件节点

有 tag 属性,并且不能是原生 DOM 标签,它还有两个属性:

  • componentOptions:组件的 option 选项

普通 VNode 只需要标签、属性、子节点;组件节点需要保存更多上下文信息。

例如:

  • 它对应哪个组件构造器?
  • 它有哪些 props?
  • 它的 render 函数是什么?
  • 它的父 vnode 是谁?
  • 它的生命周期钩子有哪些?

源码位置:src/types/options.ts

export type ComponentOptions = {// Vue 3 支持 Composition API 的 setup(),在组件创建前调用,用于返回响应式状态或渲染函数setup?: (props: Record<string, any>, ctx: SetupContext) => unknown[key: string]: anycomponentId?: string// 组件的响应式数据系统data: object | Function | voidprops?: | string[] | Record<string, Function | Array<Function> | null | PropOptions>propsData?: objectcomputed?: { [key: string]: | Function | { get?: Function, set?: Function, cache?: boolean } }methods?: { [key: string]: Function }watch?: { [key: string]: Function | string }// 组件的渲染逻辑el?: string | Elementtemplate?: stringrender: (h: () => VNode) => VNoderenderError?: (h: () => VNode, err: Error) => VNodestaticRenderFns?: Array<() => VNode>// 生命周期钩子beforeCreate?: Functioncreated?: FunctionbeforeMount?: Functionmounted?: FunctionbeforeUpdate?: Functionupdated?: Functionactivated?: Functiondeactivated?: FunctionbeforeDestroy?: Functiondestroyed?: FunctionerrorCaptured?: () => boolean | voidserverPrefetch?: FunctionrenderTracked?(e: DebuggerEvent): voidrenderTriggerd?(e: DebuggerEvent): void// 当前组件的“私有资源”注册表,这些资源只在该组件内有效directives?: { [key: string]: object }components?: { [key: string]: Component }transitions?: { [key: string]: object }filters?: { [key: string]: Function }// 依赖注入系统provide?: Record<string | symbol, any> | (() => Record<string | symbol, any>)inject?: | { [key: string]: InjectKey | { from?: InjectKey; default?: any } } | Array<string>model?: { prop?: string, event?: string }parent?: Componentmixins?: Array<object>name?: stringextends?: Component | objectdelimiters?: [string, string]comments?: booleaninheritAttrs?: booleanabstract?: any// 标识是否是组件_isComponent?: true_propKeys?: Array<string>// 关联父级 vnode_parentVnode?: VNode_parentListeners?: object | null_renderChildren?: Array<VNode> | 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.ts
  • src/core/vdom/helpers/resolve-async-component.ts
  • src/core/vdom/patch.ts

这里我们只看简化版的关键部分,在 createComponent 方法中:

return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
export function createAsyncPlaceholder(factory: Function,data: VNodeData | undefined,context: Component,children: Array<VNode> | undefined,tag?: string
): VNode {// 创建一个空的 VNodeconst node = createEmptyVNode()// 保存异步工厂函数node.asyncFactory = factory// 保存上下文node.asyncMeta = { data, context, children, tag }return node
}

而 createEmptyVNode() 会返回一个空的 VNode:

export 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 = truereturn 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.nscloned.isStatic = vnode.isStaticcloned.key = vnode.keycloned.isComment = vnode.isCommentcloned.fnContext = vnode.fnContextcloned.fnOptions = vnode.fnOptionscloned.fnScopeId = vnode.fnScopeIdcloned.asyncMeta = vnode.asyncMetacloned.isCloned = truereturn cloned
}

DOM-Diff

一句话定义:对比 虚拟 DOM 树 的差异,找出需要更新的部分,然后只更新必要的节点,避免整体重渲染。

而这个过程就叫做 patch,这个过程主要做三件事:

1.创建 DOM:判断 oldVnode 是否存在,如果不存在,说明是首次渲染 → 创建新 DOM

当数据变化时,判断新旧 vnode 是否相同节点(sameVnode)—— 判断依据:新旧节点的 key 和 tag 分别都相同

2.更新 DOM:是同一个节点,以新的VNode为准,更新旧的oldVNode

3.删除 DOM:不是同一个节点,直接删除旧节点,创建新节点

源码位置: /src/core/vdom/patch.js

1.创建节点

简化版代码如下:

// 根据给定的虚拟节点创建对应的真实 DOM 节点,并将其挂载到父节点 parentElm 下
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {// 获取 vnode 属性const data = vnode.dataconst children = vnode.childrenconst tag = vnode.tagif (isDef(tag)) { // 创建普通元素节点vnode.elm = vnode.ns // 命名空间? nodeOps.createElementNS(vnode.ns, tag): nodeOps.createElement(tag, vnode) // 实际创建 DOMsetScope(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,后续更新只修改属性、文本或子节点,而不替换 DOMif (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.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (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.childrenif (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.ts

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {let oldStartIdx = 0let newStartIdx = 0let oldEndIdx = oldCh.length - 1let oldStartVnode = oldCh[0]let oldEndVnode = oldCh[oldEndIdx]let newEndIdx = newCh.length - 1let newStartVnode = newCh[0]let newEndVnode = newCh[newEndIdx]let oldKeyToIdx, idxInOld, vnodeToMove, refElmconst canMove = !removeOnlywhile (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] = undefinedcanMove && 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].elmaddVnodes(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 = !removeOnly
  • removeOnly 是一个特殊标志,表示暂时只删除,不移动节点
  • 当 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.四种组合匹配策略(最大化利用旧节点,避免频繁创建新 DOM)

    • oldStartVnode vs newStartVnode
    • 如果相同,表示前端节点未变
    patchVnode(oldStartVnode, newStartVnode)
    oldStartIdx++
    newStartIdx++
    • oldEndVnode 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--
    newStartIdx++

    3.四种组合都不匹配

    新节点的起始节点 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] = undefinedcanMove && 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(key=1), B(key=2), C(key=3)]
    👉 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 是 undefined

    3.如果没找到,创建新节点

    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] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}
    }

    这段代码复用了上面的逻辑

    5.移动新索引指针

    newStartVnode = newCh[++newStartIdx]

    处理完当前新节点后,移动指针,准备进入下一轮比较

    至此,虚拟 DOM 源码篇 over!

    http://www.dtcms.com/a/606090.html

    相关文章:

  • Pig4Cloud微服务分布式ID生成:Snowflake算法深度集成指南
  • 考研资源合集
  • Go语言编译器 | 探讨Go语言编译器的工作原理与优化策略
  • 宁夏一站式网站建设网站做的简单是什么意思
  • 重庆网站建设重庆无锡做企业网站
  • 永嘉县住房和城乡建设局网站哪个程序做下载网站好
  • 刷题leetcode——链表2
  • Telegram 自动打包上传机器人 通过 Telegram 消息触发项目的自动打包和上传。
  • vps网站管理助手下载网页设计及网站建设在线作业
  • Frida 把MessagePack的二进制数据反序列化成JSON,
  • JavaScript 中的 Promise 详解
  • Spring Boot 条件注解:@ConditionalOnProperty 完全解析
  • 做自己的网站多少钱商贸有限公司怎么注销
  • 从近期Kimi-Linear、LongCat-Video和Qwen-Next解读下一代大模型架构升级
  • 记一次 .NET 某理财管理客户端 OOM溢出分析
  • 英文网站如何做seo下载期货行情软件
  • 2022年没封网站直接进入中太建设集团官方网站
  • DeepSeek-OCR实战(06):SpringBoot应用接入
  • 三十、STM32的USART (串口发送+接收)
  • WebSocket-学习调研
  • GPU-Initiated Networking (GIN)及其核心硬件基础 SCI
  • 怎么提高网站加载速度工资卡app下载
  • 【Rust】系统编程语言的核心语法以及常见应用场景浅谈:系统、Web、网络、命令行
  • 网站建设哪公司好上饶市建设局有什么网站
  • 网站黄金比例如何在已建设好的网站做修改
  • 新网站seo优化wordpress前台出现旧版文件
  • HarmonyOS:@State 装饰器——组件内状态
  • 网站维护与建设腾讯企点是什么
  • ListBox控件扩展内容高度自适应,添加图标
  • 如何将短信从安卓手机传输到电脑