目录

虚拟dom 源码分析一下

一、虚拟DOM:连接理想与现实的桥梁

1.1 虚拟DOM的本质

虚拟DOM(Virtual DOM)是现代前端框架(React/Vue)的核心架构设计,它是一个轻量级的JavaScript对象,通过树状结构精确描述真实DOM的层次关系与属性特征。就像建筑师的蓝图,它并不直接参与施工,却能指导如何高效建造真实DOM这座"大楼"。

技术特性:

  • 内存映射:真实DOM在内存中的镜像对象
  • 结构描述:包含节点类型、属性、子节点等元数据
  • 跨平台基础:为服务端渲染、小程序等提供统一抽象层

1.2 虚拟DOM的必要性

在没有虚拟DOM的时代,直接操作DOM会导致严重的性能问题。假设每次状态变更都要重建DOM树:

js 复制代码
// 传统DOM操作示例
const container = document.getElementById('app')
container.innerHTML = `<div class="new-layout">${renderContent()}</div>` // 全量替换

这种简单粗暴的方式会导致:

  1. 频繁触发重排(Reflow)与重绘(Repaint)
  2. 跨平台渲染能力缺失
  3. 状态与视图耦合度过高

而虚拟DOM通过差异比对机制,将多次DOM操作合并为单次批量更新,性能提升可达3-5倍(React官方数据)。

二、框架中的虚拟DOM实现

2.1 Vue3的虚拟DOM

通过h()函数创建虚拟节点,支持组合式API:

js 复制代码
import { h } from 'vue'

const vnode = h('div', 
  { id: 'app', class: 'container' },
  [
    h('span', { style: { color: 'red' } }, 'Hello'),
    h('button', { onClick: handleClick }, 'Submit')
  ]
)

编译原理:

复制代码
Template → AST → 渲染函数 → 虚拟DOM树 → 真实DOM

2.2 React的虚拟DOM

使用React.createElement构建元素:

jsx 复制代码
function ListItem({ text }) {
  return React.createElement(
    'li',
    { className: 'item' },
    React.createElement('span', null, text)
  )
}

Babel会将JSX转换为React.createElement调用,生成虚拟DOM树。

三、Diff算法:虚拟DOM的灵魂

3.1 算法演进

传统树差异算法(如Myers)的时间复杂度为O(n³),而现代框架通过两个核心策略优化到O(n):

  1. 层级比较:只对比同级节点,不跨层级
  2. 类型预设:节点类型不同则直接替换整棵子树

3.2 核心策略解析

当比较新旧虚拟DOM树时:

比较维度 处理方式 时间复杂度
根节点类型不同 整树替换 O(1)
属性差异 仅更新变化的属性 O(m)
子节点列表 双端对比+Key优化(Vue) O(n)
文本节点 直接更新文本内容 O(1)

3.3 子节点Diff过程详解

以Vue的Keyed Diff为例:

js 复制代码
function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let newEndIdx = newCh.length - 1
  
  // 双端指针法进行四阶段对比
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 1. 旧首 vs 新首
    // 2. 旧尾 vs 新尾
    // 3. 旧首 vs 新尾(交叉比对)
    // 4. 旧尾 vs 新首(交叉比对)
    // ... 具体比对逻辑
  }
  
  // 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 新增节点处理
  } else {
    // 移除旧节点
  }
}

感兴趣可以看看完整diff 比较:

js 复制代码
const oldChildren = n1.children  // 旧节点的子节点
const newChildren = n2.children

let lastIndex = 0  // 上一个节点的索引
// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]
    let j = 0
    let find = false
    // 遍历旧的 children
    for (j; j < oldChildren.length; j++) {
      const oldVNode = oldChildren[j]
      // 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新
      if (newVNode.key === oldVNode.key) {
        find = true
        patch(oldVNode, newVNode, container)
        // 移动
        if (j < lastIndex) { // 旧vnode 数组下标在上一个index之前
            const prevVNode = newVNode[i-1]
            if(prevVNode) {
              // 将当前节点插入到前一个节点的后面
                const anchor = prevVNode.el.nextSibling
                insert(newVNode.el, container, anchor)
            } else {
              // 如果是第一个节点,插入到最前面
                insert(newVNode.el, container,container.firstChild)
            }
        } else {
                // 更新
                lastIndex = j 
        }
        break //跳出循环,处理下一个节点
      }
   }
   // 没有找到就是新增了
   if (!find) {
      const prevVNode = newChildren[i - 1]
      let anchor = null
      if (prevVNode) {
        // 获取锚点
        anchor = prevVNode.el.nextSibling
      } else {
        // 根节点,在第一个节点之前
        anchor = container.firstChild
      }
      patch(null, newVNode, container, anchor)
   }
}

3.4 Key的重要性

通过唯一Key实现最小化移动:

jsx 复制代码
// 没有Key时,所有<li>都会被重新创建
<ul>
  <li>Apple</li>
  <li>Banana</li>
</ul>

// 使用Key后,仅移动DOM
<ul>
  <li key="banana">Banana</li>
  <li key="apple">Apple</li>
</ul>

Key的最佳实践:

  • 使用唯一业务ID(如商品ID)
  • 避免数组索引作为Key
  • 同一层级保持Key唯一性

四、那面试遇到这么回答

4.1 标准回答结构

当被问到虚拟DOM和Diff算法时,建议采用以下结构:

  1. 概念阐述: "虚拟DOM是内存中的轻量级DOM表示,通过Diff算法计算最小变更,批量更新真实DOM"

  2. 核心优势

    • 减少直接DOM操作次数
    • 跨平台能力
    • 自动批处理更新
  3. Diff算法要点

    • 同层比较原则
    • 双端对比策略(Vue)
    • Key的作用与选择
    • 时间复杂度优化原理

4.2 常见追问与应答

Q:为什么不能精确对比所有节点?

A:完全对比的O(n³)复杂度不可接受,通过层级和类型限制将问题简化为O(n)

Q:v-for 要设置key吗?用index作为Key有什么问题?

A:当列表顺序变化时会导致:

  1. 组件状态错乱
  2. 不必要的DOM重建
  3. 性能下降

Q:Vue和React的Diff算法差异?

A:

  • React使用单指针移动策略
  • Vue采用双端对比+最长递增子序列优化
  • Vue3静态标记提升Diff效率

举个实际例子

假设旧子节点顺序:A(key:a), B(key:b), C(key:c)

新子节点顺序:C(key:c), B(key:b), A(key:a)

执行过程:

  • 处理C节点时,旧索引j=2,lastIndex初始为0 → 更新lastIndex=2
  • 处理B节点时,旧索引j=1 < lastIndex(2) → 需要移动到C后面
  • 处理A节点时,旧索引j=0 < lastIndex(2) → 需要移动到B后面

通过将理论知识与实际场景结合,展现系统化的理解深度。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
returnShitBoy2 小时前
前端面试:如何实现预览 PDF 文件?
前端·pdf
烂蜻蜓4 小时前
HTML 表格的详细介绍与应用
开发语言·前端·css·html·html5
圣京都5 小时前
react和vue 基础使用对比
javascript·vue.js·react.js
returnShitBoy5 小时前
前端面试:axios 是否可以取消请求?
前端
u0103754565 小时前
fiddler+雷电模拟器(安卓9)+https配置
前端·测试工具·fiddler
海上彼尚5 小时前
Vue3中全局使用Sass变量方法
前端·css·sass
ᥬ 小月亮6 小时前
TypeScript基础
前端·javascript·typescript
鱼樱前端6 小时前
Vue3+TS 视频播放器组件封装(Video.js + Hls.js 最佳组合)
前端·javascript·vue.js
烛阴6 小时前
JavaScript 函数进阶之:Rest 参数与 Spread 语法(二)
前端·javascript
GISer_Jing6 小时前
ES6回顾:闭包->(优点:实现工厂函数、记忆化和异步实现)、(应用场景:Promise的then与catch的回调、async/await、柯里化函数)
前端·ecmascript·es6