为了实现更清晰、更易维护的源代码架构,Vue.js作者尤雨溪于2018 年年底透露 Vue 下一版本 。
如今一年多时间已过,在上个月,我们终于见证了 Vue 3.0 beta 的发布:https://github.com/vuejs/vue-next#status-beta,不过对于该测试版,尤雨溪表示,“暂时还不建议升级生产环境应用,升级指南和配套工具都还没完善,但是可以在新/小项目里试用。”
针对重写最新版本的 Vue,尤雨溪于日前发表了长文对其过程中获得的经验教训,作了详细阐述,对此,深正互联进行了全文翻译,望共勉。
以下为译文:
在过去的一年里,Vue团队一直在开发Vue.js的下一个主要版本Vue 3,我们希望能在2020年上半年将其发布(在撰写本文时,这项开发工作正在进行中)。重写Vue新的主要版本的构想是在2018年底形成的,当时Vue 2的代码库大约已有两年半的运行历史。这看起来不像是通用软件生命周期中的一段很长的时间,但在这段时间里,前端环境发生了巨大的变化。
两个关键的因素导致了我们考虑重写Vue新的主要版本:
第一,主流浏览器对新的JavaScript语言特性的普遍支持。
第二,当前Vue代码库随着时间的推移而暴露出来的设计和体系架构问题。
为什么要重写?
使用新的语言特性
随着ES2015标准的发布,Javascript(正式称为ECMAScript,简称ES)得到了重大改进,主流浏览器终于开始为这些新添加的特性提供适当的支持。其中一些特性特别地为我们提供了极大提升Vue能力的机会。
其中一个最值得注意的特性是Proxy,它允许框架拦截针对对象(属性)的操作。Vue的一个核心特性是能够监听对用户定义的状态所做的更改,并对DOM进行响应式地更新。Vue 2通过使用getter和setter来替换有状态对象的属性,来实现这种响应式的更新。切换到Proxy方式将允许我们消除Vue现有的限制(例如无法检测新的属性添加),并提供更好的性能。
然而,Proxy是一个原生语言特性,在传统浏览器中这个特性无法用polyfill来兼容。因此,为了利用这个特性,我们必须调整Vue框架的浏览器支持范围,这是一个重大的突破性的改变,只能在新的主要版本中发布。
解决体系架构存在的问题
在维护Vue 2的过程中,我们积累了一些由于现有体系架构的限制而难以解决的问题。例如,模板编译器的编写方式使得正确的源映射(source-map)支持非常具有挑战性。另外,虽然Vue 2在技术上支持构建针对非DOM平台的更高级别的渲染器,但为了实现这一点,我们必须分叉代码库并复制大量代码。在当前的代码库中修复这些体系架构问题将需要风险巨大的重构工作,而这些重构几乎等同于重写。
同时,我们还积累了技术债务,表现为各种模块的内部和似乎不属于任何地方的浮动代码之间的隐式耦合。这使得孤立地理解代码库的一部分变得更加困难,我们注意到贡献者中很少有人有信心做出大范围的更改。重写将使我们有机会带着这些问题和想法来重新考虑整个代码库结构。
初始原型的构建阶段
我们在2018年底开始Vue 3的原型开发,初步目标是验证这些问题的解决方案。在这一阶段,我们主要致力于为进一步开发打下坚实的基础。
切换到TypeScript
Vue 2最初是用纯ES(Javascript)写成的。在原型设计阶段之后不久,我们意识到一个类型系统(Type system)对于这样一个规模的项目非常有用。类型检查(Type check)大大减少了在重构过程中引入意外错误的机会,并帮助贡献者更有信心进行大范围的更改。我们采用了Facebook的Flow type checker,因为它可以逐渐添加到现有的纯ES项目中。Flow type checker在一定程度上起到了帮助作用,但我们并没有从中得到我们所希望的那么多好处。特别是,持续的重大改变使得升级成为一种痛苦。相比较TypeScript与Visual Studio Code集成开发工具的深度集成,Flow type checker对集成开发环境的支持也不理想。
我们还注意到,用户越来越多地同时使用Vue和TypeScript。为了支持它们的用例,我们必须独立于使用不同类型系统的源代码来编写和维护TypeScript声明。切换到TypeScript将允许我们自动生成声明文件,从而减轻维护负担。
解耦内部包
我们还采用了monorepo设置,其中框架由内部包组成,每个包都有各自的API、类型定义和测试程序。我们希望能够使这些模块之间的依赖关系更加明确,以便开发人员能够更容易地阅读、理解和更改所有模块。这是我们努力降低项目贡献障碍和提高其长期可维护性的关键。
设置RFC(征求修正意见)流程
到2018年底,我们已经成功构建了一个可工作的原型,它带有新的响应式系统和虚拟DOM渲染器。我们已经验证了我们想要进行的内部架构改进,但是面向公众的API的更改还停留在草稿阶段。是时候把它们变成具体的设计了。
我们知道我们必须尽早并且小心地处理这件事。Vue的广泛采用意味着任何重大改变都可能导致用户的巨大迁移成本和潜在的生态系统碎片化。为了确保用户能够提供关于这些重大改变的反馈,我们在2019年初采用了RFC(征求修正意见)流程。每个RFC都使用一个模板,重点关注动机、设计细节、权衡和采用策略。由于该过程是在GitHub repo中进行的,我们将我们建议的更改作为pull请求提交,因此讨论以评论的形式有效地展开了。
事实证明,RFC流程非常有效,它作为一个思想框架,迫使我们充分考虑潜在变化的方方面面,并允许我们的社区参与设计过程,提交深思熟虑的功能请求。
更快更小性能对前端框架至关重要。尽管Vue 2号称具有良好的性能,但重写提供了一个机会,可以通过试验新的渲染策略来更提供更好的性能。
克服虚拟DOM的瓶颈
Vue有一个相当独特的渲染策略:它提供类似于HTML的模板语法,但是,它是将模板编译成渲染函数来返回虚拟DOM树。Vue框架通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性,来确定实际DOM的哪些部分需要更新。由于现代JavaScript引擎执行的高级优化,这种有点暴力的算法通常非常快速,但是DOM的更新仍然涉及许多不必要的CPU工作。当你看到一个基本上是静态内容、只有少量动态绑定的模板时,效率低下的情况尤其明显,因为这时候仍然需要递归地遍历整个虚拟DOM树,以找出需要更改的内容。
幸运的是,模板编译步骤使我们有机会对模板执行静态分析并提取有关动态部分的信息。Vue 2在某种程度上是通过跳过静态子树来实现的,但是过于简单的编译器体系架构使得更高级的优化很难实现。在Vue 3中,我们使用适当的AST转换管道重写编译器,这允许我们以转换插件的形式将编译时(compile-time)优化组合进来。
随着新的体系架构的出现,我们希望找到一种能够尽可能减少开销的渲染策略。一种选择是抛弃虚拟DOM并直接生成命令式DOM操作,但这样做会消除直接编写虚拟DOM渲染函数的能力,而我们发现这种能力对于高级用户和库的编写者非常有价值。另外,这将是一个巨大的突破性改变。
另一个更好的办法是去掉不必要的虚拟DOM树遍历和属性比较,这在更新期间往往会产生最大的性能开销。为了实现这一点,编译器和运行时需要协同工作:编译器分析模板并生成带有优化提示的代码,而运行时尽可能获取提示并采用快速路径。这里有三个主要的优化:
首先,在DOM树级别。我们注意到,在没有动态改变节点结构的模板指令(例如v-if和v-for)的情况下,节点结构保持完全静态。如果我们将一个模板分成由这些结构指令分隔的嵌套“块”,则每个块中的节点结构将再次完全静态。当我们更新块中的节点时,我们不再需要递归遍历DOM树 - 该块内的动态绑定可以在一个平面数组中跟踪。这种优化通过将需要执行的树遍历量减少一个数量级来规避虚拟DOM的大部分开销。
其次,编译器积极地检测模板中的静态节点、子树甚至数据对象,并在生成的代码中将它们提升到渲染函数之外。这样可以避免在每次渲染时重新创建这些对象,从而大大提高内存使用率并减少垃圾回收的频率。
第三,在元素级别。编译器还根据需要执行的更新类型,为每个具有动态绑定的元素生成一个优化标志。例如,具有动态类绑定和许多静态属性的元素将收到一个标志,提示只需要进行类检查。运行时将获取这些提示并采用专用的快速路径。
综合起来,这些技术大大改进了我们的渲染更新基准,Vue 3有时占用的CPU时间不到Vue 2的十分之一。
注:CPU时间指的是执行JavaScript计算所花费的时间,不包括浏览器DOM操作。
最小化bundle的大小
框架的大小也会影响其性能。这是Web应用程序的一个独特关注点,因为所有相关的代码需要动态下载,并且在浏览器解析出必要的JavaScript之前,应用程序不会进行交互。对于单页应用程序尤为如此。虽然Vue一直都比较轻量级,但Vue 2的运行时压缩后的大小也约有23 KB,我们注意到两个问题:
首先,并不是每个人都使用框架的所有特性。例如,一个从不使用transition特性的应用程序仍然需要付出与使用transition特性相关的代码的下载和解析成本。
第二,随着我们添加新特性,框架会无限制地增长。当我们权衡一个新特性添加的利弊时,我们就给了与bundle大小不成比例的权重。因此,我们倾向于只包含大多数用户使用的特性。
理想情况下,用户应该能够在构建时删除未使用的框架特性(也称为“树抖动-tree shaking”)的代码,并且只为他们使用的代码付出成本。这也将使我们能够在不增加其他用户的有效负荷成本的情况下,发布一些用户认为有用的特性。
在Vue 3中,我们通过将大多数全局API和内部帮助程序移动到Javascript的module.exports属性上实现这一点。这允许现代模式下的module bundler能够静态地分析模块依赖关系,并删除与未使用的module.exports属性相关的代码。模板编译器还生成了对树抖动友好的代码,只有在模板中实际使用某个特性时,该代码才导入该特性的帮助程序。
框架中有些部分永远不会被“树抖动”(这部分的代码永远不会从框架中删除),因为它们对任何类型的应用程序都是必不可少的。我们称这些不可缺少的部分为基线大小。尽管增加了许多新特性,但Vue 3被压缩后的基线大小约为10 KB,不到Vue 2的一半。
解决规模性的需求
我们还希望提高Vue处理大规模应用程序的能力。我们最初的Vue设计专注于低门槛和温和的学习曲线。但是随着Vue被越来越广泛地采用,我们对包含数百个模块并由数十名开发人员长期维护的项目的需求了解得更多。对于这种类型的项目,TypeScript之类的类型系统和干净地组织可重用代码的能力是至关重要的,而Vue 2在这些领域的支持并不理想。
在设计Vue 3的早期阶段,我们试图通过提供对使用类编写组件的内置支持来改进TypeScript集成。然而挑战在于,在正式成为JavaScript的一部分之前,我们需要使用可用的许多语言特性(如类字段和装饰器)仍然是建议的,并且可能会发生更改。所以,这种方法所涉及的复杂性和不确定性使我们怀疑添加类API是否真的是合理的,因为除了提供稍好一点的TypeScript集成之外,它没有提供任何其他特性。
我们决定研究其他方法来解决规模性难题。受React钩子的启发,我们考虑公开较低级别的响应性和组件生命周期API,以实现一种更自由形式的编写组件逻辑的方法,我们称之为Composition API。Composition API不需要通过指定一长串选项来定义组件,而是允许用户像编写函数一样自由地表达、组合和重用有状态的组件逻辑,同时提供出色的TypeScript支持。
我们对这个主意很兴奋。尽管Composition API是为解决一类特定的问题而设计的,但在技术上只有在编写组件时才有可能使用它。在提案的初稿中,我们有点超前,并暗示在将来的版本中,我们可能会用Composition API替换现有的Options API,这导致了社区成员的强烈抵制。这件事给了我们一个宝贵的教训,让我们学会清楚地传达长期计划和意图,以及理解用户的需求。在听取了我们社区的反馈后,我们彻底修改这个提议,明确表示Composition API将是Options API的附加和补充。对于这一订正提案的反馈要积极得多,并收到了许多建设性的建议。
寻求平衡
在Vue超过100万开发人员的用户群中,有只掌握HTML/CSS基础知识的初学者,有从jQuery转移来的专业人员,有从另一个框架迁移过来的老手,有寻找前端解决方案的后端工程师,以及处理大规模软件的软件架构师们。开发人员知识背景的多样性导致了用例的多样性:一些开发人员可能希望在老旧的应用程序上添加交互性,另一些开发人员可能正在处理一次性的项目,这些项目的周转速度很快,但维护问题有限;而架构师们可能不得不在项目的生命周期中处理大型的、多年的项目和面对不断变化的开发团队。
Vue的设计不断地受到这些需求的影响,我们试图在各种需求之间取得平衡。Vue的口号“渐进式框架”就是对通过这种过程产生的分层API设计的一种概括。初学者们可以通过使用CDN脚本、基于HTML的模板和直观的Options API,享受平滑的学习曲线,而专家们则可以使用功能齐全的CLI、渲染函数和Composition API来处理复杂的用例。
为了实现我们的愿景,还有很多工作要做。最重要的工作是更新支持库、提供文档和工具,以确保迁移的顺利进行。我们将在接下来的几个月里努力工作,我们迫不及待地想看看Vue社区会使用Vue 3构建出什么。
深圳 · 龙岗 · 大运软件小镇22栋201
电话:400 182 8580
邮箱:szhulian@qq.com