【JS】922- Virtual DOM 认知中的一些误区
共 8505字,需浏览 18分钟
·
2021-04-10 10:14
在当下最流行的两个前端框架都存在 Virtual DOM 的前提下, 渐渐比较多的听到类似“使用 Virtual DOM 有什么优势?” 的面试题,但一直没有太在意。直到今天在写一个文档时,突让想到要把“为什么需要 Virtual DOM ?”也写进去,待我流畅的写好答案,略一思索——漏洞百出!也不知道是接纳了哪方的知识,让我一直有能轻松回答这个问题的错觉, 其实对于这个问题我是缺乏思考的。
你或许还不清楚我想说什么,但请耐下心来,先来看看网络上关于此问题的一些见解:
虚拟DOM同样也是操作DOM,为啥说它快?[1]
虚拟DOM不会进行排版与重绘操作; 虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分(注意!),最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗; 真实DOM频繁排版与重绘的效率是相当低的; 虚拟DOM有效降低大面积(真实DOM节点)的重绘与排版,因为最终与真实DOM比较差异,可以只渲染局部(同2)。
Virtual Dom 的优势在哪里?[2]
具备跨平台的优势,由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。 提升渲染性能 Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
Virtual Dom的优势[3]
不会立即进行排版与重绘; VDOM频繁修改,一次性比较并修改真实DOM中需要修改的部分,最后在真实DOM中进行重排 重绘,减少过多DOM节点重排重绘的性能消耗; VDOM有效降低大面积真实DOM的重绘与重排,与真实DOM比较差异,进行局部渲染;
上面是从 Google 搜索到的三个平台中的分析摘选,总结下来大概四点:
操作 DOM 太慢,操作 Virtual DOM 对象快 使用 Virtual DOM 可以避免频繁操作 DOM ,能有效减少回流和重绘次数(如果有的话) 有 diff 算法,可以减少没必要的 DOM 操作 跨平台优势,只要有 JS 引擎就能运行在任何地方(Weex/SSR)
它们的理解正确吗?本文测试数据都基于 Chrome 86.0.4240.198
Virtual DOM 快?
有人认为操作 Virtual DOM 速度很快?Virtual DOM 是一个用来描述 DOM(注意,并不一定一一对应)的 Javascript 对象,Javascript 操作 Javascript 对象自然是快的。但 Virtual DOM 仍然需要调用 DOM API 去生成真实的 DOM ,而你其实是可以直接调用它们的,所有就有一个很有意思结论,正数再小也不可能比零还小——Virtual DOM 很快,但这并不是它的优势,因你本可以选择不使用 Virtual DOM 。除了速度不是优势,Virtual DOM 还有个最大的问题——额外的内存占用,以 Vue 的 Virtual DOM 对象为例,100W 个空的 Virtual DOM(Vue) 会占用 110M 内存。内存占用截图:
测试代码:
let creatVNode = function(type) {
return {
__v_isVNode: true,
SKIP: true,
type,
props: null,
key: null,
ref: null,
scopeId: 0,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag: 0,
patchFlag: 0,
dynamicProps: null,
dynamicChildren: null,
appContext: null
}
}
let counts = 1000000
let list = []
let start = performance.now()
// 创建 VNode(Vue)
// 10000: 1120k
for (let i = 0; i < counts; i++) {
list.push(creatVNode('div'))
}
// 创建 DOM
// 10000: 320k
// for (let i = 0; i < counts; i++) {
// list.push(document.createElement('div'))
// }
console.log(performance.now() - start)
_令人意外的是 100W 个空的 DOM 对象只占用 45M 内存,不清楚在 DOM 属性明显更多的情况下 Chrome 是如何优化的,或则是 Dev Tools 存在问题,希望有人能替我解惑。_你看 Virtual DOM 不但执行快没有用,还增加了大量的内存消耗,所以我们说它快自然是有问题的,因为没有 Virtual DOM 时更快。
Virtual DOM 减少回流和重绘?
也有人认为 Virtual DOM 能减少页面的 relayout 和 repaint ?通常有两个原因来支撑这个观点:
DOM 操作会先改变 Virtual DOM ,所以一些无效该变(比如把文本 A 修改为 B ,然后再修改为 A)就不会调用 DOM API ,也就不会导致浏览做无效的回流和重绘。 DOM 操作会先改变 Virtual DOM ,最终由 Virtual DOM 调用 patch
方法批量操作 DOM ,批量操作就不会导致过程中出现无意义的回流和重绘。
无效回流与重绘
第一个观点看着很有道理,但有个问题很难解释:浏览器的 UI 线程在什么时候去执行回流和重绘?要知道现代浏览器在设计上为了避免高复杂度,Javascript 线程和 UI 线程是互斥的,即如果浏览器要在 Javascript 执行期间触发 relayout/repaint 则必须先挂起 Javascript 线程,这是个连我都觉得蠢的设计,显然不会出现在各大浏览器身上。事实上也确实如此,无论你在一次事件循环中调用多少次的 DOM API ,浏览器也只会触发一次回流与重绘(如果需要),并且如果多次调用并没有修改 DOM 状态,那么回流与重绘一次都不会发生。Timeline 截图(没有回流和重绘发生):
测试代码:
<body>
<div class="app"></div>
<script> let counts = 1000
let $app = document.querySelector('.app')
setTimeout(() => {
for (let i = 0; i < counts; i++) {
$app.innerHTML = 'aaaa'
$app.style = 'margin-top: 100px'
$app.innerHTML = ''
$app.style = ''
}
}, 1000) </script>
</body>
无意义的回流与重绘
第二个观点是比较有意思的,虽然看了上面的分析,你应该也知道它是错的,批量操作并不能减少回流与重绘,因为它们本身就只会触发一次。但我还是要列出来证明一下,因为这是我们当下众多前端的一个固有思维,我在准备写这篇文章前问了一下众神交流群的朋友们,他们几乎都掉进了这个认知陷阱中,认为批量操作会减少回流与重绘。批量操作并不能减少回流与重绘,原因也和上文一致,Javascript 是单线程且与 UI 线程互斥,所以直接放测试数据:Javascript 执行耗时(数据取3次平均值):
Layout 耗时(数据取3次平均值):
测试代码:
<body>
<div class="app"></div>
<script> let counts = 1000
let $app = document.querySelector('.app')
let start = performance.now()
// 单独操作
// for (let i = 0; i < counts; i++) {
// let node = document.createTextNode(`${i}, `)
// $app.append(node)
// }
// 批量操作
let $tempContainer = document.createElement('div')
for (let i = 0; i < counts; i++) {
let node = document.createTextNode('node,')
$tempContainer.append(node)
}
$app.append($tempContainer)
console.log(performance.now() - start) </script>
</body>
可以看到的是,批量处理和单次处理再 Layout 期间耗时是几乎一致的,虽然在 script 执行阶段还是存在一定的性能优势(大概 30%),但大抵上只要你用好 DOM 操作,批量或不批量带来的性能影响是很小的( 10W 次调用多损耗 27ms )。题外话:这里提出一个问题,为什么在 script 执行阶段还是存在一定的性能差距?答案会在晚些时候公布(等我看完这部分逻辑)
Virtual DOM 有 diff 算法?
严格来说 diff 算法和 Virtual DOM 是两个独立的东西,二者互相之间也没有充分必要的关联,比如 svelte[4] 没有 Virtual DOM 也有其自己的 diff 算法。但由于前端框架存在 Virtual DOM 就总有 diff 算法,并且使用了 Virtual DOM 对 diff 算法也有两个助力:
得益于 Virtual DOM 的抽象能力,diff 算法更容易被实现和理解 得益于 Virtual DOM Tree 总是在内存中, diff 算法功能可以更强大(比如组件移动,没有完整的 Tree 结构是不可能实现的)
diff 算法能减少 DOM API 调用,显然是存在设计和性能优势的,而由于 Virtual DOM 的存在,diff 算法可以更方便且更强大,所以我认同这是 Virtual DOM 的优势,但不能用“Virtual DOM 有 diff 算法”这样的表述。
Virtual DOM 有跨平台优势?
上文提到的 svelte 没有 Virtual DOM ,但一样可以实现服务端渲染,这说明跨平台并不依赖于 Virtual DOM 。其实只要 Javascript 框架有实现平台 API 分发机制,就能在不同平台执行不同的渲染方法,即拥有跨平台能力。这个能力的根本,是 Javascript 代码能低代价地在各个平台运行(得利于浏览器在各个平台的普及和 NodeJS),也就是常说的 Javascript 的优势之一是跨平台。所以把跨平台当做 Virtual DOM 的优势,其实是不正确的,但我们或许应该去思考下他们为什么会这么认为。我的想法,可能是这两个原因:
Virtual DOM 的优势,可以在不接触真实 DOM 的情况下操作 DOM,并且性能更好在 Virutal DOM 上的改动,最终还是会调用平台 API 去操作真实的 DOM ,所以没有 Virtual DOM 只是相当于少了一个中间抽象层,并不影响跨平台能力有无。但还是需要明白,就目前的分析来看,这个抽象层对跨平台能力还是提供了相当大的方便(或者说助力)的。 Virtual DOM 在 Vue 中很重要,Vue 本身就是一个围绕 Virtual DOM 创建起来的框架,脱离了 Virtual DOM 其设计思想必然会和当下迥乎不同
总结
本文从互联网上摘选了部分对开发者对 Virtual DOM 优点的认知,也从现实生活中了解到一些误解,总结为 Virtual DOM 的四个“优势”,并分别对这四个“优势”进行了单独分析或举证。最终我们识别了几个关于 Virtual DOM 优势误区:
操作 DOM 太慢,操作 Virtual DOM 对象快 ❌ (Virtual DOM 很快,但这并不是它的优势,因你本可以选择不使用 Virtual DOM ); 使用 Virtual DOM 可以避免频繁操作 DOM ,能有效减少回流和重绘次数 ❌ (无论你在一次事件循环中调用多少次的 DOM API ,浏览器也只会触发一次回流与重绘(如果需要),并且如果多次调用并没有修改 DOM 状态,那么回流与重绘一次都不会发生。批量操作也不能减少回流与重绘); Virtual DOM 有跨平台优势 ❌ (跨平台是 Javascript 的优势,与 Virtual DOM 无关)。
我们也提到了 Virtual DOM 真正的优点是其抽象能力和常驻内存的特性,让框架能更容易实现更强大的 diff 算法,缺点是增加了框架复杂度,也占用了更多的内存。
参考资料
[3]Virtual Dom的优势 (https://juejin.cn/post/6844904179715014669)
[4]svelte(https://github.com/sveltejs/svelte)
本文作者:莫得盐
本文链接:https://juejin.cn/post/6898526276529684493
回复“加群”与大佬们一起交流学习~
点击“阅读原文”查看 120+ 篇原创文章