【译】开发 Vue3 的过程
原文标题: The process: Making Vue 3
作者: 尤雨溪 Evan You (Vue.js 作者)
原文链接: https://increment.com/frontend/making-vue-3/
Over the past year, the Vue team has been working on the next major version of Vue.js, which we hope to release in the first half of 2020. (This work is ongoing at the time of writing.) The idea for a new major version of Vue took shape in late 2018, when the codebase of Vue 2 was about two-and-a-half years old. That may not sound like a long time in the life span of generic software, but the frontend landscape had changed drastically during that period.
Two key considerations led us to the new major version (and rewrite) of Vue: First, the general availability of new JavaScript language features in mainstream browsers. Second, design and architectural issues in the current codebase that had been exposed over time.
在过去的一年里,Vue 团队一直在开发下一个大版本的 Vue.js (即 Vue 3.0),预期在 2020 上半年发布(在撰写本文时,这项工作仍在进行当中)。关于新的 Vue 大版本的构想形成于 2018 年底,当时 Vue 2 的代码库大约有两年半的历史。两年半对于一般软件的生命周期来说,可能不算是很长的时间,但在这段时间里,前端环境发生了巨大的变化。
有两个关键的考虑因素催生了新版本的 Vue :一是主流浏览器中 JavaScript 语言新特性的普遍可用性。二是当前代码库中(指 Vue 2.0)随着时间推移暴露出的一些在设计和架构方面存在的问题。
为何要重写
利用语言的新特性
With the standardization of ES2015, JavaScript—formally known as ECMAScript, abbreviated to ES—received major improvements, and mainstream browsers were finally starting to provide decent support for these new additions. Some in particular presented opportunities for us to greatly improve Vue’s capabilities.
The most noteworthy among them is Proxy, which allows the framework to intercept operations on objects. A core feature of Vue is the ability to listen to changes made to the user-defined state and reactively update the DOM. Vue 2 implements this reactivity by replacing the properties on state objects with getters and setters. Switching to Proxy would allow us to eliminate Vue’s existing limitations, such as the inability to detect new property additions, and provide better performance.
However, Proxy is a native language feature that cannot be fully polyfilled in legacy browsers. In order to leverage it, we knew we’d have to adjust the framework’s browser support range—a big breaking change that could only be shipped in a new major version.
随着 ES2015 (ES6) 的标准化,JavaScript(ECMAScript)的能力得到了很大的增强,主流的浏览器终于开始支持 ES 新特性。而其中一些特性使我们得以有机会极大提升 Vue 的性能。其中最值得注意的是 Proxy ,它允许框架拦截对对象的操作。 Vue 的一个核心特性是能够监听开发者定义的状态(即你经常写的 data 、 pros 和 computed,统称为用户状态)的改变,并且响应式更新 DOM 。Vue 2 通过用 Object.defineProperty 中传入的 getter 和 setter 方法重载状态的属性改变来实现响应式更新。切换到 Proxy 能让我们突破 Vue 原有的响应式实现限制,例如无法检测新的属性添加,并且性能也会更好。然而 Proxy 没有办法在过时浏览器(IE,说的就是你)中能够被完全 polyfill (即用旧代码实现新特性的兼容)。这一点经过我们的再三权衡后决定收窄浏览器支持范围,这种破坏性改动只能在下一个大版本中发布。
解决架构问题
Over the course of maintaining Vue 2, we’ve accumulated a number of issues that have been difficult to address due to the limitations of the existing architecture. For example, the template compiler was written in a way that makes proper source-map support very challenging. Also, while Vue 2 technically enables building higher-level renderers that target non-DOM platforms, we had to fork the codebase and duplicate lots of code in order to make this possible. Fixing these issues in the current codebase would require huge, risky refactors that are almost equivalent to a rewrite.
At the same time, we’ve accumulated technical debt in the form of implicit coupling between the internals of various modules and floating code that doesn’t seem to belong anywhere. This made it harder to understand a part of the codebase in isolation, and we noticed that contributors rarely felt confident making nontrivial changes. The rewrite would give us the opportunity to rethink the code organization with these things in mind.
在维护 Vue 2 的过程中,因为现有架构的局限性,我们积攒了很多难以解决的问题。比如 template compiler (Vue 的模板编译器)对于 source-map 的支持成本有点高。再者,Vue2 为了实现针对非 DOM 端(如 mpvue 、weex)启用构建高阶渲染器(renderer 指实现渲染相关的代码,下同)这一功能,我们不得不把代码整个 fork 出来,还留了许多重复代码。在当前的代码中修复这些问题需要大量且高风险的重构,等于重写这个框架。同时也欠下了许多技术债,主要表现在框架内部各个地方出现的一些隐式耦合的、难以归类于某个模块的摇摆代码,造成代码的许多独立的部分理解起来变得更加困难,贡献代码的人们很少能够自信地提供一些有重要的改动。这次重写能让我们抓住机会重新反思了代码组织方式。
原型设计阶段
We started prototyping Vue 3 in late 2018 with the preliminary goal of validating the solutions to these problems. During this stage, we mostly focused on building a solid foundation for further development.
为了初步验证解决方案,我们在 18 年末开始 Vue 3 的原型设计。在这一阶段我们主要集中精力在为将来开发打下坚实基础上。
转用 Typescript
Vue 2 was originally written in plain ES. Shortly after the prototyping stage, we realized that a type system would be very helpful for a project of this magnitude. Type checks greatly reduce the chance of introducing unintended bugs during refactors and help contributors be more confident in making nontrivial changes. We adopted Facebook’s Flow type checker because it can be gradually added to an existing plain-ES project. Flow helped to a certain extent, but we didn’t benefit from it as much as we’d hoped; in particular, the constant breaking changes made upgrading a pain. The support for integrated development environments was also not ideal compared to TypeScript’s deep integration with Visual Studio Code.
We also noticed that users were increasingly using Vue and TypeScript together. To support their use cases, we had to author and maintain the TypeScript declarations separately from the source code, which used a different type system. Switching to TypeScript would allow us to automatically generate the declaration files, alleviating the maintenance burden.
Vue 2 使用原生 ES编写。在原型设计阶段过后 不久,我们意识到类型系统能对于大规模的项目非常有利。类型检查能最大程度减少在重构过程中或者开源贡献者贡献重要改动时产生意外的 bug。我们之前采用的是 Flow,因为它能够渐进地在原生 ES 项目中引入。 Flow 确实有一定程度的帮助,但契合度也没能达到我们的期望。尤其是不断的破坏性改动使升级成为一种痛苦。拿 TypeScript 与 VSCode 的深度集成相比,Flow 对 IDE 的支持也不理想。我们也注意到越来越多的开发者使用 Vue 与 TS 相结合。为了满足他们的场景,我们需要开发和维护一套独立于源码的 TypeScript 声明文件,而转用 TypeScript 允许我们自动生成声明文件,减少维护成本。
解耦内部模块
We also adopted a monorepo setup in which the framework is made up of internal packages, each with their own individual APIs, type definitions, and tests. We wanted to make the dependencies between these modules more explicit, making it easier for developers to read, understand, and make changes to all. This was key to our endeavor to lower the project’s contribution barriers and improve its long-term maintainability.
我们采用了 monorepo 来构建内部模块,每个模块都有单独的 API 、类型定义和单元测试。这么做的目的是为了让每个模块之间的依赖更加清晰,让开发者们阅读代码、理解逻辑和修改更加容易。这是我们努力降低项目贡献门槛、提高长期可维护性的关键。
发起 RFC 流程
By the end of 2018, we had a working prototype with the new reactivity system and virtual DOM renderer. We had validated the internal architectural improvements we wanted to make, but only had rough drafts of the public-facing API changes. It was time to turn them into concrete designs.
We knew we had to do this early and carefully. Vue’s widespread usage means breaking changes can lead to massive migration costs for users and potential ecosystem fragmentation. To ensure users would be able to provide feedback on breaking changes, we adopted an RFC (Request for Comments) process at the beginning of 2019. Each RFC follows a template, with sections focused on motivation, design details, trade-offs, and adoption strategies. Since the process is conducted in a GitHub repo with proposals submitted as pull requests, discussions unfold organically in the comments.
The RFC process has proven immensely helpful, serving as a thought framework that forces us to fully consider all aspects of a potential change, and allowing our community to participate in the design process and submit well-thought-out feature requests.
在 2018 年底,我们有了新的响应式系统和虚拟 DOM 渲染器的原型。尽管已经验证了内核结构性升级的构想,但是的对公众开放的 API 改动信息只有一些简陋的草案。这正是将草案正式落地的时候。我们也清楚,做 RFC 必须非常谨慎, 如今 Vue 的流行度意味着破坏性改动会导致用户或者潜在的生态圈花费大量的成本迁移。为了确保用户能对破坏性改动提出反馈,我们在 2019 年初启用了 RFC (Request for Comments) process 。每一条 RFC 都遵循固定模板:修改动机、设计细节、权衡考虑及采取的策略。由于 RFC 在 Github 仓库中新型,提案以 PR (Pull Request) 的形式提出,使得对于 RFC 讨论也能在评论区中展开来。事实证明:开展 RFC 非常有效。它促使我们对于隐式的改动进行更周全的考虑,也允许社区参与到设计流程并提交更成熟的方案中。
更快更小
Performance is essential to frontend frameworks. Although Vue 2 boasts competitive performance, the rewrite offers an opportunity to go even further by experimenting with new rendering strategies.
对于一个框架来说,性能是非常重要的。尽管 Vue 2 的性能非常优异,但新的渲染策略中对于性能能更上一层楼。
克服虚拟 DOM 的瓶颈
Vue has a fairly unique rendering strategy: It provides an HTML-like template syntax but compiles the templates into render functions that return virtual DOM trees. The framework figures out which parts of the actual DOM to update by recursively walking two virtual DOM trees and comparing every property on every node. This somewhat brute-force algorithm is generally pretty quick, thanks to the advanced optimizations performed by modern JavaScript engines, but updates still involve a lot of unnecessary CPU work. The inefficiency is particularly obvious when you look at a template with largely static content and only a few dynamic bindings—the whole virtual DOM tree still needs to be recursively walked to figure out what’s changed.
Vue 具有相当独特的渲染策略:提供了类 HTML 的模板语法,可以编译成一个返回虚拟 DOM 树的渲染函数 (译者注:模板字符串编译成 JS 函数表达式)。Vue 框架能通过递归比对两颗虚拟 DOM 树的差异,计算出哪一部分的真实 DOM 需要更新。由于现代 JavaScript 引擎比较给力,这种略暴力的算法总体来说并不慢,但是也造成了不少不必要的 CPU 计算,尤其是对于一个由绝大部分静态内容和极少数动态绑定的大型模板来说由非常明显的性能短板。因为整个 DOM 树仍然需要递归地算出哪一部分需要更新。
Luckily, the template compilation step gives us a chance to perform a static analysis of the template and extract information about dynamic parts. Vue 2 did this to some extent by skipping static sub-trees, but more advanced optimizations were difficult to implement due to the overly simplistic compiler architecture. In Vue 3, we rewrote the compiler with a proper AST transform pipeline, which allows us to compose compile-time optimizations in the form of transform plug-ins.
幸运的是,我们能在模板编译这一阶段进行静态分析,并抽离动态的部分。虽然 Vue 2 做了一些跳过一些静态子节点的优化操作,但是这过于简单粗暴,对于一些更进一步的优化难以实现。在 Vue 3.0 中,我们重写了一种能结合编译期优化的 AST (Abstract Syntax Tree 抽象语法树)转换管道插件。
With the new architecture in place, we wanted to find a rendering strategy that would eliminate as much overhead as possible. One option was to ditch virtual DOM and directly generate imperative DOM operations, but that would eliminate the ability to directly author virtual DOM render functions, which we’ve found to be highly valuable to advanced users and library authors. Plus, it would be a huge breaking change.
有了新的架构,我们希望找到一种尽可能减少开销的渲染策略。一种选择是放弃虚拟
DOM,直接生成命令式 DOM 操作,但这将消除直接编写虚拟 DOM 呈现函数的能力,我们发现这对高级用户和库作者非常有价值。另外,这将是一个巨大的突破性变化。
The next best thing was to get rid of unnecessary virtual DOM tree traversals and property comparisons, which tend to have the most performance overhead during updates. In order to achieve this, the compiler and the runtime need to work together: The compiler analyzes the template and generates code with optimization hints, while the runtime picks up the hints and takes fast paths whenever possible. There are three major optimizations at work here:
另一种极好的方法是消除不必要的虚拟 DOM 树遍历和属性比较,它们在更新过程中往往性能开销最大。为了实现这一点,编译器和运行时需要协同工作:编译器分析模板并生成带有优化提示的代码,而运行时则获取这些提示,并尽可能采用快速路径。这里有三个主要的优化:
First, at the tree level, we noticed that node structures stay completely static in the absence of template directives that dynamically alter the node structure (e.g., v-if and v-for). If we divide a template into nested “blocks” separated by these structural directives, the node structures within each block become completely static again. When we update the nodes within a block, we no longer need to recursively traverse the tree—dynamic bindings within the block can be tracked in a flat array. This optimization circumvents much of the virtual DOM’s overhead by reducing the amount of tree traversal we need to perform by an order of magnitude.
首先,在虚拟 DOM 树层面,我们注意到节点结构在没有动态改变节点结构的模板指令(例如 v-if 、 v-for)的情况下保持完全静态。如果我们将一个模板划分成由这些结构指令分隔开的嵌套“块”,那么每个块中的节点结构将再次变得完全静态。当我们更新块中的节点时,我们不再需要递归地遍历树,块中的动态绑定可以在平面数组中跟踪。这种优化通过将需要执行的树遍历量减少一个数量级,从而避免了虚拟 DOM 的大部分开销。
Second, the compiler aggressively detects static nodes, subtrees, and even data objects in a template and hoists them outside the render function in the generated code. This avoids recreating these objects on each render, greatly improving memory usage and reducing the frequency of garbage collection.
第二,编译器积极地检测模板中的静态节点、子树甚至数据对象,并将它们提升到生成代码的渲染函数之外。这样可以避免在每个渲染器上重新创建这些对象,从而大大提高内存使用率并降低垃圾回收的频率。
Third, at the element level, the compiler also generates an optimization flag for each element with dynamic bindings based on the type of updates it needs to perform. For example, an element with a dynamic class binding and a number of static attributes will receive a flag that indicates only a class check is needed. The runtime will pick up these hints and take the dedicated fast paths.
第三,在元素层面,编译器还会根据需要执行的更新类型,为每个元素生打上优化标记,并使用动态绑定。例如,具有动态类绑定和许多静态属性的元素将收到一个标志,该标志指示只需要进行类检查。运行时将获取这些提示并使用专用的快速路径。
Combined, these techniques have significantly improved our render update benchmarks, with Vue 3 sometimes taking less than a tenth of Vue 2’s CPU time. (CPU time That is, time spent performing JavaScript computations, excluding browser DOM operations.)
这些技术结合起来,极大地提高我们的渲染更新 benchmarks,某些时候 Vue 3 占用的 CPU 时间还不到 Vue 2 的十分之一。(CPU时间,即 JavaScript 执行的时间,不包括浏览器 DOM 操作)
最小化构建体积
The size of the framework also affects its performance. This is a unique concern for web applications because assets need to be downloaded on the fly, and the app will not be interactive until the browser has parsed the necessary JavaScript. This is particularly true for single-page applications. While Vue has always been relatively lightweight—Vue 2’s runtime size is around 23 KB gzipped—we’ve noticed two problems:
对于 Web 应用程序来说,有一个独特的问题:框架的体积也会影响性能,因为脚本资源需要动态下载,并且在浏览器解析 JavaScript 之前,应用程序是不能交互的。对于 SPA 尤其如此。尽管 Vue 一直是相对轻量级的,Vue 2 的运行时(排除模板编译相关的代码)代码体积约为23 KB,我们注意到两个问题:
First, not everyone uses all of the framework’s features. For example, an app that never uses the transition features still pays the download and parse costs of transition-related code.
首先,不是所有的开发者都会使用框架里面的全部功能。比如:有些应用压根就不用动画相关的功能,但这部分功能也要跟着框架一起下载和解析。
Second, the framework keeps growing indefinitely as we add new features. This gives bundle size disproportionate weight when we consider the trade-offs of a new feature addition. As a result, we tend to only include features that will be used by the majority of our users.
其次,框架是不定期更新增加新功能的,代码体积的增大,同新功能添加是不成比例的(添加一个新功能指不定要多写多少代码)。结果就是我们为绝大部分用户只提供最基本功能的代码。
Ideally, the user should be able to drop code for unused framework features at build time—also known as “tree-shaking”—and only pay for what they use. This would also allow us to ship features that a subset of our users would find useful without adding payload costs for the rest.
理想情况下,用户应该能够在构建时消除代码引入模块中没有用到的部分,也就是所谓的 tree-shaking (可以理解为按需加载)。如果不增加其他功能的话,就不需要下载额外的代码。
In Vue 3, we achieved this by moving most of the global APIs and internal helpers to ES module exports. This allows modern bundlers to statically analyze the module dependencies and drop code related to unused exports. The template compiler also generates tree-shaking friendly code, which only imports helpers for a feature if the feature is actually used in the template.
在 Vue 3 中,我们通过将大部分全局 API 和内部辅助函数移到 ES module (ES module 是JavaScript 模块实现的一种方案,也是目前主流的一种)的 export 来实现这一点。这使得现代的 bundler 能够静态地分析模块依赖关系,并删除与未使用的导出相关的代码。模板编译器还生成 tree-shakng 友好代码,只有在模板中实际使用了某个功能时,才会为该功能导入辅助函数。
Some parts of the framework can never be tree-shaken because they’re essential to any type of app. We call the measure of these indispensable parts the baseline size. Vue 3’s baseline size is around 10 KB gzipped—less than half that of Vue 2, despite the addition of numerous new features.
框架的核心部分永远不会被 tree-shakng,因为它们对任何类型的应用程序都是必不可少的。我们称这些不可缺少的部分的度量称为基础尺寸。尽管增加了许多新特性,Vue 3 通过 gzip 压缩后,基础尺寸只有 10 kb,比 Vue2 的一半还小(译者注:尽管这个压缩非常极致,但是受益场景越来越小,因为现在的网速越来越不值钱,这一优化今后可能只会在弱网情况下得以体现)。
满足大规模项目需求
We also wanted to improve Vue’s ability to handle large-scale applications. Our initial Vue design focused on a low barrier to entry and a gentle learning curve. But as Vue became more widely adopted, we learned more about the needs of projects that contain hundreds of modules and are maintained by dozens of developers over time. For these types of projects, a type system like TypeScript and the ability to cleanly organize reusable code are critical, and Vue 2’s support in these areas was less than ideal.
我们也希望提升 Vue 对于大型应用开发的能力。一开始 Vue 的设计初衷着眼于降低开发门槛和学习曲线。然而随着 Vue 的使用越来越广泛,我们理解了许多由几十个开发者维护上百个模块的痛点。使用类似于 TypeScript 的类型检查系统是能清晰组织可复用代码的关键,可惜 Vue 2 在这方面的支持没有想象中那么高。
In the early stages of designing Vue 3, we attempted to improve TypeScript integration by offering built-in support for authoring components using classes. The challenge was that many of the language features we needed to make classes usable, such as class fields and decorators, were still proposals—and subject to change before officially becoming part of JavaScript. The complexity and uncertainty involved made us question whether the addition of the Class API was really justified, since it didn’t offer anything other than slightly better TypeScript integration.
在设计 Vue 3 的早期阶段,我们试图通过提供对使用类编写组件的内置支持来改进TypeScript集成。在成为 JavaScript 的一部分之前,我们仍然需要修改 JavaScript 类的许多特性。所涉及的复杂性和不确定性使我们怀疑添加类 API 是否真的合理,因为除了稍微好一点的 TypeScript 集成之外,它没有提供任何东西。
We decided to investigate other ways to attack the scaling problem. Inspired by React Hooks, we thought about exposing the lower-level reactivity and component lifecycle APIs to enable a more free-form way of authoring component logic, called Composition API. Instead of defining a component by specifying a long list of options, the Composition API allows the user to freely express, compose, and reuse stateful component logic just like writing a function, all while providing excellent TypeScript support.
我们决定研究其他方法来解决缩放问题。受React Hooks的启发,我们考虑暴露底层的响应式和组件生命周期API,以支持一种更灵活的方式编写组件逻辑,称为Composition API (不翻译,可以理解为组合 API)。 Composition API 不是通过定义一大坨 option 属性代码来编写组件,而是允许用户像编写函数一样自由地表达、组合和重用有状态的组件逻辑,所有这些都提供了出色的 TypeScript 支持。
We were really excited about this idea. Although the Composition API was designed to address a specific category of problems, it’s technically possible to use it only when authoring components. In the first draft of the proposal, we got a bit ahead of ourselves and hinted that we might replace the existing Options API with the Composition API in a future release. This resulted in massive pushback from community members, which taught us a valuable lesson about communicating longer-term plans and intentions clearly, as well as understanding users’ needs. After listening to feedback from our community, we completely reworked the proposal, making it clear that the Composition API would be additive and complementary to the Options API. The reception of the revised proposal was much more positive, and received many constructive suggestions.
我们对这个想法非常兴奋。尽管 Composition API 是为解决特定类别的问题而设计的,但技术上讲,只有在编写组件时才使用它是可能的。在提案的初稿中,我们有点超前,并暗示在将来的版本中,我们可能会用 Composition API 替换现有的 Options API (也就是 Vue 2 中的 data 、props、methods 等)。这导致了社区成员的强烈反对,这给我们上了一堂宝贵的一课:清楚地传达长期计划和意图,以及理解用户的需求。在听取了社区的反馈意见后,我们对提案进行了全面的修改,明确了 Composition API 将是 Options API 的补充和补充,对修改后的提案的接受度更高,并收到了许多建设性的建议。
寻求平衡点
Among Vue’s user base of over a million developers are beginners with only a basic knowledge of HTML/CSS, professionals moving on from jQuery, veterans migrating from another framework, backend engineers looking for a frontend solution, and software architects dealing with software at scale. The diversity of developer profiles corresponds to the diversity of use cases: Some developers might want to sprinkle interactivity on legacy applications, while others may be working on one-off projects with a fast turnaround but limited maintenance concerns; architects may have to deal with large-scale, multiyear projects and a fluctuating team of developers over the project’s lifetime. Vue’s design has been continuously shaped and informed by these needs as we seek to strike a balance between various trade-offs. Vue’s slogan, “the progressive framework,” encapsulates the layered API design that results from this process. Beginners can enjoy a smooth learning curve with a CDN script, HTML-based templates, and the intuitive Options API, while experts can handle ambitious use cases with full-featured CLI, render functions, and the Composition API.
Vue 拥有超过100万的开发者,其中包括只掌握 HTML/CSS 基本知识的初学者、从jQuery 转行的专业人士、从其他框架迁移过来的老司机、寻找前端解决方案的后端工程师以及大规模处理软件的软件架构师。开发者们各种各样的配置对应着各种各样的使用场景:一些开发人员可能希望在某些老项目上增加交互性,而另一些开发人员则可能致力于一次性项目,其开发周期很短而维护有限;架构师可能必须处理大型、祖传代码和不断变化的团队项目生命周期内的开发者(铁打的代码,流水的程序员←.←)。
Vue 的设计一直在不断地被这些需求所塑造,在各种权衡之间寻求平衡。Vue 的口号“渐进式框架”封装了从这个过程中产生的分层API设计。初学者可以通过 CDN 脚本、基于 HTML 的模板和直观的 Options API 享受流畅的学习曲线,而老司机们可以通过全功能的 CLI 工具、渲染函数和 Composition API 来驾驭一些复杂场景。
There’s still a lot of work left to do to realize our vision—most importantly, updating supporting libraries, documentation, and tools to ensure a smooth migration. We’ll be working hard in the coming months, and we can’t wait to see what the community will create with Vue 3.
要实现我们的愿景还有很多要做的事,最重要的还是更新支持库、文档和工具,以确保顺利迁移。我们将在接下来的几个月里继续致力于上面的工作。这个社区将用 Vue 3 创造出什么,让我们拭目以待!