尤雨溪自述:打造Vue 3背后的故事

做者 | 尤雨溪
译者 | 王强
编辑 | 蔡芳芳前端

尤雨溪在今年年初 Vue 3 正式发布以前撰写了这篇长文,详述 Vue 3 的设计过程。前端之巅将全文翻译以下,但愿能帮助你更好地了解 Vue 3 背后的故事。vue

在过去的一年中,Vue 团队一直都在开发 Vue.js 的下一个主要版本,咱们但愿能在今年上半年发布它(本文完成时这项工做尚在进行)。Vue 新版本的理念成型于 2018 年底,当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比了。git

在咱们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题。github

为何重写

利用新的语言特性

随着 ES2015 标准的落地,JavaScript(之前被称为 ECMAScript,缩写为 ES)得到了诸多重大改进,同时主流浏览器也终于开始对这些新特性提供良好的支持了。其中的一些特性使咱们可以大幅提高 Vue 的能力。算法

这里面最值得一提的就是 Proxy:后端

developer.mozilla.org/en-US/docs/…api

它为框架提供了拦截针对对象的操做的能力。Vue 的一项核心特性就是监听用户定义状态的变化,并响应式更新 DOM。Vue 2 是经过替换状态对象属性的 getter 和 setter 来实现这种响应能力的。转向 Proxy 后,咱们就能解决 Vue 当下存在的诸多局限(好比没法检测新增属性等),还能提供更好的性能。数组

但 Proxy 是一个原生的语言特性,没法在老式浏览器中提供完整的 polyfill。为此咱们须要改动新版框架的浏览器支持范围——这是一项破坏性变动,只有新的主要版本才能实现。浏览器

解决架构问题

在现有代码库上修复这些问题须要大量高风险的重构工做,几乎等同于重写了。前端框架

在维护 Vue 2 的过程当中,咱们积累的不少问题受限于现有的架构是很难解决的。例如,模板编译器的写法使咱们很难实现良好的源映射支持。另外,虽然 Vue 2 技术上支持构建以非 DOM 平台为目标的高级渲染器,但为了实现这一支持,咱们须要 fork 代码库,还得复制一大堆代码。在现有代码库上修复这些问题须要大量高风险的重构工做,几乎已经等同于重写了。

同时,咱们在不少内部模块与看起来无处可去的零散代码之间生成了不少隐藏的耦合关系,结果积累了很多技术债。如今咱们很难单独理解代码库中某一部分的含义,并且咱们也注意到贡献者们不多有信心作出突破性的更改。经过重写,咱们得以基于这些问题从新思考代码的组织方式。

早期的原型阶段

咱们是从 2018 年底开始建立 Vue 3 的原型的,主要目标是验证针对上述问题的解决方案。在这一阶段,咱们主要是为后续的开发工做打下牢固的基础。

转向 TypeScript

Vue 2 最初是用纯粹的 ES编写的。原型阶段开始后不久,咱们意识到对于这么大规模的项目来讲,类型系统会很是有用。类型检查能够大幅下降在重构中引入意外 bug 的概率,也能提高贡献者在作出突破性更改时的信心。咱们采用了 Facebook 的 Flow 类型检查器,由于它能够渐进添加到一个现有的纯 ES 项目中。Flow 起了必定做用,但咱们的收益不及预期;特别是它的重大更改太多了,升级起来至关痛苦。它对集成开发环境的支持也不如 TypeScript 与 VS Code 的深度集成水平。

咱们还注意到愈来愈多的用户在结合使用 Vue 和 TypeScript。为了支持他们的使用场景,咱们须要在源码以外单独编写和维护一套 TypeScript 声明,其使用了另外一套类型系统。转向 TypeScript 后,咱们就能自动生成声明文件,下降维护成本。

解耦内部包

咱们还采用了一个单体仓库方案,其中框架是由众多内部包组成的,每一个包都有本身独立的 API、类型定义和测试用例。咱们想让各个模块间的依赖关系更明显,让开发人员更容易阅读、理解和修改全部这些依赖项。这是咱们下降项目贡献门槛,提高其长期可维护性的关键举措。

制定 RFC 流程

2018 年底,咱们有了一个带有新的响应系统和虚拟 DOM 渲染器的原型。咱们验证了计划中的内部架构优化,但只是粗略起草了面向外部的 API 更改想法。如今该将这些想法转变为具体的设计了。

咱们知道这一步要在早期谨慎进行。Vue 的普遍流行意味着重大更改可能会给用户带来巨大的迁移成本,还可能让生态碎片化。为了让用户对重大更改提交反馈,咱们在 2019 年初制定了一套 RFC(征求意见)流程。

github.com/vuejs/rfcs

全部 RFC 都有一个模板,包括动机、设计细节、权衡以及采用策略等内容。这套流程的实现形式,是在一个 Github 仓库上将提案提交成拉取请求,这样天然就能够在评论中讨论提案了。

结果代表这个 RFC 流程很是有用。做为一个思惟框架,它强制咱们全面考虑一个潜在更改的全部层面,并让整个社区能够参与到设计过程当中,并提交通过充分思考的特性需求。

更快,更小

前端框架的性能相当重要。

前端框架的性能相当重要。尽管 Vue 2 已经提供了颇具竞争力的性能表现,但此次重写让咱们有机会试验新的渲染策略来进一步提高性能。

突破虚拟 DOM 的瓶颈

Vue 有一套独特的渲染策略:它提供了一个类 HTML 的模板语法,但将模板编译成了一个返回虚拟 DOM 树的渲染函数。框架会递归遍历两个虚拟 DOM 树,对比每一个节点的全部属性来判断该更新 DOM 的哪些部分。这种相对暴力的算法通常仍是很快的,这要感谢现代 JS 引擎实现的那么多高级优化措施。可是更新过程仍是会涉及不少没必要要的 CPU 工做。当你的模板存在大量静态内容,却只有少许动态绑定时,更新的效率就会显得尤其低下——仍是要递归遍历整个虚拟 DOM 树,才能找出要更新的部分。

所幸模板编译这一步让咱们能够对模板进行静态分析,并提取动态部分的信息。Vue 2 跳过了静态子树,在必定程度上作到了这一点;可是因为过分简化的编译器架构,更高级的优化就很难实现了。在 Vue 3 中咱们重写了编译器,加入了一个合适的 AST transform 管道,让咱们能以 transform 插件的形式进行编译时优化。

如今有了新的架构,咱们想要找到一个尽量减小额外开销的渲染策略。一个选项是抛弃虚拟 DOM 并直接生成命令式 DOM 操做,但这会失去直接编写虚拟 DOM 渲染函数的能力,咱们发现这是对于高级用户和库做者们很是有价值的能力。此外,这也会是一个影响巨大的重大更改。

接下来的选项就是摆脱没必要要的虚拟 DOM 树遍历和属性对比,这也是更新过程当中性能开销最大的部分。为此,编译器和运行时须要协同工做:编译器分析模板,生成带有优化线索的代码,而运行时获取线索并选择最快路径。这里有三大优化工做:

首先,在树级别,咱们注意到没有动态调整节点结构的模板指令(如 v-if 和 v-for)时,节点的结构彻底保持静态。若是咱们将模板根据这些结构化指令拆分为一些嵌套"块",每个块中的节点结构也会保持静态。当咱们更新一个块中的节点时,就没必要再递归遍历整个树了——块内的动态绑定能够在一个平面数组里追踪。这一优化极大减小了须要遍历的树的数量,规避了大部分虚拟 DOM 树开销。

其次,编译器会激进检测模板中的静态节点、子树甚至数据对象,并在生成的代码中将它们提取到渲染函数以外。这就能够避免在每次渲染时从新建立这些对象,大幅减小了内存占用,并减小了垃圾收集的频率。

最后,在元素级别,编译器会为每个有动态绑定的元素,根据其须要进行的更新类型生成一个优化标志。好比说一个元素有一个动态的 class 绑定和一些静态属性,它会得到一个标志,表示这里只须要进行 class 检查。运行时会获取这些标志,而后选择最快的路径。

CPU 时间:是指 JavaScript 运算所消耗的时间,不包括浏览器 DOM 操做所用的时间。

结合这些优化,咱们的渲染更新速度得到了显著改进,在某些场景下 Vue 3 的 CPU 时间 仅为 Vue 2 的十分之一不到。

缩小包体积

框架的大小也会影响其性能。这是 Web 应用程序特有的现象,由于资产须要在线下载,而应用须要等到浏览器解析完必要的 JavaScript 代码后才能开始交互。单页面应用程序在这方面的矛盾尤其明显。尽管 Vue 一直以来都是相对轻量级的框架——Vue 2 的运行时大小为 23KB(gzip 压缩后),咱们仍是注意到了两个问题:

首先,不是全部人都须要框架的所有功能。例如,历来不须要过渡特性的应用仍是须要下载和解析相关代码。

另外,咱们在不断给框架增长新特性,框架也在不断变大,没有止境。这样咱们在权衡新特性的利弊时,就得很是在乎包大小这个权重。结果,咱们会倾向于只加入那些大多数用户都会用到的特性。

理想状态下,用户能够在构建时去掉框架中本身不须要的特性(也就是"摇树优化"),只保留本身用到的特性。这样咱们在添加只有部分用户会用到的特性时,并不会给其余用户增添应用体积的负担。

在 Vue 3 中,咱们把大多数全局 API 和内部 helper 移到了 ES 模块导出中,从而实现了这个目标。这样现代的打包器就能够静态分析模块依赖项,并去掉与未使用导出相关的代码。模板编译器也会生成适合摇树优化的代码,只会对模板确实用到的特性导入 helper。

框架的有些部分是永远没法摇树优化的,由于它们对于全部应用类型来讲都很重要。咱们将这部分没法舍弃的代码的体积称做基线大小。虽然 Vue 3 增长了不少新特性,但其基线大小只有大约 10KB(gzip 后),不到 Vue 2 的一半。

知足扩展需求

咱们还想改善 Vue 应对大规模应用程序的能力。咱们最初设计 Vue 时主要想的是下降入门门槛并平滑学习曲线。但随着 Vue 越发流行,咱们也看到了愈来愈多的项目需求随着时间推移不断扩大,后期甚至包含数以百计的模块,须要几十名开发人员来维护。对于这种类型的项目,TypeScript 这样的类型系统和能够提供组织清晰、易于复用的代码的能力是必不可少的,但 Vue 2 在这些方面的支持水平不甚理想。

在 Vue 3 的早期设计阶段,咱们尝试内置对使用 class 编写组件的支持,从而更好地整合 TypeScript。这里的问题在于,为了让 class 可用而须要的不少语言特性(例如 class fields 和 decorators)都还处在提案阶段,有可能在正式版中出现变化。随之而来的复杂性和不肯定性让咱们开始质疑 Class API 是否真的合适,由于它只能改善一点 TypeScript 的整合能力而已。

因而咱们决定探索其余途径来解决扩展问题。受到 React Hooks 的启发,咱们想到了暴露底层的响应式和组件生命周期的 API,从而提供一种更灵活地编写组件逻辑的方式,也就是 Composition API。

vue-composition-api-rfc.netlify.com/

Composition API 再也不须要用一个长长的配置列表定义组件,它容许用户自由定义、组合和重用组件逻辑,就像写函数同样,同时还能提供完善的 TypeScript 支持。

咱们很是喜欢这个想法。尽管 Composition API 是为解决特定类型的问题设计的,但也能用在单纯的组件开发中。在提案的初稿中咱们有些得意忘形,暗示咱们可能会在将来的版本中用 Composition API 替换掉当前的 Options API。这引发了社区成员的极大反弹,给咱们上了重要的一课,让咱们认识到了与社区沟通长期计划和发展方向,以及理解用户需求的重要性。在听取社区反馈以后,咱们彻底重作了提案,确认 Composition API 只是锦上添花,是 Options API 的补充。新版提案的反馈要正面许多,咱们还收到了不少建设性的意见。

把握平衡

开发人员的多样性意味着使用场景的多样性。

现在有超过一百万的开发人员在使用 Vue,其中有只懂一点 HTML/CSS 的新手,从 jQuery 一路走来的专家,从其余框架迁移过来的老鸟,在寻找前端解决方案的后端工程师,还有负责设计大规模软件的架构师。开发人员的多样性意味着使用场景的多样性:有的开发人员可能想要提高旧项目的交互体验,另外一些人可能想要快速开发低成本的一次性项目;架构师可能要应对规模巨大的长期项目,以及项目生命周期内的开发团队成员变更。

Vue 的设计在不断根据这些需求变化和发展,咱们也设法从诸多权衡中找到平衡点。Vue 的口号“渐进式框架”,背后就是这个过程当中造成的分层 API 设计。新手能够经过 CDN script、基于 HTML 的模板以及直观的 Options API 顺利学习入门。而专家能够经过全功能的 CLI、渲染函数以及 Composition API 来处理复杂需求。

要实现咱们的愿景还有不少工做要作,其中最重要的就是更新支持库、文档和工具,以保证平滑的迁移。咱们会在将来的几个月中继续努力,并且咱们火烧眉毛想要看到社区能用 Vue 3 创造怎样的精彩了。

做者介绍

尤雨溪是 Vue.js 框架的建立者和项目领导,也是一位独立开源开发者。

原文连接

increment.com/frontend/ma…

本做品系 转载阅读原文

相关文章
相关标签/搜索