一、框架设计思路:权衡的艺术
从范式角度看,框架设计成命令式
好还是声明式
好?
框架设计成运行时
?编译时
?运行时 + 编译时
?
1.1、命令式
代表框架:jQuery
特点:关注过程
1.2、声明式
代表框架:Vue.js
特点:关注结果
1.3、例子
有以下需求:
1、获取id为app的div标签
2、它的文本内容为hello world
3、为其绑定点击事件
4、当点击时弹出提示: ok
转换成命令式代码就是:
$('#app') //获取div
text('hello world') //设置文本内容
. on( 'click', () => { alert('ok') }) //绑定点击事件
可以看出代码描述的是做事的过程
。
转换成声明式代码就是:
<div @click="() => alert('ok')">hello world</div>
可以看出,代码直接给我们展示一串结果,这段结果就是告诉Vue让它去做上面命令式代码做的东西。也就是说,Vue帮我们封装了过程(你肯定猜到Vue内部的实现方式是命令式的)
,而我们只需要使用特定的指令就可以让Vue为我们工作而不需要我们去弄清楚其中实现的细节。
1.4、二者优缺点(命令式、声明式)
性能上
命令式代码直接执行,而声明式相当于要先编译声明式代码,将其转成命令式,再去执行,所以声明式代码的性能是永远也优先不了命令式代码的
。
可维护性上
命令式代码可维护性更差,它需要我们手动去创建、更新、删除dom结构,而声明式代码没有这个操作,只需使用指定的指令即可。
权衡
这体现了我们在框架设计上要做出的关于可维护性与性能之间的权衡:在采用声明式提升可维护性的同时,性能就会有一定的损失,而框架设计者要做的就是:在保持可维护性的同时让性能损失最小化
。
1.5、虚拟DOM
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
如果我们能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能。而所谓的虚拟DOM,就是为了最小化找出差异这一步的性能消耗而出现的。
一般来说采用虚拟DOM的更新技术的性能理论
上不可能比原生JavaScript 操作DOM更高。这里我们强调了理论上三个字,因为这很关键,为什么呢?因为在大部分情况下,我们很难写出绝对优化的命令式代码,尤其是当应用程序的规模很大的时候,即使你写出了极致优化的代码,也一定耗费了巨大的精力,说白了就是投入产出比不高,无法在一定时间内达到性能、代码完成度两者的最大化
,而这个就是虚拟DOM需要解决的问题。
1.6、运行时
假设我们设计了一个框架,它提供一个Render函数,开发者们可以为该函数提供一个树型结构的数据对象,然后Render函数会根据该对象递地将数据渲染成DOM元素。我们规定树型结构的数据对象如下:
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
};
每个对象都有两个属性:tag 代表标签名称,children 既可以是一个数组(代表子节点),也可以直接是一段文本(代表文本子节点)。
Render函数的实现如下:
function Render(obj, root) {
const el = document.createElement(obj.tag)
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)
}
有了这个函数,我们就可以这样使用它:
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
};
// 渲染到 body 下
Render(obj, document.body)
如果开发者觉得手写树型结构的数据对象太麻烦,不直观,希望支持用类似于HTML标签的方式描述树型结构的数据对象,但是我们不支持,那么,我们刚刚编写的框架就是一个纯运行时的框架,即用指定结构的对象
和Render函数
来进行工作的这么一种方式。
1.7、运行编译时
书接上文,我们考虑了开发者的诉求,决定引人编译的手段,把HTM标签编译成树型结构的数据对象,这样心智就会大大降低,编码的速度也会快很多。
我们决定编写一个Compiler程序,它的作用j就是把HTML字符串编译成树型结构的数据对象。那么开发者该怎么用呢?其实这也是我们要思考的问题,最简单的方式就是让用户分别调用Compiler函数和Render函数:
const html = `<div>
<span>hello world</span>
</div>
`
// 调用 Compiler 编译得到树型结构的数据对象
const obj = Compiler(html)
// 再调用 Render 进行渲染
Render(obj, document.body)
此时我们的框架就是一个运行时 + 编译时
框架,准确地说,上面的代码其实是运行时编译
。意思是代码运行的时候才开始编译,而这会产生一定的性能开销,因此我们也可以在构建的时候就执行Compiler程序将开发者提供的内容编译好,等到运行时就无须编译了,这对性能是非常友好的。
1.8、编译时
书接上文,既然编译器可以把HTML字符串编译成数据对象,那么也可以直接编译成命令式代码吧?
这样我们只需要一-个Compiler函数就可以了,连Render都不需要了。其实这就变成了一个纯编译时
的框架,因为我们不支持任何运行时内容,用户的代码通过编译器编译后才能运行。
1.9、三者优缺点(运行时、编译时、运行时+编译时)
运行时
由于它没有编译的过程,因此我们没办法分析用户提供的内容。
运行时+编译时
如果在运行时的基础上加入编译步骤,我们可以分析用户提供的内容,看看哪些内容未来可能会改变,哪些内容永远不会改变,这样我们就可以在编译的时候提取这些信息,然后将其传递给Render函数,Render 函数得到这些信息之后,就可以做进一步的优化了。
编译时
假如我们设计的框架是纯编译时的,那么它也可以分析用户提供的内容。由于不需要任何运行时,而是直接编译成可执行的JavaScript代码,因此性能可能会更好,但是这种做法有损灵活性,即用户提供的内容必须编译后才能用。
总结
实际上,在这三个方向上业内都有探索,其中svelte就是纯编译时的框架,但是它的真实性能可能达不到理论高度。Vue3仍然保持了运行时+编译时
的架构,在保持灵活性的基础上能够尽可能地去优化。在Vue3在保留运行时的情况下,其性能甚至不输纯编译时的框架。