1.写在前面
本文便带领大家进入《Vue.js设计与实现》描述的宇宙,开启探索框架设计的思想的旅程。
2.框架设计里到处都体现了权衡的艺术
作者在文章中写到『框架设计里到处都体现了权衡的艺术』,的确在进行设计模式和技术选型的时候,我们都会去综合考虑性能和开发效率,去权衡各方面因素从而得到尽可能完善的框架。
框架是由各个模块组成的,彼此关联又相互独立,要做到实现当前的功能,又要考虑到后续的模块拆分和拓展。作为框架的设计者,需要站在全局的角度去思考和设计,需要对整体的设计思路有着清晰的掌控。实现细节是在设计的时候不用太过于在意的视点,不要囿于高山的雾层,毕竟它只是整个框架的冰山一角。
在Vue框架的设计中,最能体现这种权衡思想的可能是『命令式和声明式』、『编译时和运行时』等之间的权衡,需要了解彼此的差异、汲取两者的优点。
3.命令式和声明式
正如你所知道的,在计算机编程范式中有三种:命令式编程,声明式编程和函数式编程。
命令式编程:是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。
声明式编程:以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。
函数式编程:是与声明式编程关联的,只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
命令式
对于前端开发从业者而言,JQuery框架并不会陌生,它其实就是最经典的命令式的框架设计,它关注计算机执行的步骤,即关注过程。命令式编程其实就是写给计算机看的,让我们的自然语言能与代码进行一一对应,更符合我们做事逻辑。
$("#app")//获取id为app的标签元素
.text("hello pingping")//设置标签的文本内容
.on("click",()=>console.log("hello onechuan"));//给id为app的便签绑定事件
等价于原生js的代码:
const div = document.querySelector("#app");
div.innerText = "hello pingping";
div.addEventListener("click",()=>console.log("hello onechuan"))
声明式
而声明式更关注实现结果,具体的实现过程并不是使用者所在意的,这也很大程度地降低了认知成本,关注表层逻辑提升使用效率。
事实上,Vue.js的设计并不是简单使用纯粹的命令式或是声明式编程,而是结合两者的优点。在内部实现使用命令式告知计算机如何运行,对外暴露的API等则是采用的声明式编程,能够用人话让使用者读懂结果。
<div @click="()=>console.log('hello onechuan')">hello pingping<div>
性能和可维护性
在《编译原理》书中,了解到命令式代码的性能优于声明式代码,这是因为声明式代码需要经过编译成计算机能够读懂的命令式代码。但是呢,声明式代码更像是人类能够读懂的人话,在尽可能牺牲少量性能的同时降低代码的维护成本。
Vue.js框架就是结合两者的优点,对命令式代码进行了封装,对使用者提供可维护性更高的声明式代码。
4.真实DOM和虚拟DOM
对于声明式代码的更新性能消耗而言:声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,如果我们找到能够让找出差异的性能消耗最小化的算法,那么就能够将声明式代码的性能消耗无限趋近于命令式代码性能消耗。
我们分别从创建页面和更新页面两方面,对真实DOM和虚拟DOM操作的性能消耗进行分析:
状态 | 虚拟DOM(纯JS创建VNODE) | 真实DOM(渲染HTML字符串) |
创建页面 | 新建所有的DOM对象 | 新建所有的DOM对象 |
更新页面 | 必要的DOM更新 | -销毁所有的旧DOM,新建所有的新DOM |
关于性能:真实DOM<虚拟DOM<原生JS。
此处简要的进行总结,后续文章将会有更详细的数据分析。
5.编译时和运行时
在框架设计时还要考虑是选择:纯运行时、纯编译时还是运行时+编译时,这需要结合你所期望的待设计框架的特征做出合适的决策。
运行时
所谓运行时,就是计算机所运行时的代码,不需要经历额外的处理,便能够实现我们所期许的结果。
例如,我们需要将提供的树形结构的数据对象,渲染到渲染成dom树,那么我们需要设计一个Render函数直接进行渲染,这样就能得到我们想要的结果:
const obj = {
tag:"div",
children:[{
tag:"span",
children:"hello world"
}]
}
Render(obj, document.body)
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.appendChild(el)
}
浏览器显示如下:
编译时
那么,编译就是一种转换技术,将高级语言转换低级语言,Vue.js将HTML标签通过编译转换成树形结构的数据对象。
这样我们需要编写一个Compiler函数,用于将HTML标签通过编译换成树形结构的数据对象。如下:
const html = `
<div>
<span>hello world</span>
</div>`
const obj = compoler(html)
Render(obj, document.body)
这样就能将:
<div>
<span>hello pingping</span>
</div>
编译成:
const obj = {
tag: 'div',
children: [
{tag: 'span', children: 'hello world'}
]
}
结合Render函数进行渲染,这样我们就初步设计了一个运行时+编译时的框架了。
运行时+编译时
所谓在Vue.js是运行时+编译时框架,其实指的是:
支持运行时:使用者可以直接提供树形结构的数据对象而无需编译;
支持编译时:使用者可以提供HTML字符串,将其编译成树形结构的数据对象后再交给运行时处理。
为什么Vue.js要设计成运行时+编译时框架?
这所以这样设计也是开源团队进行权衡的结果,运行时无法分析用户提供的内容,而加入编译后就可以对用户内容进行分析和编译。在编译的时候提取这些用户内容的信息,再通过Render函数进行渲染。
当然,将框架设计成纯编译时,可以分析用户内容直接编译成可执行的JS代码,在保证性能的同时牺牲了框架的灵活性和可维护性,对用户而言必须对内容编译后才能使用。
对此,Vue.js的设计是综合考量,才用的运行时+编译时的框架设计,在保留运行时的灵活性的同时,尽可能不牺牲性能。
6.写在最后
在本文中,了解到开源团队对于命令式和声明式、真实DOM和虚拟DOM、运行时和编译时的权衡选择,在尽可能减少性能损耗的同时提供最好的用户体验和可维护性、灵活性。