前端发展至今,为什么 Vue 和 React 都需要虚拟DOM。答:DOM操作很慢,而 JS 却可以很快,然而真的是这样吗?
从头开始:什么是虚拟 DOM
虚拟 DOM 本质上是 js 和 DOM 之间的一个映射,它在心态上表现为一个能够描述 DOM 结构及其属性信息的 js 对象。在 react 中 js 对象表现如下:
就此示例而言,总结起来虚拟 DOM 应该有以下两个特点:
- 虚拟 DOM 是 js 对象
- 虚拟 DOM 是对真实 DOM 的描述
虚拟DOM的前世今生
学习一个新出的知识点,不仅仅要了解这玩意儿是个啥,我想更重要的应该是这玩意儿解决了什么问题。任何新出的技术抛开场景都将是无水之源,无本之木。明白了场景再来谈实现我想也将是水到渠成的事情。
学习虚拟 DOM 也是如此。要对虚拟 DOM 有个很好的学习,我想应该对前端的发展应该有个大概的了解。
请大家关注我的公众号,从零手写简版 Vue2/Vue3、从零手写 Promise 完整源码、从零使用 typescript 手造轮子封装 axios、手把手教你搭建 node+egg 项目等等高质量资源。快快关注 公众号 发送「1024」获取吧!!!
- 原生 js 下的 DOM 操作
long long ago,页面还只是用来做简单展示的时候,运用到的 DOM 操作并没有很频繁。那时候还是 table 布局,页面仔们会用大量的时间去实现页面的静态布局,形如 html+css 的那种展示型页面,最后再补充少量的 js 实现一些类似于隐藏显示的选项卡等交互。
这个阶段,网页仔们还被称为切图仔。不需要大量的DOM操作,没有大量的甚至没有业务逻辑的网页,那时候的我们生活的还是很快乐。这样的网页,原生js足够。
- 解放兼容的 jQuery 时期
随着时代的发展,产品经理越来越不满足于这些简单的、无聊的、呆板的交互,于是他们开始追求丰富的用户体验。映射到实现中伴随的是大量的 DOM 操作。在实际开发中,网页仔们渐渐的发现:原生的 js 提供的 DOM 操作 API 实在是太TM难用了。于是有大牛开发出 jQuery 库解决 js 操作 DOM 不好使的问题,顺便兼容了让人一提及便想跑路的 IE 系列,这实在让人兴奋。jQuery 的一系列链式操作、可组装式的插件扩展让人用起来实在赞不绝口。
jQuery 其 DOM 操作简单、快速的特点,使网页仔们少飙脏话。现在阅读起来仍然舒服。在当年能够一统江湖,实在是当之无愧。
虽然这帮助人们以更舒服的姿势操作 DOM ,但人们渐渐的发现,因为数据的改变频频的操作 DOM 也是个大问题,实在让网页仔们心碎。
- 早期模板解决方案
jQuery 就像是个让人随意使唤的建筑工人,想改造建筑就得告诉他,先去拿工具,再通过什么方式找到那堵墙,然后把他掀了重新用水泥砖垒起来,在加上表面......
这样的操作实在是累人。如果有个管事儿的工头,我只告诉他我想把那堵墙改造成什么样子,后面的事情他去督促这样就方便多了。
而模板引擎正是那个工头的雏形。我们来看这样一个例子。比如现在我有一份班级花名册,数据如下:
const students = [
{ name: '小明', stu_no: '001' },
{ name: '刚子', stu_no: '002' },
{ name: '李华', stu_no: '003' }
]
前端要用列表展示出来,我们这里使用 ejs 的语法:
<% students.forEach(student => { %>
-
<%= student.name %>
<%= student.stu_no %>
<% }) %>
然后将 html 插入到 document 文档中
const targetContainer = document.getElementById('target')
const innerHTML = ejs.render({students: students})
targetContainer.innerHTML = innerHTML
使用模板来写项目还是挺爽的,只需要关注数据层的变化。当数据发生了变化,我们再重新做一次 render 。到此为止,模板引擎已经具有数据渲染页面的雏形。但也有其相应的不足,比如:
- 一直都是重新渲染
- 当数据量巨大时,在性能上并不如人意
- 实际的应用场景基本局限在“字符串的拼接”
- 及其死板,数据发生变化要人为的重新做拼接
- ......
即使这样,但模板引擎开了个好头,在思想上踏上了一个新的台阶——把大量的精力花费在数据上,而并非渲染视图上。所以其核心问题出在性能上,每一次数据改变都得大刀阔斧的重新渲染整个列表视图,这谁顶得住啊?
- 虚拟 DOM 的横空出世
于是,就有一批仁人志士(后面的内容纯属杜撰)提出,既然模板引擎每次都要重新渲染,那我不重新渲染,只重新渲染那些发生变化的 DOM 不就行了,既然操作真实 DOM 性能损耗巨大,那我操作假的不就行了。于是,虚拟 DOM 横空出世。那我们来对比一下模板引擎与虚拟 DOM 的在初始渲染时渲染流程:
- 模板引擎的渲染是:数据 + 模板 --> 直接渲染为真实 DOM --> 挂载至页面
- 虚拟 DOM 的渲染时:数据 + 模板 --> 虚拟DOM --> 真实DOM --> 挂载至页面
在虚拟 DOM 中的模板应该是具有自己的特色类模板,像在 React 中的 jsx 本质上并不是模板,而是一种在视觉上和模板类似的 js 语法糖。
通过上面的对比可以看出,虚拟 DOM 的渲染多出了虚拟 DOM 层。这个缓存层带来的好处就是:当 DOM 存在少量更新时,重新渲染时做一个 diff 操作,定位出需要修改的部分并生成一个补丁集,在此之后做出 patch。
虚拟 DOM 的出现,真的是因为性能吗
故事的发展貌似是因为性能的原因,从模板引擎到虚拟 DOM 的发展的主要矛盾貌似全部指向性能。但 React 在最初引入虚拟 DOM 真的是因为性能吗?我们来做一下模板引擎和虚拟 DOM 在不同场景下的性能对比,一定要在不同场景下做对比,撇开场景谈技术,那都是耍流氓。
在流程上,模板引擎和虚拟 DOM 的前半段搜属于 js 范畴,如模板引擎生成 html 字符串、虚拟 DOM 渲染中的生成虚拟 DOM 部分以及在更新后的两次虚拟 DOM 中做 diff 都是在 js 中进行的,这并不涉及到真实 DOM 的操作,这两次相比之下,字符串的拼接为性能的损耗显然不足为道,而在 diff 中所涉及到的遍历、递归相对字符串的拼接那也算是比较耗时了,因此在 js 这个范围内,模板引擎必然胜出。
但在最后一步真实 DOM 的操作上,两者确实具有对比性,这时确实需要分场景比较了:
如果数据量巨大,有上万的列表渲染在页面上却只需要更改其中的一两条数据,模板引擎就算是把头拼破也不及虚拟 DOM 的 diff patch。但如果数据内容变化非常大,大到差量更新和全量更新极为接近,或者就已经是全量更新了,那上万条的遍历、递归也够客户端的 diff 喝一壶的。
所以在这两种极端的情况下,各有千秋。其实这像极了生活,总是在两者之间做取舍。在不同的场景,二者难分伯仲。但总要分个高低出来,在实际开发中,更加高频的场景不论在 Vue 中还是 React 中,我想大多改变的都是 vm.xxx = xxx
或者 this.setState({xxx: xxx})
,只是其中的极个别属性发生变化,即使 xxx 是个数组也是重新赋值的过程。
说了这么多,所有的出现证据仿佛都在指向性能的提高,即使我们在分析之后做出了取舍也只是在性能的考虑范围上。然而我想说的是,我前面说的其实都是废话,我想说的其实是: 虚拟 DOM 的价值并非单单在性能上。
识得庐山真面目
我所认为的虚拟 DOM ,你可能并不认同。就像看到一个美女,因为审美的差异,每个人可能并不会认同她就是个美女。我给出的可能并不是一个标准答案。我认为虚拟 DOM 解决的一下几个至关重要的问题:
- 页面性能的提升:尽管在我们前面的各种论证对比下,虚拟 DOM 并非最优解,但在大多数场景下,虚拟 DOM 能给你的页面过得去的性能,并且提供给你更爽的开发模式。
- 开发效率的提升:通过对前端发展历史的描述可以看出,每一次对 DOM 操作的革新,背后都是对开发效率的提升。不得不提的是,在 React 或者 Vue 时代,同样的页面,我们的开发效率是提升了不少。就单单一个将输入框的内容输出到页面的 mvvm 模式的开发,引入 Vue 或 React 就可以快速搞定。
- 跨平台的问题:虚拟 DOM 是对渲染内容的一层抽象描述,这就使得视图层和渲染层做了解耦。这层对渲染层的描述可以是 web、native、小程序等多端的,在不同端可能只需要一份代码就可以 work 。毕竟连 Vue3 也将 DOM 操作的内容提在一个独立的 runtime-dom.js 模块中,目的就是在多端操作渲染层的内容从而实现跨平台的可能性。
总结
本次分享先回顾的前端操作 DOM 的发展史,从中了解到虚拟 DOM 的定位和解决的问题,之后对比了虚拟 DOM 与模板引擎上的渲染差异与性能差异。最后做出虚拟 DOM 出现原因的结论。
另外:文中若有表述不妥或是知识点有误之处,欢迎留言批评指正,共同进步!