not a better man

前端技术

打造vue3的历程

Vue3的新路历程

本文翻译转载自https://increment.com/frontend/making-vue-3/

重写下个Vue.js下一个主要版本的收获

在上一年,vue团队一直在开发vue3这个版本,我们希望在2020的上半年发布。(在写这篇文章的时候,我们还在开发中)。新版本功能定型在2018年末,vue2已经有两年多没有大的更新。对于一般的软件来说,这个时间不算长,但是前端有很多东西发生变化。

重写vue有两个关键原因,第一,javascript 新特征,主流浏览器都支持的不错,第二点,当前版本的设计和架构问题有很长时间没有解决了。

为什么重写

利用新的语法特性

ES2015标准,也就是ECMAScript ,缩写为es, 有很多的改善,并且主流的浏览器对这些新特征支持的不错。其中的一些特征让我们有机会大幅度提高vue的性能。

这里面最重要的特征是Proxy 它能让框架拦截对象的操作。Vue核心的一个功能是监听用户定义的state的改变,并改变相应的dom。Vue实现这个响应操作靠使用getter 和setter来替代对象的属性。我们使用Proxy 可以打破vue当前自身的限制。例如无法侦测新增属性的变化可以得到解决,提供更好的性能。

但是 Proxy 是语言的新特征,我们无法在老版本浏览器中使用polyfill 的手段完全实现相关功能。为了利用这个功能,我们必须调整框架的浏览器支持范围,这个是个重大的改变,我们只能在一个新的主要版本中使用这个功能。

解决架构问题

在维护Vue2的过程中,我们积累了许多issues,因为vue2架构的限制,这些问题很难在当前版本去解决。例如,模板编译器的编写方式使得正确的源代码映射支持非常具有挑战性,此外虽然Vue 2在技术上支持构建针对非dom平台的更高级的呈现程序,但为了实现这一点,我们必须派生代码库并复制大量代码。在当前版本修复这些问题,重构量的代价几乎跟重写差不多。

与此同时,模块内部代码的分散让我们欠下了技术债。这使得孤立地理解代码库的一部分变得更加困难,我们注意到,贡献者很少对做出重要的更改感到自信。重写将使我们有机会在考虑这些事情的情况下重新考虑代码组织。

初始原型阶段

在2018年底我们开始Vue3的原型设计,初步目标是验证这些问题的解决方案,在这段时间我们主要工作是打好地基。

切换到typescript


Vue 2最初是用普通的ES写的。在原型化阶段之后不久,我们就意识到类型系统对于这种规模的项目非常有帮助。类型检查极大地减少了在重构过程中引入意外错误的机会,并帮助贡献者更有信心地进行重要的更改。我们采用了Facebook的Flow type checker.因为它可以逐渐添加到现有的pES项目中。Flow在一定程度上有所帮助,但并没有像我们希望的那样从中受益;特别是,不断的变化使升级成为一种痛苦。与TypeScript与Visual Studio代码的深度集成相比还是差很多,对集成开发环境的支持也不是很理想。

我们也注意到用户越来越多地同时使用Vue和TypeScript。为了支持他们的使用,我们必须分别编写并维护TypeScript声明和使用不同类型系统的源代码。切换到TypeScript将允许我们自动生成声明文件,减轻维护负担。

解耦的内部包


我们还采用了monorepo设置,其中框架由内部包组成,每个包都有自己的api、类型定义和测试。我们希望使这些模块之间的依赖关系更加明确,使开发人员更容易阅读、理解和对所有模块进行更改。这是我们努力降低项目的贡献壁垒和提高其长期可维护性的关键。

设置RFC进程


到2018年底,我们有了一个新的反应系统和虚拟DOM渲染器的工作原型。我们已经验证了我们想要进行的内部架构改进,但是只有面向公众的API更改的大致草稿。是时候把它们变成具体的设计了。

我们知道我们必须尽早并且小心地做这件事。Vue的广泛使用意味着破坏更改可能导致用户的大量迁移成本和潜在的生态系统碎片化。为了确保用户能够对中断的更改提供反馈,我们在2019年初采用了RFC(征求意见)流程。每个RFC都遵循一个模板,其中的部分重点介绍动机、设计细节、权衡和采用策略。由于这个过程是在GitHub的repo中进行的,提案作为pull请求提交,讨论在评论中自然展开。

事实证明,RFC过程非常有用,它作为一种思想框架,迫使我们充分考虑潜在更改的所有方面,并允许我们的社区参与设计过程并提交经过深思熟虑的特性请求。

更快和更小

性能对于前端框架来说是至关重要的,尽管vue2有着不错的性能,但是重写框架给与我们机会使用使用新的渲染策略进一步提高性能。

解决虚拟dom的性能瓶颈

Vue有个相当独特的渲染策,它提供了一个类似于html语法的模板,然后将模板编译成返回虚拟dom的渲染函数。通过递归的遍历两棵虚拟dom树,比较每个节点中的每个属性,收集到真实dom中的哪些变化。这种暴力算法也比较快,由于现代的javascript引擎的优秀性能,但是update的操作仍然浪费了不必要的cpu处理操作。当我们在一个大量静态节点的模板中只有一些动态绑定,diff算法就变动低效能了,因为整个virtual dom 都需要递归的遍历来查找到哪些地方发生了变化。

幸运的是,模板编译阶段给我们一个机会对模板进行静态分析并抽取出动态部分。Vue 2在某种程度上通过跳过静态子树实现了这一点,但是由于过于简单的编译器体系结构,更高级的优化很难实现。Vue3中,我们用一个合适的AST转换管道重写了编译器,这允许我们以转换插件的形式组合编译时优化。

有了新的架构,我们希望找到一种渲染策略,尽可能地减少开销。一种选择是抛弃虚拟DOM并直接生成命令式DOM操作,但编写虚拟DOM render 函数的功能就没有了,我们发现这对高级用户和库作者非常有价值。另外,这个变化太大

下一个最好的方法是去掉不必要的虚拟DOM树遍历和属性比较,它们在更新期间的性能开销最大。为了实现这一点,编译器和运行时需要一起工作:编译器分析模板并生成有优化提示的代码,而渲染函数运行的时候能够使用这些优化提示的代码快速的生成实际dom。这里有三个主要的优化工作:

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

其次,编译器会主动检测模板中的静态节点、子树,甚至数据对象,并将它们悬挂在生成代码的render函数之外。这避免了在每次呈现时重新创建这些对象,极大地提高了内存使用率,减少了垃圾回收的频率。

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

结合这些技术,我们的渲染更新基准测试得到了显著改善,Vue 3有时的CPU占用时间还不到Vue 2的十分之一。

包体积更加小型化

框架的体积也会影响性能。对于web开发者来说这是一个很特殊的问题,我们需要把web 应用加载到本地来执行。只有当浏览器加载并解析必要的javascript才会执行app应用程序。对于单页应用来说尤其如此。虽然vue2 也很轻量化,Vue2 打包好的代码gzip压缩之后才23KB。但我们注意到两个问题:

首先并不是每个人都要使用框架的所有特征。例如对于一个不需要使用transition 动画特性的程序来,它仍然需要trasition相关的代码并解析相关代码。

其次,当我们添加新特性时,框架会无限增长。当我们考虑添加新特性时,这使得包的大小与重量不成比例。因此,我们倾向于只包含大多数用户将会使用的特性。

理想情况下,用户应该能够在打包的时候能够删除掉没有使用到代码也就是tree-shaking,他们只需要打包好自己需要的东西。通过tree shaking 我们也可以增加一些新的特性,当用户并不使用这些特性的时候,也不会增加他们下载和解析的成本。

在Vue3 中,我们将全局api和内部一些模块迁移到es exoprt 来实现这一点。这样在静态分析模块依赖的时候删除无关的代码。模板编译器也能编译成对tree-shaking友好的代码,如果我们使用哪些特性,那么我们就导出哪些特性。

框架中的某些部分永远不能动摇,因为它们对于任何类型的应用程序都是必不可少的。我们将这些不可缺少部分的度量称为基线大小。尽管Vue3增加了许多新特性,但是Vue 3的基准大小约为10 KB gzippeg—不到Vue 2的一半。

解决超大应用的问题

我们还想提高Vue处理大规模应用程序的能力。我们最初的Vue设计着重于较低的入门门槛和平缓的学习曲线。但是随着Vue得到更广泛的应用,我们了解了对应包含数百个模块的项目,随着时间的推移,这些模块由几十个开发人员维护。这些类型的项目,类型系统(如TypeScript)和干净地组织可重用代码的能力是至关重要的,Vue 2在这些方面的支持并不理想。

在设计Vue 3的早期阶段,我们试图通过提供使用class编写组件的内置支持来改进TypeScript集成。挑战在于,我们需要使类可用的许多语言特性(如类字段和装饰器)仍然是提议的,而且在正式成为JavaScript的一部分之前可能会发生变化。所涉及的复杂性和不确定性使我们怀疑添加class API是否真的是合理的,因为它除了提供稍微更好的TypeScript集成之外没有提供任何东西。

我们决定研究解决这个问题的其他方法。受React hook的启发,我们考虑公开较低级别的反应性和组件生命周期API,以支持一种更自由的创建组件逻辑的方式,即复合API。与通过指定一长串选项来定义组件不同,复合API允许用户像编写函数一样自由地表达、组合和重用有状态组件逻辑,同时提供优秀的TypeScript支持。

我们对这个想法非常兴奋。尽管复合API的设计是为了解决特定类别的问题,但是只有在编写组件时才可以使用它。在建议的第一份草案中,我们做了一些超前的工作,并暗示我们可能会在将来的版本中用复合API替换现有的选项API。这导致了社区成员的大量反对,这给我们上了宝贵的一课,让我们清楚地传达长期计划和意图,以及了解用户的需求。在听取了社区的反馈之后,我们完全重新设计了提案,明确表示复合API可以作为选项API的补充。修改建议的接受情况要积极得多,收到了许多建设性的建议。

探索平衡

数百万的vue用户中包括对HTML/CSS只有基本了解的初学者、从jQuery跳槽过来的专业人员、从其他框架迁移过来的老手、寻找前端解决方案的后端工程师以及大规模处理软件的软件架构师。开发人员配置文件的多样性对应于用例的多样性:一些开发人员可能希望在遗留应用程序上点缀一些交互性,而其他开发人员可能正在处理一次性项目,这些项目具有快速的周转能力,但是维护问题有限;架构师可能必须处理大型的、多年的项目和在项目生命周期中不断变化的开发团队。

Vue的设计一直受到这些需求的影响,我们试图在各种权衡之间取得平衡。Vue的口号“渐进框架”封装了这个过程产生的分层API设计。初学者可以使用CDN脚本、基于html的模板和直观的选项API来享受平滑的学习曲线,而专家可以使用全功能的CLI、呈现函数和复合API来处理复杂的用例。

要实现我们的远景,还有很多工作要做——最重要的是更新支持库、文档和工具,以确保顺利迁移。我们将在接下来的几个月里努力工作,我们已经迫不及待地想要看到Vue 3将会给社区带来什么。

发表评论