Vuejs设计与实现 —— 为什么需要虚拟 DOM

前言

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

请思考下面的问题,你是否能够很好的回答出来:

  • 编程范式是什么?和 Vuejs 有什么关系?
  • 为什么需要虚拟 DOM?

如果你有一个明确的答案,那么也许你并不需要继续阅读下面的内容了,但是如果没有一个很明确的方向、思路去回答,那么建议你还是继续阅读!

编程范式

编程范式分为以下三种:

  • 命令式编程(Imperative)
  • 声明式编程(Declarative)
  • 函数式编程(Functional)

但视图层框架(如:Vue.js)通常使用的范式分为 命令式声明式,一个优秀的框架是在选择正确的范式的基础上,甚至是包含这两种范式优点的基础上进行实现的。

命令式框架

命令式框架 的特点就是 关注过程 ,而早年间流行的 jQuery 就是典型的命令式框架。

下面就通过实现一个简单需求来说明什么是命令式框架。

需求描述

  • 获取下面的 button 元素
  • 为其设置文本内容 "show message"
  • 为其添加对应的点击事件,点击按钮后弹出对应提示 "hello world"
html 复制代码
  <button id="btn"></button>

实现需求

  • 原生 JavaScript 实现版本

    js 复制代码
    var btn = document.querySelector('#btn') // 获取对应元素
    btn.innerText = 'show message' // 设置文本内容
    btn.addEventListenner('click', () => { // 绑定点击事件
        alert('hello world')
    })
  • jQuery 实现版本

    js 复制代码
    $('#btn') // 获取对应元素
        .text('show message') // 设置文本内容
        .on('click', () => { // 绑定点击事件
          alert('hello world')
        })

可以看到,代码实现过程需求描述 能够产生相互对应的关系,并且在代码实现中其实就是 "做事的过程",即不论你使用什么语言去实现,都可以按照特定的描述去实现功能。

声明式框架

声明式框架 的特点就是 关注结果 ,例如 Vuejs 就是声明式框架。

同样的,下面是结合 Vuejs 实现上述的功能需求的示例:

html 复制代码
<button id="btn" @click="() => { alert('hello world') }">show message</button>

可以看出,代码实现过程需求描述 并不是完全对应的,但是这段 Vuejs 的代码却能够实现需要的 结果

换句话说,是 Vuejs 将 实现过程 进行了封装,那么也就意味着其内部实现一定是 命令式 的,但是暴露给使用者却是 声明式 的。

Vuejs 为什么选择声明式

声明式代码的性能不优于命令式代码的性能

前面有提到,Vuejs 对外是声明式的,但内部实现还是命令式的,即最终的本质还是回归到了命令式,所以从这一点来看声明式代码的性能确实不优于命令式代码的性能。

例如,现在我们需要将前面按钮的文本内容改变成 "hello vue3"

命令式代码

通过执行以下代码后,修改立即生效:

js 复制代码
btn.textContent = 'hello vue3' // 直接修改

声明式代码

html 复制代码
// 更新前
<button id="btn" @click="() => { alert('hello world') }">show message</button>

// 更新后
<button id="btn" @click="() => { alert('hello world') }">hello vue3</button>

对于 Vuejs 来说,执行上面的代码后,它还需要找到修改前后的差异,并只更新变化的地方,但是对于最终的更新还是通过下面的代码:

js 复制代码
btn.textContent = 'hello vue3' // 直接修改

假设 A 为直接修改的性能消耗,B 为找出差异的性能消耗,则:

  • 命令式代码的更新性能消耗 = A
  • 声明式代码的更新性能消耗 = B + A
    即便是最理想的情况下(出差异的性能消耗为 0,即 B = 0),声明式代码才会和命令式代码的性能消耗相同,但也无法超越了。

选择声明式的原因

通过前面的内容得到的结论:在性能层面命令式代码相比于声明式代码是更好的选择

那既然如此,为什么 Vuejs 还是选择了声明式的设计方案呢?

原因就在于 声明式代码的可维护性更强

  • 命令式代码 在开发过程中,使用者要关注实现目标的整个过程,其中包括:DOM 元素的创建、更新、删除等,即代码体量一大,需要关注的内容就越多
  • 声明式代码 看上去更加直观,并且不需要关注具体的实现过程,因为这些具体实现都被封装在框架内部,即声明式代码的展示内容就是我们需要的 结果

在使用声明式提升可维护性的同时,虽然性能会有一定的损失,但是最好的结果就是:在保持可维护性的同时将性能损失最小化

Vuejs 为什么需要虚拟 DOM

原因

前面提到 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗 ,这就意味着只要可以实现 找出差异的性能消耗 的最小化,就可以在保证可维护性的同时,让 声明式代码 无限趋近于 命令式代码

有了前面的理论基础,你同样可以得到一个结论:虚拟 DOM 的更新技术 理论上 不可能比原生 JavaScript 操作 DOM 更高 。因为大部分情况之下,我们是 很难写出绝对优化的命令式代码,尤其是当应用程序的规模很大的时候,如果你为了写出这样的代码而耗费了巨大的精力,那么最后的投入/产出的比例其实并不高。

小结虚拟 DOM 就是在保证使用声明式代码的同时,还能保证应用程式的性能下限,甚至是尽可能优化已达到命令式代码的消耗性能。

虚拟 DOM 的性能如何

上面提到的 原生 JavaScript 操作 DOM 性能更高 ,其中的 原生 JavaScript 操作 (如:createElement 等)并不包括 innerHTML ,因为它比较特殊。

虚拟 DOM 和 innerHTML 的性能比较

需求描述: 生成下如下的真实 DOM 结构:

html 复制代码
<div>
 <span>hello world</span>
<div>

实现需求:

以下都是伪代码实现

  • innerHTML 版本实现:

    js 复制代码
    const html = `<div>
                    <span>hello world</span>
                  <div>`
    document.body.innerHTML = html
  • 虚拟 DOM 版本实现:

    js 复制代码
    const app = {
        type:'div', 
        children:[
           {
             type: 'span',
             children: 'hello world',
           }
        ]
    }
    document.createElement(xxx)
    document.body.append(xxx)

创建阶段

  • innerHTML 创建页面的性能 = HTML 字符串拼接计算量 + innerHTML 的 DOM 计算量
  • 虚拟 DOM 创建页面的性能 = 创建 JavaScript 对象计算量(虚拟 DOM) + 创建真实 DOM 计算量

JavaScript 层面运算HTML 字符串拼接 和 创建 VNode 对象)上看,两者的差别并不大,因为都属于是 JS 的操作且都没有涉及 DOM。

DOM 层面运算(新建所有的 DOM 元素)上看,两者的差别也不大,因为在创建页面时,都需要创建所有的 DOM 元素。

更新阶段

  • innerHTML 创建页面的性能 = HTML 字符串拼接计算量 + innerHTML 的 DOM 计算量
  • 虚拟 DOM 创建页面的性能 = 创建 JavaScript 对象计算量(虚拟 DOM)+ Diff 计算量 + 必要的 DOM 更新计算量

JavaScript 层面运算HTML 字符串拼接 、 创建虚拟 DOM 对象和 Diff 对比)上看,innerHTML 还是一样的计算量,但是 虚拟DOM 多出来了一个 Diff 计算量。

DOM 层面运算 (新建所有的 DOM 元素)上看,innerHTML 是先创建新的 html 字符串,然后销毁所有旧的 DOM 元素,再全量创建新的元素;而 虚拟DOM 会创建新的 虚拟 DOM 对象,然后通过 Diff 对比 新旧虚拟 DOM,找到变化的元素并更新它。

性能因素

  • 虚拟 DOM 只与数据变化量相关
  • innerHTML 与模板的大小相关

总结

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

由于 Vuejs 在设计层面选择了 声明式 ,但又由于 声明式 的性能并不优于 命令式 ,所以采用了 声明式 + 虚拟DOM 的方式达到更新时性能消耗的最小化,目的是为了在 可维护性强心智负担小 的前提下,同时保证性能消耗可以 趋近于 声明式的性能消耗。

下面这张图可以很好的从 心智负担性能可维护 层面表明为什么需要 虚拟 DOM

相关推荐
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
从前慢丶1 天前
前端交互规范(Web 端)
前端
像我这样帅的人丶你还1 天前
别再让JS耽误你进步了。
css·vue.js
@yanyu6661 天前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing1 天前
Page-agent MCP结构
前端·人工智能
王霸天1 天前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
小领航1 天前
用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件
前端·github
@大迁世界1 天前
2026年React大洗牌:React Hooks 将迎来重大升级
前端·javascript·react.js·前端框架·ecmascript