权衡的艺术
本章节霍春阳老师关于框架的封装如何权衡命令式与声明式、如何兼顾性能与可维护性等方面进行了深入的剖析,分析了虚拟 DOM 的设计目的为了解决什么问题,还对比了运行时与编译时的区别。
命令式与声明式
命令式与声明式各有优劣,命令式代码更符合人类直觉,但可维护性较差,声明式代码可维护性较好,但不符合人类直觉。因此,在框架的设计中,需要权衡命令式与声明式,以实现更好的可维护性与符合人类直觉的代码。
原生 JavaScript 与早期框架 JQuery 都是命令式的,更加注重执行的过程;而 Vue.js 则是声明式的,更加注重结果,过程如何不关心,让 Vue.js 内部处理。因此 Vue.js 是声明式框架,其内部核心处理逻辑是命令式的。
const div = document.querySelector('#app') // 获取 div
div.innerText = 'hello world' // 设置文本内容
div.addEventListener('click', () => { alert('ok') }) // 绑定点击事
$('#app') // 获取 div
.text('hello world') // 设置文本内容
.on('click', () => { alert('ok') }) // 绑定点击事件
<div @click="alert('ok')">hello world</div>
性能与可维护性
声明式与命令式各有优劣,在设计框架时,还需要权衡性能与可维护性。这里可以先说出结论:
结论
声明式代码的性能不优于命令式代码的性能。
举个例子,想要修改一个 div
的内容,用命令式代码实现只需要一行代码。而且这个代码是性能最好的代码,没有优化的空间了。而用声明式代码想要实现最优的性能,需要先找到前后的差异变化,再更新变化的部分。但是最终完成的还是修改文本的那行代码。
div.textContent = 'hello world'
<div @click="alert('ok')">hello world</div>
<div @click="alert('ok')">hello vue3</div>
<!-- 实际上等同于 -->
div.textContent = 'hello vue3'
如果把直接修改所消耗的性能设为 A,找出差异所消耗的性能设为 B,那么命令式代码的性能为 A,而声明式代码的性能为 A + B。
因此可以得出结论,找出差异所消耗的性能越少,声明式代码的性能消耗就越接近命令式代码的性能消耗,但肯定无法超越。也就有上面的结论:声明式的性能不优于命令式代码的性能。
而声明式代码的优处在于更好的可维护性,因此,在框架的设计中,需要权衡性能与可维护性,在采取声明式代码的可维护性同时,尽可能降低找出差异的性能消耗。
虚拟 DOM
前文提到过声明式代码的性能消耗 = 找出差异性能消耗 + 直接修改性能消耗,而虚拟 DOM 的设计目的就是为了减少找出差异的性能消耗。
下面来对比一下虚拟 DOM 和原生 document.createElement
、innerHTML
的性能消耗对比:
性能比较 创建页面时:innerHTML 和虚拟 DOM 性能差距不大,都需新建所有 DOM 元素。 更新页面时:
- innerHTML:需销毁所有旧 DOM 新建所有新 DOM 重新构建 HTML 字符串,性能消耗大,且页面越大更新性能越差.
- 虚拟 DOM:只需更新变化的内容,JavaScript 层面多出 Diff 性能消耗,但 DOM 层面性能优势明显,且不受页面大小影响.
心智负担与可维护性
- 原生 DOM 操作:心智负担最大,需手动处理大量 DOM 元素,代码可维护性差.
- innerHTML:有一定心智负担,拼接 HTML 字符串且需处理事件绑定等,模板大时更新性能差.
- 虚拟 DOM:声明式,心智负担小,可维护性强,性能在保证心智负担和可维护性的前提下表现不错.
性能因素
- 虚拟 DOM:更新页面时性能因素主要与变化内容有关,不受页面大小影响.
- innerHTML:更新页面时性能因素与页面大小密切相关,页面越大更新性能越差.
维度 | 原生 DOM 操作 | innerHTML | 虚拟 DOM |
---|---|---|---|
心智负担 | 最大 | 中等 | 最小 |
可维护性 | 最差 | 较差 | 最好 |
创建页面性能 | 最高 | 高 | 高 |
更新页面性能 | 最高(需优化) | 最差 | 较好 |
运行时与编译时
设计框架时有三种选择:纯运行时、运行时 + 编译时、纯编译时。选择取决于框架特征和期望,需了解运行时和编译时的特征及其对框架的影响.
- 特点:没有编译过程,用户直接提供数据对象进行渲染.
- 示例代码:javascript
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) { obj.children.forEach((child) => Render(child, el)); } root.appendChild(el); } const obj = { tag: 'div', children: [{ tag: 'span', children: 'hello world' }] }; Render(obj, document.body);
- 优点:用户使用简单,无需学习额外知识.
- 缺点:无法分析用户内容,优化有限;手写数据对象麻烦且不直观.
运行时 + 编译时框架
- 思路:引入编译手段,将 HTML 标签编译成数据对象,再用 Render 函数渲染.
- 示例代码:javascript
// Compiler 函数将 HTML 字符串编译成数据对象 function Compiler(html) { // 编译逻辑,返回数据对象 } const html = ` <div> <span>hello world</span> </div> `; const obj = Compiler(html); Render(obj, document.body);
- 优点:支持运行时和编译时,用户可直接提供数据对象或 HTML 字符串;编译时可分析内容优化性能.
- 缺点:运行时编译有性能开销,但可在构建时编译以优化性能.
纯编译时框架
- 思路:编译器将 HTML 字符串直接编译成命令式代码,无需 Render 函数.
- 示例代码:javascript
// Compiler 函数将 HTML 字符串编译成命令式代码 function Compiler(html) { // 编译逻辑,返回命令式代码字符串 } const html = ` <div> <span>hello world</span> </div> `; const code = Compiler(html); eval(code); // 执行编译后的命令式代码
- 优点:性能可能更好,因为直接编译成可执行代码.
- 缺点:灵活性差,用户内容必须编译后才能使用;实际性能可能达不到理论高度.
总结
- 纯运行时框架简单易用但优化有限,运行时 + 编译时框架在灵活性和性能优化上取得平衡,纯编译时框架性能可能更好但灵活性差.
- Vue.js 3 保持运行时 + 编译时架构,在保留运行时灵活性的同时,通过编译优化实现高性能,甚至不输纯编译时框架.
总结
在框架设计中,需权衡命令式与声明式编程范式。命令式代码直观但维护性差,声明式代码维护性好但性能略逊,因需先找出差异再更新。虚拟 DOM 减少差异查找消耗,使声明式性能接近命令式,且在更新页面时,虚拟 DOM 只更新变化部分,性能优势明显,同时具备低心智负担和高可维护性.
在运行时与编译时选择上,纯运行时框架简单但优化有限,运行时 + 编译时框架兼顾灵活性与性能优化,纯编译时框架性能佳但灵活性差. Vue.js 3 采用运行时 + 编译时架构,通过编译优化实现高性能,平衡了性能与可维护性.