声明式与命令式
声明式编程
声明式编程与命令式不同,其 更加在意结果而非过程,强调声明而非命令。类似于领导(开发者)发话,只要说一句话就可以了,而底下的员工(程序)要考虑的事情就很多了。
例如,你调用一个名为 getUserName()
的方法,作为调用者,你并不需要关注其内部是如何实现的,只需要知道产生的结果即可,这就是声明式。
命令式编程
命令式编程强调对程序实现过程的 具体描述,即“命令”的过程。例如在 JavaScript 中你要修改某个元素内部的文本,如果你使用原生的DOM操作方法,你需要编写以下代码:
const element = document.getElementById('#id'); // 通过Id获取元素
element.innerHTML = '新的内容'; // 将新的内容赋予元素的innerHTML
你编写了详细的修改过程,每一步都是对程序的 精确控制,这就是命令式编程。
核心区别
声明式简洁易读,命令式灵活可控。
声明式编程具有容易实现高内聚、低耦合的特性,如以下代码:
const id = getUserId();
if (isValidId(id)) {
return getUserData(id);
}
throwInvalidError();
其含义一目了然,甚至不需要添加任何注释。其中的逻辑分散在多个方法中,调用者无需关注其内部的具体实现流程。
但是,如果调用者需要对其中任何一个过程进行精确控制,由于代码逻辑被封装在方法中,除非修改对应方法的实现,否则都是不能达到目的。并且由于声明式编程的这一特性,程序往往会被迫承担多余的性能消耗。
这时候,命令式编程就体现出他的优势了,命令式允许开发者对实现流程的细节做任意程度修改,为程序开发提供了细粒度的控制。但是命令式编程的缺点也是显而易见的,命令式编程会极大增加代码的复杂度,并使得代码难以理解。
综上所述,我们可以得到两者的弊端:声明式编程可能会造成不必要的性能损耗,命令式编程则可能会使得程序变得复杂难懂。
对于性能的考量
声明式代码的性能并不优于命令式代码的性能
书中提到,对于框架而言,为了实现最优更新性能,其只需要找到更新前后变化的部分进行更新即可。
在这次更新中,如果我们使用了声明式编程的方式,那么框架并不知道实际上哪里发生了变化。因此,他需要使用一种算法来找出变化之处 —— Diff 算法。如果我们将 Diff算法 的性能损耗设为 B,更新 DOM 的性能设为 A,那么使用声明式编程的性能消耗为:A + B。
而如果我们采用命令式编程(不使用框架)的方式进行更新,那么总性能消耗就只有 A,因为这个过程省去了框架帮我们找出差异的损耗。
从中我们也可以得出一个结论,框架的性能表现主要取决于 B 的大小,即找出差异的性能。
虚拟DOM的引入
为了最小化 B (找出差异的性能)的大小,Vue 引入了 虚拟DOM 这一技术,在这种技术普遍存在于多种前端框架中,例如 React。
创建HTML元素
在以往如果我们需要创建一系列的 HTML 元素,直接将 模板字符串 赋值给父元素的 innerHTML 属性是一种可行的方法,它看起来是这样的:
element.innerHTML = '<span><div>...</div></span>';
这种方法的性能损耗相当大。网页为了渲染出模板字符串的内容,必须首先将模板字符串解析为 DOM 树,而这是一个 DOM 层面的计算。
我们都知道,涉及 DOM 的计算远比 JavaScript 层面的计算性能差。
如果我们每次修改元素内容的时候都对 innerHTML 做操作,哪怕 innerHTML 字符串替换前后只差了一个字符,那么整个innerHTML对应的内容都要重新创建。注意此处用词是 创建 而非 更新 ,由于DOM层面的计算代价十分昂贵,过多的元素重新创建将会造成数量级的性能消耗。尤其是当 innerHTML 模板绑定的元素非常多的时候,性能更是差得离谱。
这时候虚拟DOM存在的意义就显而易见了,由于虚拟DOM是Javascript层面的产物,所以对其进行更改并不会产生在DOM层面进行修改那样巨大的代价。
心智负担 or 可维护性 or 性能
诚然,如果我们采用了命令式编程的方式进行DOM层面的修改,确实能够获得较高的性能。只不过我们必须明白: 写出绝对优化的代码是一件非常困难的事情 。命令式编程会严重加剧开发者的心智负担并且降低程序的可维护性,这与其产生的部分性能损耗相比也许更加严重。
现在我们已经意识到了一件事:完全可以用可接受的、较少的性能损耗去换取较低的心智负担和更高的维护性。
虚拟DOM存在的意义也许就在于此。
编译时和运行时
对于框架而言,有三种选择:纯编译时、运行时 + 编译时、纯运行时
运行时
假设你给框架提供了一个递归对象,每个对象中都包含本元素的属性和其子元素,类似于:
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}
然后在程序运行时,你使用一个名为 Render
的函数来将 obj
渲染为DOM元素。
Render(obk, document.body); // 渲染到body下
那么这就是一个运行时框架。
编译时
你自创了一种用于表示HTML结构的语法,但是框架得在用户写完后对这种语法进行转化,转化为一种能被利用的数据。例如,Vue中的模板语法:
<template>
<div>
<span>hello world</span>
</div>
</template>
于是你编写了一个叫 Compiler
的程序,这个程序将在用户保存时自动完成以上操作。那么这就是一个运行时框架。
运行时 + 编译时
框架将用户编写的模板语法转化为了类似于 obj
的数据,然后在运行时使用 Render
函数对其进行渲染。那么,这个框架就变成了一个运行时 + 编译时的框架。Vue.js 3.0 就是这样的一种框架。
总结
本章主要讲了声明式和命令式各自的特点和局限,框架开发者应当如何尽可能降低声明式带来的性能损耗,并降低开发者的心智负担,提高编写程序的可维护性。
接着,书中讨论了虚拟DOM的性能,以及其与传统操作模式的区别的优势。
最后,书中讨论了编译时和运行时这两种框架类型。并指出 Vue 3.0 术语运行时 + 编译时的框架。
本文同样也发布在我的个人博客网站:《Vue.js设计与实现》学习笔记 | 权衡的艺术 。欢迎来访噢!