Vue.js 权衡的艺术

82 阅读5分钟

Vue.js 权衡的艺术

刚工作时把《Vue.js设计与实现》这本书看了一遍,当时觉得写的确实挺好的,很多地方讲解的很清晰,但奈何当时Vue3用的不多,很多地方没看懂,现在想回过头来重新撸一遍,顺便当作笔记记录一下。

1 命令时和声明式

视图层框架通常分为命令式和声明式,它们各有优缺点,jQuery就是典型的命令式框架。命令式框架的一大特点就是关注过程。而声名式框架更加关注结果。

如用下面两段代码:

//命令式
const div = document.querySelector('#app') //获取div
div.innerText = 'hello world' //设置文本内容
div.addEventListener('click',()=>{alert('ok')}//绑定点击事件
//声明式
<div @click="()=> alert('ok')">hello world</div>

1.2 性能与可维护性的权衡

结论:声明式代码的性能不优于命令式代码的性能。

以上面代码为例子,假设要将div标签的文本内容改为hello vue3:

//命令式
div.textContent = 'hello vue3'//直接修改
//声明式 vue.js
//修改前
<div @click="()=> alert('ok')">hello world</div>
//修改后
<div @click="()=> alert('ok')">hello vue3</div>

可以看到,理论上命令式代码可以做到极致的性能优化,而声明式代码需要找到前后的差异并只更新变化的地方,如果把直接修改的性能小号定义为A,把找出差异的性能消耗定义为B,则

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

因此最理想的情况是,当找出差异的性能消耗为0时,声明式代码与命令式代码的性能相同,当无法超越。毕竟框架本身就是封装了命令式代码才实现了面向用户的声明式。

声明式相比于命令式的优点在于它的可维护性更强,因为声明式的代码更加的直观,我们只需要关心结果,至于过程都被Vue.js封装好了。

1.3 虚拟DOM的性能到底如何

虚拟DOM目的是为了最小化找出更新的差异这一步的性能消耗而出现的。

innerHTML创建页面过程:

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

我们知道涉及到DOM层面的运算要远比JavaScript层面的计算性能差,如图跑分结果:

2261.png

上面是纯JavaScript层面的运算,下面是DOM操作,他们的速度不在一个量级上。

下面直观的对比innerHTML和虚拟DOM在创建页面时的性能:

2262.png 可以看到在创建时虚拟DOM和innerHTML没有差异,但是在更新页面时性能会有所不同:

2263.png

可以发现,在更新页面时,虚拟DOM在JavaScript层面的运算要比创建页面时多一个Diff的性能消耗,然而它毕竟时JavaScript层面的运算,不会产生太大的差异。而DOM层面的运算只会更新必要的元素,但是innerHTML需要全部更新。

当更新页面时,对于虚拟DOM来说,无论页面多大,都只会更新变化的内容,而对于innerHTML来说,页面越大,更新时的性能消耗越大。

2264.png

从三个维度分析innerHTML、虚拟DOM和原生JavaScript在更新页面时的性能:心智负担、可维护性和性能。

  • 原生DOM操作方法的心智负担最大,因为你要手动创建、删除、修改大量的DOM元素。但它的性能是最高的,与此同时我们也要承受巨大的心智负担。另外,这种编码方式可维护性也极差。
  • innerHTML通过拼接HTML字符串来实现,这有点接近声明式的意思,但是拼接字符串总归也是有一定心智负担的,而且对于事件绑定之类的,还要使用原生JavaScript来处理。如果innerHTML很大,则其更新页面性能会最差,尤其是在只有少量更新时。
  • 虚拟DOM,他是声明式的,因此心智负担小,可维护性强,性能虽然比不上极致优化的原生JavaScript,当也不差。

2265.png

1.4 运行时和编译时

设计一个框架时,有三种选择:纯运行时的、运行时+编译时、纯编译时的。

纯运行时:

假设我们设计一个框架,它提供了一个Render函数,用户可以为该函数提供一个树型结构的数据对象,然后Render函数会根据该对象递归地将数据渲染成DOM元素。

const obj = {
	tag:'div',
	children:[
		{ tag:'span',children:'hello world' }
	]
}

function Render(obj,root){
	const el = document.createElement(obj.tag)
	if(typeof obj.children === 'string') {
		const text = document.createTextNode(obj.children)
		el.appendChildren(text)
	}else if(obj.children){
		//数组,递归调用Render,使用el作为root参数
	obj.children.forEach((child) => Render(child,el))
	}

	//将元素添加到root
	root.appendChild(el)
}

Render(obj, document.body)

用户在使用它渲染内容时,直接为Render函数提供了一个树形结构的数据对象。这就是一个纯运行时的框架。

编译时+运行时

const html = `
<div>
	<span>hello world</span>
</div>
`
//调用Compiler编译得到树型结构的数据对象
const obj = Compiler(html)
//再调用Render进行渲染
Render(obj,document.body)

用户可以直接提供HTML字符串,我们将其编译为数据对象后再交付给运行时处理。

纯编译时

2266.png

编译器直接将HTML字符串编译成命令式代码,不再需要Render函数。

纯运行时的框架没有编译的过程,因此没办法分析用户提供的任容,如果加入编译步骤,就可以分析用户提供的内容,看看那些内容未来可以改变,哪些不会改变,这样可以在编译的时候提取这些信息,然后传递给Render函数做进一步的优化。

纯编译时的框架不需要任何运行时,而是直接编译为JavaScript代码,因此性能可能会更好,但是灵活性很低,用户提供的内容必须编译后才能用。