如果你最近这几年一直在用JavaScript框架,那么你一定听过很多开发者对虚拟DOM性能优异,要比实际的DOM操作快很多的评价。其实,这里有一个令人惊讶的弹性模型——例如,Svelte并不使用虚拟DOM,然而它依然运行速度极快。

      下面,我们进行详细的分析:

      首先,让我们明白一个概念,到底什么是虚拟DOM:

      在使用框架的过程中,在建立应用的过程中,首先要使用render函数,以react组件例:

function HelloMessage(props) {

return (

<div className="greeting">

Hello {props.name}

</div>

);

}

这是基于JSX语法实现,那么不使用JSX语法怎么实现呢,看到一段代码:

function HelloMessage(props) {

return React.createElement(

'div',

{ className: 'greeting' },

'Hello ',

props.name

);

}

最终结果其实是一样的,用一个对象来展示页面,这个对象就是虚拟DOM。每一次应用的状态更新。例如当name属性的prop改变时,其实是重新创建了一个。框架的作用是新的DOM和旧的DOM对应变化,从而找到最新的变化对应到真实的DOM。

模仿传递如何诞生

有一种误解是虚拟DOM的产生是来自于react的诞生之初,react前核心团队成员皮特·亨(Pete Hunt)2013年的一次开创性演讲。我们摘录其中的几句:

虚拟DOM很快,实际来说是操作DOM很慢,大量的工作耗费在DOM渲染上,而大量的DOM操作其实在框架上。

我们稍微思考一下,对虚拟DOM的操作最终还是落到实际的DOM树上,对于低效率的框架来说它的确是快了。当然这是相对于2013年之前的技术框架。也确实是做了前人没有做的事情。如下代码:

onEveryStateChange(() => {

document.body.innerHTML = renderMyApp();

});

皮特后来很快做了澄清:

React框架不是变魔术,就像自己写的C语言编译器比传统的C语言编译器好,同样你也可以自己研究原生DOM操作和DOM API来超过react,无论如何,这对使用C,java或者JavaScript来说是一种巨大的性能改进。因为你不用为了系统的整体性能担心。使用react构建项目是不用考虑性能的,因为它本来就很快。

但是皮特的说法没有说到点上。

所以,问题来了,怎么证明虚拟DOM很慢

说虚拟DOM快,其实并不确切,react最初的目标是在不影响性能的前提下,把单一状态的变化反馈给应用。实际上,对这种说法我并不认同。如果这种说法正确,何必还要做性能优化,比如使用shouldComponentUpdate()让react知道当前状态或属性的改变是否不影响组件的输出。

甚至使用shouldComponentUpdate()全部更新react应用的虚拟DOM是一件工作量巨大的事。不久之前,react开发团队推出了对react新的更新React Fiber,对原有的算法进行重构,采用分片更新的方式对组件树逐步更新。如果想对React Fiber进行深入研究,可以参考这边文章——​​React Fiber是什么​​。这意味着除此之外,虽然它无法缩短或者减少更新全部虚拟DOM的工作量和时间,但是它确实不会长时间阻塞主线程。

那么,说来说去虚拟DOM的开销从何而来

显而易见的是,diff算法不是最有效的,如果不首先对虚拟DOM的上一个快照进行一一映射,那么真实的DOM树是无法更新的。以HelloMessage为例,假如name 的prop属性,从'world' 变成 'everybody'。实现的原理如下:

  1. 两个snapshots等候只有一个子元素,在这两种情况下,它都是一个<div>,这样保证生成的是一样的DOM节点。
  2. 我们把原来的DOM属性做枚举,来监听是否有新的DOM节点新增,修改或者删除。两种情况下都有一个单一属性className,值的结果是greeting。
  3. 然后对元素进行降序排列,当文本改变时,对应的改变真实DOM。

经过以上三步,发现只有第三步是有用的,事实上绝大多部分的DOM跟新就是这样,应用的基本架构不变,跳过前两步,直接走第三步是最有效的:

if (changed.name) {

text.data = name;

}

差异并不明显

diff算法被证明是很快的,至少在react和vue使用者看来是这样的,可以说,开销更大的在组建本身,一般不会这样写代码:

function StrawManComponent(props) {

const value = expensivelyCalculateValue(props.foo);

return (

<p>the value is {value}</p>

);

}

因为这样每次更新的时候都是重新计算值,不管props.foo是不是改变,但这对于不必要的计算和配置方式又是很普遍。

function MoreRealisticComponent(props) {

const [selected, setSelected] = useState(null);

return (

<div>

<p>Selected {selected ? selected.name : 'nothing'}</p>

<ul>

{props.items.map(item =>

<li>

<button onClick={() => setSelected(item)}>

{item.name}

</button>

</li>

)}

</ul>

</div>

);

}

这儿,我们生成一系列的虚拟<li>元素,每一个都挂载一个事件处理函数,每次状态改变,不管props.items是否改变,除非只关注界面渲染,否则不会去优化它。其实更快的方式不是这样。

即使有些工作微不足道,却值得我们冒险一试。好的应用都是经历成千上万次的调整优化,直到没有明显的短板和错误。

虚拟DOM不是一个功能,而是实现目标的一种方法。通过数据来驱动视图。虚拟DOM是有价值的,你可以构建应用而不需要考虑数据变化,而且性能得到优化,这也意味着更少的bug,让开发者专注业务开发,减少其他方面的时间浪费。

但这样我们也可以通过其他的数据模型而不是虚拟DOM一样能实现目的。这就是Svelte的作用。