1. 权衡的艺术

121 阅读5分钟

学习vue3之前需要知道各个模块的实现思路和细节。
1. 框架设计成命令式还是声明式? 两者区别和优缺点呢?
2. 框架要设计成纯运行时还是纯编译时?或者运行时+纯编译时?三者区别和优缺点呢?

1.1 命令式和声明式

jQuery是命令式框架,特点是关注过程

 1. 获取id为app的div标签
 2. 它的文本内容为hello world
 3. 为其绑定点击事件
 4. 当点击时弹出提示:ok

对应的代码为:

$("#app") // 获取div
    .text('hello world') //设置文本内容
    .on('click', () => { alert('ok') }) // 绑定点击事件

可以看出,自然描述语言能与代码产生一一对应的关系,代码本身描述的是“做事的过程”。

vue是声明式框架,特点是关注结果

上面的例子,通过vue的话,实现如下:

<div @click="() => alert('ok')">hello world</div>

也就是说,vue帮我们封装了过程, 内部实现是命令式的,暴露给用户的更加声明式

1.2 性能与可维护性的权衡

声明式代码的性能不优于命令式代码的性能

例如,修改div文本内容为hello vue3

命令式代码实现:

div.textContent = 'hello vue3'

声明式代码实现:

先看实现前后差异:

// 修改之前
div.textContent = 'hello world'

// 修改之后
div.textContent = 'hello vue3'

那么声明式代码只关注结果,vue实现是找到差异+修改内容才可

如果定义修改性能消耗为 A, 查找消耗为 B
命令式代码性能消耗: A
声明式代码代码性能消耗: A + B
所以 声明式代码代码性能消耗<=命令式代码性能消耗

声明式代码性能不一定优,那为何vue会选择声明式呢?

为了更好的代码可维护性以及代码的直观体现!不必再去纠结于元素的增删改查。

在保持可维护性的同时让性能损失最小化, 即性能与可维护性的权衡

1.3 虚拟DOM的性能到底如何

为什么会使用虚拟DOM

由上一节得出:声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗

那么如果能最小化找出差异的性能消耗, 就可以使得声明式代码性能无限接近命令式代码的性能,所谓的虚拟DOM, 就是为了解决这一问题而出现的。

创建页面时性能对比

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

虚拟DOMinnerHTML
纯JavaSript运算创建JavaScript对象(VNode)渲染HTML字符串
DOM运算新建所有DOM元素新建所有DOM元素

注:在创建页面时都需要新建所有DOM元素,所以只需要在宏观的角度只看数量级上的差异

更新页面时性能对比

innerHTML和虚拟DOM在更新页面时的性能对比图:

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

注:更新页面时,在纯JavaSript运算层面虚拟DOM会多处一个Diff的性能销毁,但是也构不成数量级的差异,而在DOM运算层面,虚拟DOM只进行必要的更新,这时候虚拟DOM的优势就体现出来了

基于此,可以总结innerHTML、虚拟DOM以及原生JavaScript(指createElement等方法)更新页面时的性能,如图所示:

innerHTML(模板)< 虚拟DOM < 原生JavaScript

innerHTML(模板)虚拟DOM原生JavaScript
心智负担中等心智负担小心智负担大
可维护性强可维护性差
性能差性能不错性能差

有没有办法做到,既声明式地描述UI,又具备原生JavaScript的性能呢?

1.4 运行时和编译时

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

纯运行时的

const obj = {
    tag: 'div',
    children: [
        { tag: 'span', children: 'hello world'}
    ]
}
function Render(obj, root) {
    const el = document.createElement(obj.tag)
    // obj.children为字符串直接添加进el
    if(typeof obj.children === 'string'){
        const text = document.createTextNode(obj.children)
        el.appendChild(text)
    } else if(obj.children){
        // 数组,递归调用Render,使用el作为root参数
        obj.children.forEach(child => Render(child, el))
    }
    // 将元素添加到root
    root.appendChild(el)
}
// 渲染到body下
Render(obj, document.body)

运行结果如下: image.png

特点:需要手写树形结构的对象,不直观

运行时+编译时的

假设有个编译函数Compiler,可以将模板字符串转化为树型结构的数据对象

const html = `
<div>
    <span>hello world</span>
</div>

// 调用 Compiler编译得到树型结构的数据对象
const obj = Complier(htnl)

// 再调用 Render进行渲染
Render(obj, document.body)

纯编译时的

const div = document.createElement('div')
const span = document.createElement('span')
span.innerText = 'hello world'
div.appendChild(span)
document.body.appendChild(div) 

对比分析

  • 纯运行时的:没有编译的过程,没法分析用户提供的内容未来会不会改变
  • 运行时+编译时的:有编译过程,可以分析内容,可以为Render提供优化空间
  • 纯编译时的:也可以分析用户提供的内容,不经过其他运行过程,直接编译可能性能更好,但是有损灵活性

Vue3 保持了运行时编译时的架构,在保持灵活性的基础上尽可能的优化,甚至不输纯编译时的框架

总结

  • 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
  • 虚拟DOM的意义在于使找出差异的性能消耗最小化
  • innerHTML、虚拟DOM以及原生JavaScript(指createElement等方法)三者操作DOM的性能,不可以简单的下定论,这与页面大小变更部分的大小都有关系,还需要结合心智负担可维护性等因素综合考虑,发现虚拟DOM是个还不错的选择
  • Vue3是一个运行时+编译时的框架,它在保持灵活性的基础上,还能通过编译手段分析用户提供的内容,从而进一步提升更新性能