Vue3 的诞生之路

目录

本文已收录在Github github.com/Geekhyt ,感谢Star。

原文地址:https://increment.com/frontend/making-vue-3/

译者:童欧巴

因时间有限,文中翻译不对的地方还请指出,望海涵。想感受原汁原味还请移步上方链接。致敬尤大!

Vue3 的诞生之路

EVAN YOU

重写 Vue.js 下一个主要版本的经验

在过去的一年中,Vue 团队一直在研究 Vue.js 的下一个主要版本,我们希望在 2020 年上半年发布该版本。(在撰写本文时,这项工作仍在进行中。)Vue 的新的主要版本的构想形成于 2018 年年末,当时 Vue 2 的代码库已经两年半大了。在通用软件的生命周期中听起来可能并不长,但是在此期间,前端环境发生了翻天覆地的变化。

有两个主要的因素促使我们开发(重写) Vue 新的主要版本:首先是主流浏览器普遍支持了新的 JavaScript 语言特性。其次,随着时间的推移,当前代码库中的设计和架构问题已经暴露出来。

为什么要重写?(重写初衷)

利用新的语言特性

随着 ES2015 的标准化,JavaScript(正式称为 ECMAScript ,缩写为 ES)得到了重大改进,主流浏览器终于开始为这些新特性提供良好的支持。特别是其中一些新特性为我们极大提高 Vue 的性能提供了机会。

其中最值得关注的是 Proxy,它允许框架拦截对象上的操作。Vue 的核心功能是能够侦听用户定义的状态变更并响应式的更新 DOM。Vue2 是通过使用 getter 和 setter 替换响应式对象上的属性来实现的这个功能。换作使用 Proxy 能够消除 Vue 现存的一些限制,例如无法检测到新的属性添加并为其提供更好的性能。

然而,Proxy 是语言原生的特性,在旧版本的浏览器中不能被完全的支持。为了使用它,我们意识到必须调整框架的浏览器支持范围,这是一个突破性的变化 (breaking change),也只能在新的主要版本中发布。

解决架构问题

在当前代码库汇总解决这些问题,重构的风险较大,几乎等同于重写

在维护 Vue 2 的过程中,由于现有架构的限制,我们积累了许多难以解决的问题。例如,模板编译器的编写方式使适当的 source-map 支持非常具有挑战性。同样,虽然 Vue 2 从技术上允许构建针对非 DOM 平台的更高级别的渲染器,但我们必须 fork 代码库并复制大量代码,才能实现这一点。要在当前代码库中解决这些问题,重构的风险很大,几乎等同于重写。

同时,我们在各种模块的内部与浮动代码之间的隐式耦合上积累了技术债,而浮动代码 (floating code) 似乎并不属于任何地方。这使得单独理解代码库的一部分变得更加困难,并且我们注意到,贡献者对重要的更改很没有信心,重写将使我们有机会牢记这些注意事项来重新考虑代码组织。

初始原型阶段

我们于 2018 年底开始对 Vue 3 进行原型设计,其初步目标是验证这些问题的解决方案的可行性。在此阶段,我们主要致力于为进一步发展奠定坚实的基础。

切换到 TypeScript

Vue 2 最初是用纯 ES 编写的。在进入原型开发阶段之后不久,我们意识到类型系统对于这种规模的项目将会非常有帮助。类型检查极大地减少了在重构期间引入意外错误的机会,并有助于贡献者更自信地进行重要的更改。我们采用了 Facebook 的 Flow 类型检查器,因为它可以逐渐添加到现有的 Plain-ES 项目中。 Flow 在一定程度上有所帮助,但是我们没有从中获得我们希望的最大收益。特别是不断的变化使升级变得很痛苦。与 TypeScript 和 Visual Studio Code 之间的深度集成相比,对集成开发环境的支持也不理想。

我们还注意到,用户越来越多地同时使用 Vue 和 TypeScript。为了支持它们的用例,我们在不同的类型系统下,必须与源代码分开编写和维护 TypeScript 声明。切换到 TypeScript 将使我们能够自动生成声明文件,从而减轻了维护负担。

解耦内部封装

我们还采用了 monorepo 设置框架的内部 packages 构成,每个 package 都具有自己的独立 API,类型定义和测试用例。我们希望使这些模块之间的依赖关系更加明确,从而使开发人员更容易阅读、理解并进行所有更改。这是我们努力降低该项目的贡献障碍并提高其长期可维护性的关键。

设置 RFC 流程

到 2018 年底,我们有了一个使用新的响应式系统和虚拟 DOM 渲染器的工作原型。我们已经验证了我们想要进行的内部体系结构改进,但是只包含了面向公众的 API 更改的草稿 (drafts)。是时候将它们变成具体的设计了。

我们知道我们必须未雨绸缪。 Vue 的广泛使用意味着突破性变化 (breaking changes) 可能导致用户大量迁移成本和潜在的生态系统碎片化。为了确保用户能够提供有关重大更改的反馈,我们于 2019 年初采用了 RFC(征求意见)流程。每个 RFC 遵循一个模板,各章节重点介绍动机、设计细节以及取舍和权衡策略。由于此过程是在 GitHub 仓库中进行的,提案通过 pull requests 形式,因此讨论在评论中 (comments) 自然展开。

事实证明,RFC 流程非常有用,它是一个使我们能够充分考虑潜在变更的全方位的思想框架,并且允许我们的社区参与设计过程并提交经过深思熟虑的feature requests。

更快更小

性能对于前端框架至关重要

性能对于前端框架至关重要。尽管 Vue 2 具有出色的性能,但通过尝试新的渲染策略重写,提供了进一步提升性能的机会。

克服虚拟 DOM 的瓶颈

Vue有一个相当独特的渲染策略:它提供了类似于 HTML 的模板语法,但是将模板编译为可返回虚拟 DOM 树的渲染函数 (render function)。该框架通过递归遍历两个虚拟 DOM 树并比较每个节点上的每个属性来确定真实 DOM 的哪些部分需要更新。得益于现代 JavaScript 引擎执行的高级优化,因此这种有点蛮力的算法通常很快,但是更新仍然涉及许多不必要的 CPU 工作。当您查看的模板中大部分是静态内容并且只有少量动态绑定时,效率低下尤其明显-整个虚拟 DOM 树仍需要递归遍历以找出更改之处。

幸运的是,模板编译步骤使我们有机会对模板进行静态分析并提取有关动态部分的信息。Vue 2 通过跳过静态子树在某种程度上做到了这一点,不过由于编译器体系结构过于简单,难以进行更高级的优化。在 Vue 3中,我们使用适当的 AST 转换 pipeline 重写了编译器,这使我们能够以转换插件的形式编写编译时的优化。

有了新的体系结构,我们希望找到一种尽可能减少开销的渲染策略。一种选择是放弃虚拟 DOM 并直接生成命令式 DOM 操作,但这将消除直接编写虚拟 DOM 渲染功能的能力 (render functions),我们发现该功能对于高级用户和库的作者非常有价值。另外,这将是一个突破性的变化 (breaking change)。

其次,最佳的方案是消除不必要的虚拟 DOM 树遍历和属性比较,这在更新过程中往往会带来最大的性能开销。为了实现这一点,编译器 (compiler) 和运行时 (runtime) 需要协同工作:编译器分析模板并生成带有优化提示的代码 (hints),而运行时将选择这些提示的代码并在可能的情况下采用快速路径 (fast paths)。这里有三个主要的优化工作:

首先,在树级别,我们注意到,在没有模板指令的情况下节点结构保持完全静态,模板指令动态地改变了节点结构(例如:v-if 和 v-for)。如果我们将模板划分为由这些结构指令分隔的嵌套“块”,则每个块内的节点结构将再次变得完全静态。当我们更新一个块中的节点时,我们不再需要递归遍历该树-可以在平面数组中追踪该块内的动态绑定。这种优化通过将我们需要执行的树遍历数量减少了一个数量级,从而避免了虚拟 DOM 的大部分开销。

其次,编译器会主动检测模板中的静态节点、子树甚至数据对象,并将其提升到生成代码中的 render 函数之外。这样可以避免在每次渲染上重新创建这些对象,从而大大提高了内存使用率并减少了垃圾回收的频率。

第三,在元素级别,编译器还会根据需要执行的更新类型为具有动态绑定的每个元素生成一个优化标志。例如,具有动态类绑定和许多静态属性的元素将收到一个标志,该标志指示仅用于类型检查。运行时将获取这些提示并采用专用的快速路径。

综上所述,这些技术大大改善了我们的渲染更新基准 (benchmarks),有时 Vue 3 占用的 CPU 时间不到 Vue 2 的十分之一。

(CPU 时间,即执行 JavaScript 计算所花费的时间,不包括浏览器 DOM 操作。)

最小化包体积

框架的大小也会影响其性能。这是 Web 应用程序的唯一关注点,因为需要即时下载资源,在浏览器解析必要的 JavaScript 之前该应用程序是不可交互的。对于单页应用程序尤其如此。尽管 Vue 一直是相对轻量级的(Vue 2 的运行时大小压缩为 23 KB),但我们注意到了两个问题:

首先,并不是每个人都使用框架的所有功能。例如,一个从不使用 transition 功能的应用仍需下载与 transition 相关的代码并解析。

其次,当我们添加新功能时,该框架会无限期地增长。当新功能添加时,我们不得不为包的尺寸比例折衷考虑。因此,我们倾向于框架仅包含大多数用户使用的功能。

理想情况下,用户应该能够在构建时删除未使用的框架功能的代码(也称为“tree-shaking”),并且只打包使用的代码部分。这也将使我们能够发布一部分用户会觉得有用的功能,而不会增加其余用户的成本。

在 Vue 3 中,我们通过将大多数全局 API 和内部帮助程序移至 ES 模块导出来,实现了这一目标。这使现代的打包工具可以静态分析模块依赖性并删除未使用的导出相关的代码。模板编译器还会生成友好的 Tree-shaking 代码,在模板中实际使用了该功能时才导入该功能的帮助程序。

框架的某些部分永远不会 Tree-shaking,因为它们对于任何类型的应用都是必不可少的。我们将这些必不可少的部分的度量标准称为基准尺寸。尽管增加了许多新功能,但 Vue 3 的基准大小压缩后约为 10 KB,不到 Vue 2 的一半。

解决规模需求

我们还希望提高 Vue 处理大型应用程序的能力。我们最初的 Vue 设计着重于降低入门门槛以及温和的学习曲线。但是随着 Vue 越来越广泛地被应用,我们了解到了更多有关项目需求的信息,这些项目包含数百个模块,并且随着时间的流逝由数十名开发人员维护。对于这些类型的项目,像 TypeScript 这样的类型系统以及可复用代码的能力至关重要,而 Vue 2 在这些领域的支持并不理想。

在设计 Vue 3 的早期阶段,我们尝试通过提供对使用类编写组件的内置支持来改善 TypeScript 集成。挑战在于,我们需要使类可用的许多语言特性(如类字段和装饰器)仍是提案,并且在正式成为 JavaScript 一部分之前可能会发生变化。其涉及的复杂性和不确定性使我们对添加 Class API 是否真的合理这件事产生怀疑,因为它除了提供 TypeScript 集成稍好之外并无其他功能。

我们决定研究其他解决扩展问题的方法。受 React Hooks 的启发,我们考虑过公开较低级别的响应式和组件生命周期 API,以实现一种更自由的编写组件逻辑的方式,称为 Composition API。无需通过指定一长串选项来定义组件,Composition API 允许用户像编写函数一样自由表达、组合和复用有状态组件逻辑,同时还提供了出色的 TypeScript 支持。

我们对这个想法感到非常兴奋。尽管 Composition API 旨在解决特定类别的问题,但从技术上讲,仅在编写组件时才可以使用它。在该提案的初稿(first draft)中,我们预先暗示可能会在将来的版本中将现有的 Options API 替换为 Composition API。这导致了社区成员的大量反对,这使我们获得了宝贵的经验,即如何清楚地传达长期计划和意图,以及更好地理解用户的需求。在听取了我们社区的反馈之后,我们对提案进行了完全的重新设计,从而明确表明 Composition API 将是 Options API的补充和附加。修改后的提案更为积极,并收到了许多建设性的建议。

寻求平衡

开发人员的配置文件多样性与用例的多样性相对应

Vue 拥有超过一百万的开发人员,有只了解 HTML / CSS 的基础知识的初学者、从 jQuery 迁移的专业人员、从另一个框架迁移的资深人士、正在寻找前端解决方案的后端工程师以及处理大规模软件的架构师。开发人员配置文件的多样性与用例的多样性相对应:一些开发人员可能希望将交互性扩展到旧版应用程序上,而另一些开发人员则可能正在快速处理但维护需求有限的一次性项目中工作;在项目的整个生命周期中,架构师可能不得不应对大型的、多年的项目以及不断变化的开发商团队。

在我们平衡各种因素的同时,Vue 的设计不断受到这些需求的影响,并不断适应这些需求。Vue 的口号是“渐进式框架”,封装了此过程中产生的分层 API 设计。初学者可以使用 <script> 引入 CDN,基于 HTML 的模板和直观的 Options API 来享受流畅的学习曲线,而专家可以使用功能齐全的 CLI,render functions 和 Composition API 处理大型复杂的项目。

要实现我们的愿景,还有许多工作要做-最重要的是,更新周边生态库、文档和工具以确保顺利迁移。在接下来的几个月中,我们将继续努力,我们迫不及待地想看看社区将通过 Vue 3 创造出什么。