前言
时至今日,前端框架已经步入了稳定的成熟期,Vue React二分天下,虽然网上诸多争论,但客观讲现在确实没办法明确的分出好坏,因为他们有着各自的用户群体,代表着他们各自的设计理念大相径庭,哪种设计思想能掌握未来呢,只能留待时间的考证。
但是呢,好的设计,即使设计理念大相径庭,但实现细节也会殊途同归,时间来到了2023年,一个好的框架应该是什么结构,已经有一个很明确的范式了,比如
- 生命周期
- 组件化
- 响应式
- 虚拟DOM
- 单向数据流
- state 组件&全局状态
- 渲染函数
- ........
虚拟DOM
本文将着重讲述下虚拟DOM,这已经是一个老生常谈的话题,大量的文章讲述虚拟DOM的细节,因为本文旨在对比分析 Vue React 两门框架,所以按理应该讲述下两门框架中对于虚拟DOM实现上的差别。
但其实这是一个很无聊的过程,双方的实现细节隐藏在各自源码中,阅读源码是一件费时费心费力的事情,对于前端开发者来讲,框架是如何做的 Dom diff ,用的树还是用的链表、用的什么算法,都没有感知,即使你真的从头到尾认真看一遍,让你对框架的理解也不会有太大提升,而且你现在看了一遍,下个迭代版本他们逻辑又换了。个人感觉这部分源码复杂多变,对于理解框架本身的意义不是很大。
你只需要理解无论是 Vue 还是 React,对于 虚拟DOM 所做的事就是:
基于自身的设计带来的框架能力 在虚拟DOM中做相应的注入 ,再通过策略+算法 ,最终在性能 or 速度中寻找各自的最佳平衡点。
-
框架能力:
Vue 采用 template 固定模板提供开发 HTML 的能力,通过自定的语法(如指令),在编译阶段即可拿到一份【充满信息标注】的模板字符串,在生成渲染函数时,可以通过这些信息做更加详尽、更有针对性的渲染优化。而 React 直接采用开发函数组件的方式直接输出渲染函数,在虚拟DOM渲染时,自然可利用的信息更少,这就是框架自身的设计带来的不同框架能力
-
策略 + 算法:
具体到虚拟DOM要真正渲染时,所谓的 DOM diff 过程是复杂的,具体可分为两个层面,第一是核心的 diff 算法,而现在实现 diff 通常是多个算法组合使用,将时间复杂度降为O(n);其次是更新策略,比如对于当前已经变更的子节点是直接替换,还是再深入的比对更深层的子节点,框架开发者会有自己的考量
-
性能 or 速度:
无论框架开发者制定什么策略,对于渲染过程中的 DOM diff 需要考量的无非是性能 或速度,不考虑时间复杂度,diff 算法能算出极致渲染性能的渲染函数,但同时渲染前的计算时间会被大大拉长;同样如果要将 diff 复杂度降为 O(n) 甚至更短,那浏览器就要付出更多的渲染成本。
如何给最快的给用户呈现出页面,框架开发者需要再诸多影响变量中,寻找一条他认为性价比最高的一条路。
Vue 模板的优势
很多人都说 Vue 模板看起来太死板了,设计思想偏传统,远不如 React 大胆、颠覆、优雅,我相信很多开发者是被 React 优雅的函数式编程、简洁颠覆的设计思想、极度灵活的开发方式所吸引,但这里就需要着重提一下 Vue 模板所带来的好处了。
静态分析
上文已经简单说过了,Vue 通过自定的模板语法,固定的写法,在模板中留下大量的信息标注,经过静态分析,这些有价值的标注将附加在模板字符串,虚拟DOM中,在整个编译时,运行时带来很多可优化的可能。
静态提升
html
<div>
<div>foo</div> <!-- 需提升 -->
<div>bar</div> <!-- 需提升 -->
<div>{{ dynamic }}</div>
</div>
以上是官网实例,表明在没有变量引用的纯静态dom "foo" "bar" 会在编译阶段直接提升渲染函数之外,作为一份不可变的"常量",在 DOM diff 时会问完全跳过对比,当遇到连续多个静态节点时,还会被压缩成一个静态 Vnode 直接通过 innerHTML
渲染到页面,并缓存下来,如果有其他地方复用一样的静态节点,使用原声的cloneNode
方法克隆新 DOM 节点,非常高效。
类型标记
html
<!-- 仅含 class 绑定 -->
<div :class="{ active }"></div>
<!-- 仅含 id 和 value 绑定 -->
<input :id="id" :value="value">
<!-- 仅含文本子节点 -->
<div>{{ dynamic }}</div>
对于动态绑定的元素,在编译时推断出相应的类型,在生成渲染函数时,标注上该类型,运行时根据类型做不同的渲染逻辑
VDOM 树变数组
将传统树形结构,打散成一个个区块,每个区块是一个稳定的 vnode 组合,在区块内部将所有后代节点标记出那些是静态节点,那些需要动态追踪,最终将一个区块输出成一个数组。如下代码
html
// 区块1
<div v-if>
<div>aaaaa</div> <!-- 不会追踪 -->
<div>{{ bar }}</div> <!-- 要追踪 -->
<div :id="id"></div> <!-- 要追踪 -->
</div>
// 区块2
<div v-else>
</div>
当这个区块内有内容需要重新渲染时,只需要遍历该数组,所有静态部分会被高效的略过。
虚拟DOM 常见误区
误区:虚拟DOM渲染比真实DOM渲染更快
认为虚拟DOM会更快是最常见的误区之一,但这是不一定的,或者说这两者本身就没有必然的联系。
所谓的虚拟DOM,就是在真实DOM前做用js对象一层映射,当有变动改变页面渲染时,先改变虚拟DOM,再通过虚拟DOM找到需要渲染的最小DOM元素,所以如果只从表面上看,那么直接操作真实DOM变更应该是更快的。
- 真实DOM:内容改变------>真实DOM更新
- 虚拟DOM:内容改变------>虚拟DOM------>DOM diff------>真实DOM更新
虽然 js 对象的操作和计算是很便宜的,但虚拟DOM会无故增加维护虚拟DOM对象和 diff 过程,肯定是增加了渲染前的计算时间的,没有什么是比直接操作DOM去改变内容最快的了。
有些人的想法是:虽然手动操作DOM很快,但多数情况开发者对于数据改变,触发页面重新渲染,都只是 innerHTML
一把梭,造成了大量的渲染浪费,而渲染是远比计算昂贵的操作。
没错,传统开发方式的确会有渲染浪费的问题,但很难说接入虚拟DOM后一定会是:增加的 js 计算时间 < 节省的渲染时间,所以虚拟DOM渲染比真实DOM渲染更快是一个错误的结论。
正确的结论应该是:
- 虚拟DOM并不一定会让渲染更快,只能说在大量级的渲染时,可能会让渲染加快,而普通量级时更多的反而会让渲染变慢
- 更重要的是接入虚拟DOM后,渲染过程会变的更加稳定,传统的操作DOM方式太过不可控,随着复杂度提升浪费也会无限提升,最终造成严重性能问题,而虚拟DOM中,js的计算是很便宜的事,只要在一个正常的时间复杂度下,计算增加的时间会始终处于一个可控的范围内,大大提高系统的稳定性。
虚拟DOM真正意义
虽然虚拟DOM并不一定能带来性能的提升,但是它对前端框架的发展有着至关重要的意义:
- 完美的掩盖了DOM的底层操作,始终以最小代价更新视图
- 框架的开发者可以面对 js 对象开发,极大的降低了复杂性
最重要的是,虚拟DOM可以让前端代码真正脱离端,真正实现多平台的可移植性,只需要我们再开发一份虚拟DOM和新平台UI渲染的映射关系,这简直为前端未来的发展奠定了最坚实的路基。