Vue.js的设计思想

543 阅读8分钟

简单说说Vue.js的设计思想

这篇文章结合例子已经尽可能地将我了解到的知识表达出来了。如果有地方没有看懂的,可能是我表达能力的问题,请同学们不要怀疑自己的能力。

如果下文中有用到框架的知识,那么在没有说明的情况下都是说的Vue.js

命令时和声明式

  • 命令式 - 关注过程:简单来说就是面向过程
  • 声明式 - 关注结果

举个例子,比如说要显示一个 一级标题,内容是hello world
命令式需要先通过createElement创建标签,然后修改标签的内容,最后渲染上去,整个流程一步一步执行,写出来的是它的每一个步骤(或过程).
声明式则像使用vue的模板语法一样,直接写出来h1标签,并添加其内容,我们写出来的东西就是它将要渲染的结果。

关于声明式和命令式我们先抛出一个结论,声明式代码的性能不优于命令时代码的性能

来看看为什么?

上面的例子中我们将h1标签的内容改为hello world,h1Element.innerText = 'hello world',这样直接修改,还有比上面这行 代码更性能更好的吗?答案是没有的。因此理论上命令时代码可以做到极致的性能优化,因为我们知道明确知道哪里出现变更,只需要做必要的修改就行。

但声明式代码不一定能够做到这一点,因为它描述的是结果:<h1>hello world</h1>,对于框架来说,为了实现最优的更新性能,他需要找到前后 的差异并且只更新变化的嗲放,但是最完成这次更新的代码仍然是h1Element.innerText = 'hello world'

将直接修改的性能消耗定义为A,找出差异的性能为B,那么:

  • 命令时代码修改的性能消耗为A
  • 声明式代码修改的性能消耗为A + B

因为框架本身就是封装了命令式代码猜实现了面向用户的声明式。所以才有了上面的结论。

既然性能层面命令式代码是最好的选择,为什么Vue要选择声明式的设计方案呢?原因就在于声明式代码可维护性更强。在采用命令式代码开发的时候, 我们需要维护实现目标的整个过程,包括要手动完成DOM元素的创建、更新、删除等工作。而声明式代码展示的就是我们要的结果,看上去更直观,做事 的过程,框架本身已经封装好了。

在采用声明式提升可维护性的同时,性能就会有一定的损失,所以框架的设计者在实现的时候需要做的就是:在保持可维护性的同时让性能损失最小化。 因此Vue.js引入了虚拟dom。

虚拟dom的性能到底怎么样?

前面说到声明式代码的更新消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,因此如果能够最小化找出差异的性能消耗,就可以让声明式代码的性能。 虚拟dom就是为了最小化找出差异这一步的性能消耗而出现的。

第一个问题,为了比较innerHTML和虚拟DOM的性能,需要了解他们创建、更新页面的过程。

先来看innerHTML的:
const html = <div><span>...</span></div>,创建一个html字符串,然后将这个字符串赋值给dom元素的innerHTML属性,div.innerHTML = html,然而这行代码并没有看上去那么简单。

为了渲染出页面,首先需要将字符串解析成DOM树,这是一个DOM层面的计算。

可以用一个公式来表达通过innerHTML创建页面的性能:HTML字符串拼接的计算量 + innerHTML的DOM计算量。

再来看看虚拟DOM创建页面时的性能。虚拟DOM创建页面的过程分为两步:第一步是创建JavaScript对象,这个对象理解为真实DOM的描述; 第二步是递归地遍历虚拟DOM并创建真实DOM。

同样用公式表达:创建JavaScript对象地计算量 + 创建真实DOM地计算量。
用一个表来直观展示两种更新方式地差异。

虚拟DOM纯JavaScript运算创建DOM描述对象 + diff渲染HTML字符串
innerHTMLDOM运算必要的DOM更新销毁所有旧DOM,新建所有心DOM

可以发现在更新页面的时候,虚拟DOM在JavaScript层面的运算要比创建页面时多出一个diff的性能消耗,但他也是JavaScript层面的运算, 所以不会产生数量级的差异。而虚拟DOM值会更新必要的元素,但innerHTML需要全量更新。这时虚拟DOM的优势旧体现出来了。

当更新页面时,影响虚拟DOM的性能因素于影响innerHTML的性能因素不同。对于虚拟DOM来说,无论页面多大,都值会更新变化的内容, 而对于innerHTML来说,页面越大,以为着更新世的性能消耗越大。加上性能因素,最终更新页面时的性能如下表。

虚拟DOMinnerHTML
纯JavaScript运算创建新的JavaScript对象+diff渲染HTML字符串
DOM运算必要的DOM更新销毁所有的旧DOM,新建所有的新DOM
性能因素与数据变化量相关与模块大小相关

最后大概总结以下innerHTML、虚拟DOM及原生JavaScript在更新页面时的性能。
innerHTML性能最差 虚拟DOM性能还可以 原生JavaScript性能高,但可维护性差。 最后思考一下,能不能做到即声明式的描述UI,有具备原生JavaScript的性能呢?

运行时和编译时

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

简单说说纯运行时,假设设计了一个框架,提供了一个render函数,用户可以为该函数提供一个属性结构的数据对象,然后render函数会根据该对象 递归地将数据渲染成DOM元素。(比如vue中地render函数)

function render() {
// 省略内部实现 
}
const obj = {
   tag: 'div',
   children: [   
       { tag: 'span', children: 'hello world' }
   ]
}
render(obj, document.body)

可以发现,在使用render函数渲染内容时,直接为render函数提供了一个属性结构地数据对象。
但现在觉得使用描述dom的JS对象写起来太麻烦,所以想引入编译的手段,把HTML标签编译成树形结构的数据对象,这样也就可以继续使用render函数了。

为此,编写了一个叫做compiler的函数,它的作用就是将HTML字符串编译成树形结构的数据对象。

function render(obj, parentNode) {
// 省略内部实现 
}
function compiler(html) {
// 省略内部实现 
}
const html = `<div><span>hello world</span></div>`
const obj = compiler(html) // 调用 compiler 生成html对应的描述对象
render(obj, document.body) // 调用 render 函数渲染对应dom

通过加入上面的操作,这时框架旧编程了 运行时 + 编译时 的框架。即支持运行时,用户可以直接提供数据对象从而无需编译;无需编译时,用户 可以提供HTML字符串,将其编译为数据对象再交给运行时处理。准确地说,上面的代码其实是运行时编译,意思是代码运行的时候猜开始编译,从而产生 一定的性能开销。

因此我们也可以在构建的时候旧执行compiler程序将用户提供的内容编译好,等到运行时旧无需编译了。
那么我们可以想到,既然编译器可以把HTML字符串编译成数据对象,那么是不是也可以直接编译成命令式的代码呢。 这个时候我们只需要一个compiler函数就可以了,连render都不需要了,这就变成了一个纯编译时的框架。

最后分析一下这三种设计方案:

  • 运行时

由于没有编译的过程,因此没有办法分析用户提供的内容

  • 运行时 + 编译时

相比运行时加入了编译步骤,所以可以分析用户提供的内容,看哪些内容是未来可能会变化,就可以在编译时提取这些信息,然后将其传递给render函数, render函数得到这些信息就可以进一步优化。

分析用户提供的内容,可能有人不明白这句话,举个例子:
vue的模板语法中提供了模板语法(通过{{ xxx }}将动态内容提取出来,需要修改内容时整体结构不需要变化,只需要改变{{ }}中间的信息)。

  • 编译时

编译时它可以分析用户提供的内容。由于不需要任何运行时,而是直接编译成可执行的JavaScript代码,因此性能会更好,但这种做法有损灵活性,即用户 提供的内容必须编译后才能用。

vue使用的则是 运行时 + 编译时 的架构,在保持灵活性的基础上尽可能地去优化。

上面提到的虚拟DOM模板语法,下一周会出关于虚拟DOM和模板引擎的文章,希望可以帮助同学们深入了解vue。

并非原创,我只是知识的搬运工🤣🤣🤣
—— 文章内容来源于《Vue.js设计与实现》