虚拟DOM为何而来?

751 阅读5分钟

如果你在不断追逐层出不穷的技术框架,却忘记停下里欣赏框架设计的用心良苦;

如果你还对大多数视图框架采用虚拟DOM的处理方式一知半解;

或许这篇文章能解答你的困惑:

1、视图框架常用范式有哪些?框架设计方面有何区别?框架层面更愿意使用哪种呢?

视图框架通用范式有两种:命令式、声名式

命令式:关注过程【通过代码描述做事的过程,符合我们的逻辑直觉】

声明式:关注结果【通过代码描述想要的结果,中间的具体实现并不需要关心】

性能上:声明式不优于声明式

可维护性:声明式优于命令式

下面就举例说明一下:

  • 获取id为app的div标签
  • 它的文本内容为hello
  • 为它绑定点击事件
  • 当点击时,弹出提示:ok

原生javascript实现【声明式】:

const div = document.querySelector('#app') //获取div
div.innerText = "hello"
div.addEventListener('click', () => {alert('ok')}) //绑定点击事件

vue.js【命令式】

<div id="app" @click="() => {alert('ok')}">hello</div>

现在希望将div标签的文本内容修改为“hello world”

命令式修改:

div.textContent = 'hello world' //明确知道要修改内容,直接相关命令操作即可

声明式修改:

<div id="app" @click="() => {alert('ok')}">hello world</div>

对于框架而言,为了实现最优的更新性能,它需要找到前后的差异并只更新变化的地方,但是最终实现更新的代码仍然是:

div.textContent = 'hello world' 

如果我们把直接的性能消耗定义为A,把找出差异的性能消耗定义为B

命令式代码的更新性能消耗=A

声明式代码的更新性能消耗=A+B

只有当B为0时,性能消耗才会相同,但是框架本身就是封装了命令式代码才实现了面对用户的声明式,所有声明式无法做到超越,这也是声明式不优于命令式的原因。

从上面示例可以看出,命令式开发的时候,我们要维护实现目标的整个过程,包括手动完成DOM元素的创建、更新、删除等操作。而声明式真是我们想要的结果,看上去更直观,至于具体实现过程,在日常开发需求过程中,并不需要关心。这也是为什么声明式具有更高的可维护性。

框架一般采用声明式提升可维护性的同时,性能就会有一定的损失,而框架设计者要做的就是:在保持可维护性的同时让性能损失最小化。

2、框架采用声明式如何做到性能损失最小化呢?

虚拟DOM就是为了最小化性能差异而出现的。采用虚拟DOM的更新技术的性能理论上不可能与原生JavaScript操作DOM更高。强调理论上,因为大部分情况下,我们很难写出绝对优化的明令式代码,尤其是应用程序规模很大的情况下,即使写出了极致优化的代码,也一定耗费巨大的精力。

以下会从页面创建、页面更新两个维度,对innerHTML、虚拟DOM、原生JavaScript三种处理方式性能层面进行对比。前文所说的JavaScript实际上指的是document.createElement之类的DOM操作方法,并不包含innerHTML。

  • createElement性能更好,更安全,插入多个元素需要使用DocumentFragment
  • innerHTML使用更简单、更简洁、更直观,但安全性需要注意

页面创建过程中:

innerHTML:

const html = `<div><span>...</span></html>`
div.innerHTML = html

为了渲染出页面,首先要把字符串解析成DOM树,这是一个DOM层面的计算,我们要知道DOM的运算要远比JavaScript的计算性能差。那么可以用一个公式来表达通过innerHTML创建页面的性能 = HTML字符串拼接的计算量+innerHTML的DOM计算量。

虚拟DOM:
第一步是创建JavaScript对象,这个对象可以理解为真实DOM的描述;
第二步是递归遍历虚拟DOM并创建真实DOM。那么也可以用一个公式来表达虚拟DOM创建页面的性能 = 创建JavaScript对象的计算量+创建真实DOM的计算量。

从上面内容可以看出,无论是纯JavaScript层面的计算,还是DOM层面的计算,其实两者差距不大。这里我们从宏观的角度只看数量级上的差异。

页面更新过程中:

使用innerHTML更新页面的过程中是重新创建HTML字符串,再重新设置DOM元素的innerHTML属性,而重新设置innerHTML属性就等价于销毁所有旧的DOM元素,再全量创建新的DOM元素。

虚拟DOM:它需要重新创建JavaScript对象(虚拟DOM树),然后再比较新旧虚拟DOM,找到变化的元素再更新它。

可以发现,更新页面时,虚拟DOM的性能因素与影响innerHTML的性能因素不同,对于虚拟DOM来说,无论页面多大,都只会更新变化的内容,而对于innerHTML来说,页面越大,更新页面消耗的 性能越大。

那么可以粗略的总结一下innerHTML、虚拟DOM、原生JavaScript在更新页面时的性能