写在最前:虚拟DOM就是js里面的一个Object。
虚拟DOM的运行效率不一定比真实DOM高
虚拟DOM的运行效率不一定比真实DOM高,准确来说是 “虚拟DOM的运行效率比不正确地操作真实DOM的效率高” 。即使我们翻遍Vue和React的文档也找不到与“虚拟DOM能提高运行效率”的说法。
设想一下现在又同一个网页,如果是html+css+js三件套,浏览器会执行的渲染机制如下:HTML会被HTML解析器解析成DOM Tree,CSS会被CSS解析器解析成CSSOM Tree(先DOM后CSS),两者解析完成后会被合并到一起,形成渲染树。
而如果这个页面我们用Vue框架来写,要先创建Vue实例(初始化data和method),然后根据模板创建虚拟DOM,再经历若干处理步骤之后才会生成真实DOM并进行挂载。这么一对比,虚拟DOM其实是比真实DOM慢的。
除此之外,每一次虚拟DOM的比较(diff),虚拟DOM上事件的映射(React关于事件的处理与原生js不同,由一个全局对象来映射元素与交互事件之间的关系)都会而外消耗性能。
使用虚拟DOM的原因
首先来一张大佬解惑,这已经说了九成原因了。向TX佬低头。
虚拟DOM到底做了什么优化?
为什么说虚拟DOM比不正确的操作真实DOM的效率要高呢?yyx在zhihu上有很好的解答:
- 原生DOM操作 vs 通过框架操作
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。- 对 React 的 Virtual DOM 的误解
React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作… 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。
框架设计的初衷
“数据驱动页面”是Vue和React的核心之一。而他们两者对于虚拟DOM一直以来强调的都是 “可以提高开发效率,增强代码的可维护性” 而非提高运行效率。但即便如此虚拟DOM仍然会在保证性能的下限的前提下,为开发者提供一个还说得过去的性能。
如果不使用虚拟DOM,手撸代码实现往标签上挂载数据的功能,可能要这么写:
const realDOM = document.getElementById('haha')
realDOM.innerText = 'hello world' // 然后这行代码会时不时就出现在你的JS文件中
使用虚拟DOM之后:
this.data.name = 'haha'
总而言之,Vue的初衷就是让开发者不用写那么一大堆繁琐的,直接操作虚拟DOM的代码。而且越是大型的项目你越难以精准地操作每一个DOM。
而且,在框架中如果不正确的直接操作了真实DOM,会造成一些意料之外的错误。用React官网的一个例子来说明:
如果你坚持聚焦和滚动等非破坏性操作,应该不会遇到任何问题。但是,如果你尝试手动修改 DOM,则可能会与 React 所做的更改发生冲突。
为了说明这个问题,这个例子包括一条欢迎消息和两个按钮。第一个按钮使用 条件渲染 和 state 切换它的显示和隐藏,就像你通常在 React 中所做的那样。第二个按钮使用 remove() DOM API 将其从 React 控制之外的 DOM 中强行移除。
尝试按几次“通过 setState 切换”。该消息会消失并再次出现。然后按 “从 DOM 中删除”。这将强行删除它。最后,按 “通过 setState 切换”:import { useState, useRef } from 'react'; export default function Counter() { const [show, setShow] = useState(true); const ref = useRef(null); return ( <> <button onClick={() => { setShow(!show) }}>通过setState切换</button> <button onClick={() => { ref.current.remove() }}>从DOM中删除</button> { show && <div ref={ref}>hello world</div> } </> ) }
尝试按几次“通过 setState 切换”。该消息会消失并再次出现。然后按 “从 DOM 中删除”。这将强行删除它。最后,按 “通过 setState 切换”控制台就会报错。
在你手动删除 DOM 元素后,尝试使用 setState 再次显示它会导致崩溃。这是因为你更改了 DOM,而 React 不知道如何继续正确管理它。
所以最佳实践应该是将refs用于非破坏性操作,例如聚焦、滚动或者测量DOM元素。同时应避免更改由 React 管理的 DOM 节点。 对 React 管理的元素进行修改、添加子元素、从中删除子元素会导致不一致的视觉结果,或与上述类似的崩溃。
但是,这并不意味着你完全不能这样做。它需要谨慎。 你可以安全地修改 React 没有理由更新的部分 DOM。例如,如果某些元素在 JSX 中始终为空,React 将没有理由去变动其子列表。 因此,在那里手动增删元素是安全的。
再提一嘴性能
作为一门脚本语言,JavaScript是不能直接编译的(V8都是用C++写的……),即使是你只执行一次createElement操作js都要转译成C++去执行,而转译又会消耗性能。
此外,DOM属于渲染引擎,JS属于JS引擎,在浏览器的内核他们相互独立,当我们用JS去操作DOM的时候引擎间便发生了“跨界交流”,而这个跨界交流的次数越多,性能下降的问题会越显著。
把 DOM 和 ECMAScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能JavaScript》
ECMAScript 每次访问 DOM,都要经过这座桥,并交纳“过桥费”,访问 DOM 的次数越多,费用也就越高。因此,推荐的做法是尽量减少过桥的次数,努力呆在 ECMAScript 岛上。——《高性能JavaScript》
而虚拟DOM也考虑到这一点,所以不管是Vue还是React,在最后对真实DOM进行的操作都有经过优化,而不是简单的:
const div = document.getElementById('id')
div.innerText = 'haha'
当然这个优化如果你够diao你也能写,甚至能写的比Vue和React还精妙,只是相当繁琐。所以才会说虚拟DOM在保证下限的前提下为开发者提供一个还说得过去的性能,以及虚拟DOM的运行效率比不正确地操作真实DOM的效率高。
关于diff算法
diff算法永远是弹虚拟DOM绕不开的一个点。多次直接操作DOM,如果每次都造成页面的重排(元素的几何属性发生改变)或者重绘(颜色、字体等属性发生改变),那么会带来极大的性能开销。而diff算法则带来以下几个好处:
- 多次对比,一次性渲染。在Vue和React里面。虚拟DOM会进行频繁的比较,但比较的耗时总比我们每次都得操作真实DOM来得快,最后再在真实DOM中进行一次重排与重绘,真实DOM重绘和重排的频率是相当低的。
- 通过key的比较,筛选可复用的元素。假设我们由一个ol,里面有10个li,如果我们只是想把这十个li的顺序打乱,用原生的js来操作的话是要进行“完全的增删改出”,卸载和挂载真实DOM的操作会被频繁执行。而借助diff算法,只是元素顺序发生改变的话,元素本身则可以被复用。
跨平台
关于虚拟DOM实现跨平台的原理说实话我也不是很理解,但他确实是虚拟DOM被广泛使用的原因之一。提一嘴RN(React在原生移动应用平台的衍生产物),相同的Js代码实现在一个虚拟DOM,在React 中最终会映射为浏览器 DOM 树,而 RN 中 会通过 JavaScriptCore 被映射为原生控件树。好像也不是那么难理解……