虚拟DOM以及手写简单的Vue2虚拟DOM的核心代码!

一、什么是虚拟 DOM?

虚拟 DOM(Virtual DOM)是一种编程概念,它是将真实的 DOM 树映射到一个 JavaScript 对象上,这个对象包含了真实 DOM 树中所有节点的相关信息,包括标签名、属性、文本内容等。

你可以简单理解为 虚拟 DOM 是一个轻量级的 JavaScript 对象, 用于描述真实 DOM 的结构。例如,下面是一个简单的真实的 DOM 元素

<div id="title" class="header">Hello, world!</div>

对应的虚拟 DOM 对象可能是这样的:

{
  type: 'div',
  props: {
    id: 'title',
    className: 'header'
  },
  children: ['Hello, world!']
}

二、为什么要用虚拟 DOM?

你可能会直接回答说,虚拟 DOM 能提高效率,因为真实的 DOM 操作是昂贵的,代价比较大。

React Fiber 的出现就是为了解决虚拟 DOM 的效率问题的。因为 React 中虚拟 DOM 的效率问题要比 Vue 严重的多。

React 中的 ​Fiber 是 React 16 引入的全新协调算法(Reconciliation Algorithm)和架构,旨在解决大型应用中的性能瓶颈,支持增量渲染、任务优先级调度和异步可中断的更新。

Svelte 没有使用虚拟 DOM,为什么效率也很高呢?

Svelte 是现代的国外一个非常流行的前端框架。其核心理念与传统框架(如 React、Vue 等)有显著不同。它通过 ​ 编译时优化 将组件转换为高效的原生 JavaScript 代码,而非依赖运行时(Runtime)解析,从而在性能、体积和开发体验上提供独特优势。

Vue 和 React 的官网中也都没有提到虚拟 DOM 是为了提高效率的。

1. 框架设计

Vue 和 React 的框架设计理念都是基于数据驱动的,数据驱动意味着视图和模型之间是双向绑定,当数据发生变化时,视图会自动更新。

假设:一个组件中可能牵扯到 100 个 DOM 元素,我改动了组件中的某一个数据,那么这 100 个元素中哪个发生了变化呢,它搞不清楚,从逻辑上讲的话是可以搞清楚的,但是这些框架搞清楚是很困难的。困难那就暴力一点,在 Vue 和 React 中,都能找到类似 render 函数的东西,作用是数据一变化,全量生成界面。但是这个数据可能只牵扯到一个元素,但是这些框架不知道,只能全量生成。

// 比如说在 Vue 框架中 一定见到过这样的代码

render: (h) => h(app)

如果在这种情况下,直接操作真实 DOM 的话,由于 DOM 操作是昂贵的,因此会严重影响效率。

因此,既然是全量生成界面,那么就生成虚拟 DOM(妥协的结果)。生成过后就是 100 个对象,然后与之前的 100 个对象进行 diff 算法对比,一对比之后就会发现,到底是那个元素发生改变了,然后再根据变的东西再去操作真实的 DOM。这样就能缓解一下效率低下的问题。

一个面试题: 同样一个功能,原生 JS 的效率高呢,还是使用框架的效率高呢?

肯定是原生的,因为原生 JS 操作 DOM 是最底层的,直接精准的操作 DOM 就可以了,不会向框架那样绕一大圈。

2. 跨平台能力

像我们现在的前端框架,它不仅仅能够在浏览器上运行,它也可以在小程序,移动端或者桌面端运行。可以理解为一个抽象层,它不能去绑定具体的界面。

我们说的真实的 DOM,是指浏览器的 DOM,在浏览器环境中才有这个概念,一旦脱离了这个环境,那就要打上一个问号了所以它不能直接跟真实的 DOM 进行绑定,因此搞了一个所有环境都能够认识的虚拟 DOM,因为虚拟 DOM 就是一个普通的对象,然后根据不同的环境,对这个虚拟 DOM 进行不同的处理,去渲染界面,就可以实现一套代码,多端运行了。

3. 简化开发复杂度

虚拟 DOM 可以让开发者只需关注 ‘数据如何变化’ (如 state 或 data),无需手动操作 DOM (如 document.getElementById()),然后当数据变化的时候,框架会自动更新 DOM,改变视图。从而简化开发复杂度。

上面这部分内容是看 你真的理解虚拟DOM吗?【渡一教育】 这个视频学的, 本人水平有限,看看视频应该可以更好的理解

三、Vue2 虚拟 DOM 工作流程示意图

数据变更
  │
  ▼
触发 Watcher 重新渲染
  │
  ▼
生成新 VNode 树
  │
  ▼
新旧 VNode 树对比(Diff 算法)
  │
  ▼
应用变更到真实 DOM(Patch)

四、简单手写 Vue2 虚拟 DOM 的核心代码

以下是简化版的虚拟 DOM 实现,包含 VNode 创建、Diff、Patch 的核心逻辑。

1. 定义虚拟节点 VNode

// 虚拟节点的构造函数
class VNode {
    
    
  constructor(tag, props, children) {
    
    
    this.tag = tag // 标签名, 如 'div'
    this.props = props // 属性,如 {class: 'container'}
    this.children = children // 子节点,可以是字符串或其他VNode
  }
}

// 创建虚拟 DOM 的函数 (类似 Vue的 h() 函数)

function h(tag, props, children) {
    
    
  return new VNode(tag, props, children)
}

2. 生成真实 DOM

/**
 * 将虚拟 DOM 节点(vnode)转换为真实 DOM 元素
 * @param {Object|string} VNode 虚拟节点(对象类型为元素节点,字符串类型为文本节点)
 * @returns {HTMLElement|Text} 生成的真实 DOM 元素或文本节点
 */
// 将虚拟 DOM 转换为真实 DOM
function createElement(VNode) {
    
    
  // 情况1:处理文本节点(例如 VNode 是 'hello, world!')
  if (typeof VNode === 'string') {
    
    
    // 创建文本节点并返回(如 document.createTextNode('Hello'))
    return document.createTextNode(VNode)
  }

  // 情况2:处理元素节点(例如 VNode 是 {tag: 'div', props: {id: 'title'}, children: ['Hello, world!']})
  // 根据 标签名创建对应的HTML元素(如 document.createElement('div'))
  const el = document.createElement(VNode.tag)
  // 设置元素的 HTML 属性(如 class、id、style 等)
  if (VNode.props) {
    
    
    // 遍历虚拟节点的属性对象(如 { className: 'title', id: 'header' })
    for (const key in VNode.props) {
    
    
      // 使用 setAttribute 设置属性(例如 el.setAttribute('class', 'title'))
      el.setAttribute(key, VNode.props[key])
    }
  }

  // 递归处理子节点(构建完整的 DOM 树结构)
  if (VNode.children) {
    
    
    // 遍历虚拟节点的子节点数组(如 [childVNode1, childVNode2])
    VNode.children.forEach((child) => {
    
    
      // 递归调用 createElement 生成子节点的真实 DOM
      // 并将子节点添加到当前元素中(如 div.appendChild(span))
      el.appendChild(createElement(child))
    })
  }
  // 返回构建完成的真实 DOM 元素
  return el
}

3. Diff 算法 (简化版)

Vue2 使用双端对比算法,这里简化实现:

diff 算法(简化版)

/**
 * 核心 diff 函数,对比新旧虚拟节点,生成 DOM 更新 补丁
 * @param {Object} oldVnode 旧虚拟节点
 * @param {Object} newVnode 新虚拟节点
 * @returns {Function} 接收真实 DOM 元素并执行更新的函数
 */

function diff(oldVnode, newVnode) {
    
    
  // 情况1: 节点类型不同(如 div -> span),直接替换整个节点
  if (oldVnode.tag !== newVnode.tag) {
    
    
    // 返回 替换函数,用新节点替换旧节点
    return (parent) => {
    
    
      // 假设 createElement 函数可以根据 VNode 创建真实 DOM 元素
      const newEl = createElement(newVnode)
      // 替换父节点下的第一个子节点
      parent.replaceChild(newEl, oldVnode.el)
    }
  }

  // 收集所有属性/子节点的更新操作
  const patches = []

  // 情况2;对比属性变化
  const propsPatches = diffProps(oldVnode.props, newVnode.props)
  if (propsPatches) {
    
    
    // 将属性更新操作存入patches中
    patches.push((el) => {
    
    
      for (const key in propsPatches) {
    
    
        // 特殊处理,如果值为 null,则删除该属性
        if (propsPatches[key] === null) el.removeAttribute(key)
        else el.setAttribute(key, propsPatches[key])
      }
    })
  }

  // 情况3:对比子节点变化 (简化版,未处理 key 和 优化算法)
  const childrenPatches = diffchildren(oldVnode.children, newVnode.children)
  if (childrenPatches) {
    
    
    // 将子节点更新操作存入patches中
    patches.push((el) => {
    
    
      // 遍历子节点补丁,按索引逐个应用
      childrenPatches.forEach((childPatch, index) => {
    
    
        // 将子节点的真实DOM传给补丁函数
        childPatch(el.childNodes[index])
      })
    })
  }

  // 返回总补丁函数,按顺序执行所有属性/子节点更新
  return (el) => {
    
    
    patches.forEach((patch) => patch(el))
  }
}

辅助函数 diffProps 函数

/**
 * 对比属性差异,生成属性补丁
 * @param {Object} oldProps 旧属性
 * @param {Object} newProps 新属性
 * @returns {Object} 属性变更对象(key: 属性名, value: 新值/null 表示删除)
 */

function diffProps(oldProps, newProps) {
    
    
  const patches = {
    
    }

  // 合并新旧所有属性名,确保遍历完整, 且不重复
  const allKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)])

  // 遍历所有属性名
  allKeys.forEach((key) => {
    
    
    // 属性值变化:新增/修改/删除
    if (oldProps[key] !== newProps[key]) {
    
    
      // null 表示删除旧属性
      patches[key] = newProps[key] || null
    }
  })

  return Object.keys(patches).length ? patches : null // 无变化就返回 null
}

辅助函数 diffchildren 函数

/**
 * 对比子节点差异(极简版,未处理 key 和 优化算法)
 * @param {Array} oldChildren 旧子节点
 * @param {Array} newChildren 新子节点
 * @returns {Array} 子节点补丁数组
 */

function diffchildren(oldChildren, newChildren) {
    
    
  const patches = []
  // 取较长数组长度
  const len = Math.max(oldChildren.length, newChildren.length)

  for (let i = 0; i < len; i++) {
    
    
    const oldChild = oldChildren[i]
    const newChild = newChildren[i]

    if (!newChild) {
    
    
      // 情况1: 新节点不存在 -> 删除旧节点
      patches.push((parent) => {
    
    
        // 按索引删除 (注意:此处可能有坑!)
        parent.removeChild(parent.childNodes[i])
      })
    } else if (!oldChild) {
    
    
      // 情况2: 旧节点不存在 -> 新增节点
      patches.push((parent) => {
    
    
        parent.appendChild(createElement(newChild))
      })
    } else {
    
    
      // 情况3: 递归对比子节点差异
      patches.push(diff(oldChild, newChild))
    }
  }
  return patches
}

4. Patch 更新到真实 DOM

/**
 * 将虚拟 DOM 差异补丁应用到真实 DOM
 * @param {HTMLElement} parent 旧虚拟节点对应的真实 DOM 父节点
 * @param {Object} oldVnode 旧虚拟节点
 * @param {Object} newVnode 新虚拟节点
 */
function patch(parent, oldVNode, newVNode) {
    
    
  // 步骤1:调用 diff 函数对比新旧虚拟节点,生成差异补丁函数
  // applyPatch 是一个函数,接收真实 DOM 元素并对其应用更新操作
  const applyPatch = diff(oldVNode, newVNode)
  // 步骤2:执行补丁函数,将变更应用到真实 DOM
  // parent.childNodes[0] 是旧虚拟节点对应的真实 DOM(假设为父节点的第一个子节点)
  applyPatch(parent.childNodes[0])
}

5. 使用示例

// 初始化虚拟 DOM
const oldVNode = h('div', {
    
     class: 'container' }, [
  h('span', {
    
     style: {
    
     color: 'red' } }, 'Hello'),
  ' World'
])

// 创建真实 DOM 并插入页面
const root = createElement(oldVNode)
document.body.appendChild(root)

// 假设数据发生变化
// ...

// 更新虚拟 DOM
const newVNode = h('div', {
    
     class: 'container' }, [
  h('span', {
    
     style: {
    
     color: 'blue' } }, 'Hello'),
  ' Vue'
])

// 执行 Diff 和 Patch 算法,更新真实 DOM
patch(root.parentNode, oldVNode, newVNode)

五、Vue 虚拟 DOM 的优化

  1. 双端对比算法: Vue 在 Diff 时采用了头尾指针对比,优先处理相同位置的节点,减少移动次数。
  2. key 的作用: 通过 Key 表示节点身份,复用相同 Key 的节点,避免不必要的重新渲染。
  3. 静态树提升: 将静态不变的节点标记为常量,跳过 Diff 过程。

六、总结

  • **虚拟 DOM 本质:**用 JS 对象描述 DOM,通过 Diff 算法找出最小更新范围
  • **Vue 的优化:**双端对比算法 + Key 复用 + 静态提升
  • **适用场景:**频繁更新的复杂 UI 场景(如大型表单,列表渲染等)

通过手写核心代码,可以更深入理解 Vue 的虚拟 DOM 工作原理!