😀 前言
各位朋友,欢迎来到《React专栏》的首次亮相。我们不做多余铺垫,直接开启对 Virtual DOM 的深入探究,追溯其发展历程。这不仅是了解其实现方式(HOW),更要明确其背后的设计原因(WHY)。请跟随我的节奏,一同开启学习之旅。
🙋 回顾前端历史
以史为镜可以知兴替,我们不妨简单的回顾一下前端经历了哪些时代
切图仔时代 - 低需求下的低要求
从前,有这么一个年代,前后不分离,前端渲染主要是由服务端同学兼任,比如JSP、ASP、PHP。前端同学们最麻烦的工作是切图(会PS几乎是找工作硬性条件,类似现在的三大框架必须会其一),最枯燥的任务是搓DOM,所以又被称为切图仔...
那时候只有一些初级版本的Js框架,包括但不限于prototype,开发者靠着大量手搓DOM和原生JS实现一些简单的交互,高端的操作可能会事扒脚本,扒组件以实现自己的功能。 这个时期的前端开发者很纯粹,那时尚未开始前后端分离的进程,开发者要做的就是将页面合理的呈现出来,以及实现不多的交互需求。
JQ时代 - 日益增长的需求与低下开发效率的矛盾
历史浪潮推着新事物向前。很快,人们对页面不再只求能用,更要炫酷交互,这难住了前端工程师,毕竟当时原生Js API少且难用。于是,JQuery等Js框架诞生。
和JQ同时代的Dojo、prototype等框架,在年轻工程师眼中如同古董。当时,后浏览器战争时代的兼容性问题是折磨工程师的“元凶”(如今在插件辅助下有所缓解)。JQ凭借优异兼容性与性能,成了一代js框架主流。
JQ时代是需求和效率第一次大型冲突的产物,从此,解放生产力成为了前端工程师心中亘古不变的追求。
后JQ时代 - 模板语法的诞生
JQ 和它强大的生态让 JQ 插件一路狂飙。那时前端开发者想实现功能,第一反应就是搜插件、改插件、造轮子。HTML 文件里密密麻麻插满 <script> 标签,页面臃肿得像胖子,还带来全局变量污染和性能问题,比如经典白屏。
因为开发者水平参差不齐,插件库越来越庞大,全局变量污染严重,前端开发者从后端那得到启发,想要模块化。当时 JavaScript 没 import 和 export 这些标准化支持,他们就自己搞了个标准,叫 CommonJs,后来又分裂出 AMD、CMD 等,这给 Node.js 发展埋下了种子。
工程化萌芽:JQ插件生态爆发、
RequireJS横空出世推动前端依赖管理
JQ 也搞不定循环插入 DOM 节点的需求,拼接 DOM 字符串让开发者苦不堪言。于是Mustache、Handlebars等模板语法库闪亮登场,简单说就是 HTML = template(data),template 就是把数据变成 DOM 的规则,而且还能自己定。原理如下:
- 接收数据、解析
- 填入模板
- innerHtml
解决循环插入问题后,前端工程师发现模板语法还有个大优点,只需要做数据到 DOM 节点的映射,不用一个个操作 DOM,也不用管数据咋变,只控制规则就行。兴许是从那一刻开始,前端开发者们寻到了一个崭新而又正确的发展方向—— 数据驱动。
不过模板语法也有毛病,它就是个数据解析器,复杂炫酷的问题搞不定。早期模板语法更新时,会把所有 DOM 节点注销,再生成新节点插入页面,操作 DOM 开销大得离谱。
这里着重说明一下,操作DOM是
跨线程进行的,代价是昂贵的。每次 DOM 操作都涉及:
JS线程 → 序列化任务 → 线程间通信 → GUI线程反序列化 → 执行 即使只修改一个属性,也可能消耗0.1~1ms(随操作复杂度增加)。
开发者在写得爽和用得爽之间犯难了,这时候不知道哪位大佬一拍桌子:操作 DOM 开销大,我操作 JS不就完了!
三大框架时代 - 用JS计算换取更少的DOM操作
操作DOM开销那么大,我操作JS不就行了?
这个疯狂却又合理想法一旦产生就再也无法扑灭,因为这太适合Js了。Js在诞生之初就可以操作DOM的本能,而Js本身的能力又那么强大,模板语法的缺点它完全可以消化和包容,包括但不限于差量更新 、批量更新等一系列梦寐以求的可以提高渲染性能的手段终于可以提上日程了。
于是,新世界的大门就此打开。
至此,我们来明确一点,Virtual DOM,正是作为数据和真实DOM之间的缓冲诞生。
有了虚拟DOM,前端世界迸发了更大的活力,React,Vue 这两种基于虚拟DOM的框架在大浪淘沙之后终于存活了下来,成为了新世界的主流。而Angular凭借另一种方式增量DOM也实现了三足鼎立。
- 增量DOM不在本文重点范围中,后续会在
Angular专栏中详细讲解
🙋 Virtual DOM(虚拟DOM)
Virtual DOM是作为数据与实际DOM操作之间的 中间层(通用层) 诞生的。数据通过某种模板/语法糖/函数将数据转换为虚拟DOM,从而实现在Js层面的控制。这个转换,在Vue里是<template>,在React中是JSX。
中间层(通用层)即代表着跨平台的可行性,而事实上也的确如此。如:小程序、ReactNative
Virtual DOM 使得差量更新这一至关重要的功能得以应用,新的虚拟DOM树会和旧虚拟DOM树以diff算法进行比较,形成一个“补丁”,最后用batch方法将这个补丁打到需要更新的节点上。差量更新既能让开发者感受到舒适的开发体验,又保持了优异的性能。可谓是写的爽和用的爽的双赢典范。
在“差量更新”这一至关重要的功能得以应用之外,还有一个重要的功能是“批量更新”,即用户在短时间内dom进行高频操作的时候会取最后一次的操作结果,以此避免大量的大成本的性能消耗。这里就涉及batch方法的内容了,他会缓冲每次生成的补丁集,然后把它们放入一个队列中,算出一个渲染结果后再将结果交给渲染函数,以此实现批量更新。
虚拟DOM的优点包括:
- 促进函数式UI编程:抽象组件,简化代码维护。
- 跨平台能力:虚拟DOM本质上是JavaScript对象,可用于其他终端的(手机端、小程序)抽象层。
- 数据绑定减少DOM操作:通过合并多个DOM操作为一次操作,减少浏览器重排。
- 通过DOM Diff减少不必要的操作:减少页面重排和重绘。
- 缓存DOM和保存节点状态:优化DOM更新。
有那么多优势的情况下,人们下意识的认为虚拟DOM的性能优于模板语法或者原生JS操作,可这真的对吗?
先声明:没有任何框架可以比
纯手动的优化 DOM操作更快,所以本文不再说明原生JS与虚拟DOM性能孰强孰弱。
我们不妨回头看看三者的流程区别:
- 原生Js操作:数据 → Js操作 → 真实dom
- 模板语法的流程是: 数据 → 模板 → Js操作 → 真实dom,
- 虚拟DOM的流程是:数据 → 模版/算法/语法糖 → 虚拟dom → diff → js操作 → 真实dom。
你突然发现,虚拟DOM的流程明明长了那么多,为什么大家会说它的性能更好?
性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、少/大量数据的全量/差量更新这些不同的场合
- 初始渲染: 模版语法 > 虚拟dom
- 少量数据的全量更新:模版语法 = 虚拟dom(相差无几)
- 少量数据的差量更新:虚拟dom >= 模版语法 (数据量少,相差无几)
- 大量数据的全量更新:模版语法 >> 虚拟dom
- 大量数据的差量更新:虚拟dom >>> 模版语法 (差量更新这也是实际场景最频繁的情况,托了Ajax技术的福)
💡 真相:只看性能不看可维护性是耍流氓,反之只看可维护性不看性能也是耍流氓,虚拟DOM从来不是为「更快」而生,而是为「在复杂场景下保持可接受的性能下限」+「解放开发者生产力」而存在。正如React团队所说:
“Virtual DOM is a programmer productivity tool, not a silver bullet for performance.”
至此,我们重新总结一下虚拟DOM技术的真正(大白话)优势:
- 它让用户用的很爽,因为它解决了页面性能优化的关键性痛点。
- 他让开发者开发的很爽(促进函数式UI编程),爽意味着迭代的效率,可维护性高,意味着社区维护的积极性。
- 他能解决多端复用(跨平台)统一性的问题,这可以减少成本,而减少成本,意味着占有市场。
👳 Virtual DOM的现状及其争议
现状
目前,Virtual DOM 仍然是主流框架中的主导技术。React持续在迭代中优化虚拟DOM的diff效率,而Vue3则开始吸收Svelte预编译概念减少运行时diff成本。尽管虚拟DOM在主流框架中仍占主导地位,但像 Svelte 和 Solidjs 这样的非虚拟DOM框架开始将它们的新模式引入公众视野。
State of js 只有 2024 年的数据(仅供参考),但从数据中可以看出,React、Vue、Angular 在使用量上仍然占据主导地位,而Svelte、Solidjs也在逐渐受到欢迎。
争议
虽然Virtual DOM曾是前端开发的一大创新,但随着技术的发展,一些框架开始质疑并摒弃虚拟DOM。这背后的原因主要包括:
- 首次渲染性能:虚拟DOM在首次渲染大量DOM时,由于额外的计算开销,可能会比直接使用innerHTML插入慢。
- 内存占用:维护一份虚拟DOM树的内存副本(空间换时间)。
- 频繁更新的开销:在频繁更新时,虚拟DOM需要更多时间进行计算工作。
- 大型项目的性能成本:即使现代框架进行了优化,比较和计算虚拟DOM的成本依然存在,特别是在构建虚拟DOM树时。
不同框架的应对策略
- Uber:例如Uber,通过广泛且手动使用shouldComponentUpdate来最小化渲染调用。
- React:React 探索 WASM Diff 加速。
- Vue3:Vue3引入了一种新的编译优化策略,称为
蒸汽模式”(Vapor Mode),这是对Svelte预编译概念的响应。蒸汽模式利用编译时信息优化虚拟DOM。。 - Angular : Angular 没有使用虚拟 DOM,而是通过编译器将模板转化为轻量级增量 DOM 指令,在数据变更时重新执行指令并直接增量更新真实 DOM。这种设计避免了虚拟 DOM 的内存开销和 Diff 计算成本,是 Angular 性能优化的核心策略之一。
- Svelte:Svelte的作者Rich Harris提出了“虚拟DOM纯属开销”的观点,强调在某些情况或频繁更新下,虚拟DOM数据驱动模型带来的不必要开销。
⚖️ 展望未来
回首前端发展历程,我们已然步入全新阶段,如无虚拟 DOM、编译优化以及 WASM优化 阶段。前端技术栈的迭代速度之快,着实令人惊叹。遥想当年,前端从业者多是专注于静态页面切图的工作者,而如今,大前端、大全栈的概念深入人心。短短十几年间,前端领域如同一座宏伟的大厦拔地而起,在时代浪潮中熠熠生辉。
是什么推动了前端职能与功能的高速发展?
我想,认真了解过前端发展史的同学,心中定会有所思索。我们不妨换个更具实际意义的问题:什么样的技术或项目,能成为备受青睐的存在,甚至引领行业环境的发展方向? 在我看来,所有堪称成功的技术或项目,都有一个共同特征——实现性能与产能的双赢。以jQuery为例,它不仅极大地解放了开发者的生产力,而且在性能方面远胜一般的原生API,堪称一个时代的标志性技术。再看虚拟DOM,它精准地解决了复杂交互场景下性能不佳的难题,让开发者无需再纠结于繁琐的DOM操作,只需专注于数据及其变化。此外,它还为多端统一的实现开辟了新的道路。
未来趋势
- 编译时优化主导:Svelte/SolidJS 的“无虚拟 DOM”模型正成为高性能框架新标准,Vue 亦推出 Vapor 模式跟进。
- WebAssembly 潜力:Yew(Rust)等 WASM 框架在内存控制上展现优势,可能颠覆未来性能格局。
- 框架融合:Vue 3 同时支持虚拟 DOM 与编译优化,React 探索 WASM Diff 加速,传统框架正吸收新兴思路。