《Vue.js设计与实现》心得与总结 01:框架设计概览

401 阅读7分钟

权衡的艺术

Vue.js 是一个声明式描述 UI 的框架。之所以采用声明式的,是出于代码的可维护性考虑。

声明式的代码更加关注结果,命令式的代码关注过程。

声明式的代码虽然可维护性较高,但其性能并不优于命令式的代码。为了保持可维护性的同时让性能损失最小化,Vue.js 引入了虚拟 DOM 。

声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗。虚拟 DOM 的意义就在于使找出差异的性能消耗最小化。用原生 JavaScript 操作 DOM 的方法(如 document.createElement)、虚拟 DOM 和 innerHTML 三者操作页面的性能,不可以简单地下定论认为谁的性能最好,因为这与页面大小变更部分的大小都有关系,除此之外,与创建页面还是更新页面也有关系,选择哪种更新策略,需要我们结合心智负担可维护性等因素综合考虑。所以经过一番权衡之后,Vue.js 引入了虚拟 DOM 。

一个框架可以是纯运行时的,也可以是纯编译时的,还可以是既支持运行时又支持编译时的。

所谓纯运行时的框架,就是它没有编译的过程,源代码可以直接运行,在程序运行时根据特定的规则动态地创建和配置对象,核心思想是将程序的逻辑分离到可配置的元数据中,以便在运行时进行修改,这使得在不重新编译代码的情况下,可以修改程序的行为、添加新功能或调整配置。相对来说比较灵活。

所谓纯编译时的框架,就是它的源码需要编译后才能运行,由于加入了编译步骤,可以在编译的时候分析用户提供的内容,看看哪些内容未来可能会改变,哪些内容永远不会改变,根据这些信息做进一步的优化,所以纯编译时的框架性能往往比较好。但是这种做法有损灵活性。

Vue3 为了保持灵活性的基础上能够尽可能地去优化性能,选择了编译时 + 运行时的架构。因此 Vue3 是一个编译时 + 运行时的框架。

综上,可以发现在框架的设计中,需要权衡各种因素与场景,在综合考虑各种因素后,才能得到适合的选择。“框架设计里到处都体现了权衡的艺术。”

框架设计的核心要素

框架设计有以下几个核心要素:

  1. 良好的开发体验

  2. 控制好框架代码的体积

  3. 框架要做到良好的 Tree-Shaking

  4. 框架应该输出怎样的构建产物

  5. 特性开关

  6. 错误处理

  7. 良好的 TypeScript 类型支持

开发体验是衡量一个框架的重要指标之一。提供友好的警告信息至关重要,这有助于开发者快速定位问题,因为大多数情况下“框架”要比开发者更清楚问题出在哪里,因此在框架层面抛出有意义的警告信息是非常必要的。

但提供的警告信息越详细,就意味着框架体积越大。因此,为了框架体积不受警告信息的影响,我们需要利用 Tree-Shaking 机制,配合构建工具预定义常量的能力,例如预定义 __DEV__ 常量,从而实现仅在开发环境中打印警告信息,而生产环境中则不包含这些用于提升开发体验的代码,从而实现线上代码体积的可控性。

Tree-Shaking 是一种排除 dead code 的机制,框架中会内建多种能力,例如 Vue.js 内建的组件等。对于用户可能用不到的能力,我们可以利用 Tree-Shaking 机制使最终打包的代码体积最小化。另外,Tree-Shaking 本身基于 ESM,并且 JavaScript 是一门动态语言,通过纯静态分析的手段进行 Tree-Shaking 难度较大,因此大部分工具能够识别/*#__PURE__*/ 注释,在编写框架代码时,我们可以利用/*#__PURE__*/ 来辅助构建工具进行 Tree-Shaking。

框架有不同类型的输出产物,不同类型的产物是为了满足不同的需求。为了让用户能够通过 <script> 标签直接引用并使用,我们需要输出 IIFE 格式的资源,即立即调用的函数表达式。为了让用户能够通过 <script type="module"> 引用并使用,我们需要输出 ESM 格式的资源。这里需要注意的是,ESM 格式的资源有两种:用于浏览器的 esm-browser.js 和用于打包工具的 esm-bundler.js。它们的区别在于对预定义常量 __DEV__ 的处理,前者直接将 __DEV__ 常量替换为字面量 true 或 false,后者则将 __DEV__ 常量替换为 process.env.NODE_ENV !== 'production' 语句。

框架会提供多种能力或功能。有时出于灵活性和兼容性的考虑,对于同样的任务,框架提供了两种解决方案,例如 Vue.js 中的选项对象式 API 和组合式 API 都能用来完成页面的开发,两者虽然不互斥,但从框架设计的角度看,这完全是基于兼容性考虑的。有时用户明确知道自己仅会使用组合式 API,而不会使用选项对象式 API,这时用户可以通过特性开关关闭对应的特性,这样在打包的时候,用于实现关闭功能的代码将会被 Tree-Shaking 机制排除。

框架的错误处理做得好坏直接决定了用户应用程序的健壮性,同时还决定了用户开发应用时处理错误的心智负担。框架需要为用户提供统一的错误处理接口,这样用户可以通过注册自定义的错误处理函数来处理全部的框架异常。

“使用 TS 编写框架和框架对 TS 类型支持友好是两件完全不同的事”。有时候为了让框架提供更加友好的类型支持,甚至要花费比实现框架功能本身更多的时间和精力。

Vue.js 3 的设计思路

Vue.js 的核心设计思路是,首先明确了 Vue.js 是一个声明式的框架,采用声明式地描述 UI 。声明式的好处在于,它直接描述结果,用户不需要关注过程。Vue.js 同时支持模板的方式和虚拟 DOM 来描述 UI 。虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观。

虚拟 DOM 会被渲染器渲染为真实 DOM 元素。渲染器的工作原理是,递归地遍历虚拟 DOM 对象,并调用原生 DOM API 来完成真实 DOM 的创建。渲染器的精髓在于后续的更新,它会通过 Diff 算法找出变更点,并且只会更新需要更新的内容。从而提升框架的渲染性能。

组件的本质就是一组虚拟 DOM 元素的封装,它可以是一个返回虚拟 DOM 的函数,也可以是一个对象,但这个对象下必须要有一个函数用来产出组件要渲染的虚拟 DOM。渲染器在渲染组件时,会先获取组件要渲染的内容,即执行组件的渲染函数并得到其返回值,我们称之为 subtree,最后再递归地调用渲染器将 subtree 渲染出来即可。

Vue.js 的模板会被一个叫作编译器的程序编译为渲染函数,最后再调用渲染函数渲染出真实的 DOM 。编译器、渲染器都是 Vue.js 的核心组成部分,它们共同构成一个有机的整体,不同模块之间互相配合,进一步提升框架性能。