# V3升级目的

升级肯定是出于某些目的,为了让自己”学得动“得先发现这个东西是有让人学习的价值

V3的升级主要有两点考虑:

  1. 主流浏览器中JavaScript新特性的普遍可用性

  2. 随着时间的推移,当前代码库的设计和结构上的缺陷逐渐暴露了出来

# WHY?

# 利用新的语言特性

随着ES2015的标准化,以及JavaScript(正式名称为ECMAScript,缩写为ES) 进行了重大升级,主流浏览器也开始对这些新特性提供不错的支持。其中一些为我们提供了极大提升Vue性能的机会。

其中最值得注意的是Proxy,它允许框架拦截对对象的操作。Vue的一个核心特色就是能监听用户自定义 data 的变化,并且响应式地更新DOM。Vue 2通过 Object.defineProperty 劫持 data 内的属性来实现这一点。使用 Proxy 实现可以帮助我们消除现有的限制,比如无法检测新添加的属性,并且它还可以改善 Vue 的性能。

不过,Proxy是一个原生的语言特性,在旧浏览器中无法被完全 polyfill 。为了使用它,我们必须调整框架所支持的浏览器范围,这是一个只有在新的主版本中才可以做出的重大改变

# 克服虚拟DOM的瓶颈

Vue有一个相当独特的渲染策略:它提供一个接近HTML(HTML-like)的模板语法,并最终把它编译为一个可以返回虚拟DOM树的渲染函数。该框架通过递归遍历两个虚拟DOM树并比较每个节点上的每个属性来确定实际DOM的哪些部分需要更新。感谢现代JavaScript引擎所执行的高级优化,这个有些粗糙的算法通常执行得很快,但是更新过程仍然涉及很多不必要的CPU操作。当你观察一个包含大量静态内容而只有少量动态绑定的模板时,效率低下问题就会变得很明显 – 整个虚拟DOM树仍然需要递归遍历来算出哪里发生了变化。

幸运的是,模板编译步骤给了我们分析静态模板和动态部分的机会。Vue 2通过跳过静态子树在一定程度上做到了这一点,但是由于编译器架构过于简单,更进一步的优化很难实现。在Vue 3中,我们用更合适的AST转换管道(AST transform pipeline)重写了编译器,它使得我们能以转换插件的形式进行编译时优化。

随着新架构的实施,我们希望找到一种开销尽可能低的渲染策略。一个选择是舍弃虚拟DOM,直接生成必要的DOM操作,但是那会丧失直接编写虚拟DOM渲染函数的能力,而我们发现这个能力对高级用户和库的开发者非常有用。另外,这又将是一个重大更新。

接下来最好的方法是消除不必要的虚拟DOM树遍历和属性比较,而这在更新过程中的性能损耗是最大的。为了实现这一点,编译器和运行时必须同时工作:编译器分析模板和生成带有优化提示的代码,同时,运行时拾取这些提示,并采取尽可能快的更新策略。这里主要有三个优化:

第一,从树的层面看,我们注意到,在没有使用可以动态改变树结构的指令(例如v-if和v-for)的情况下,节点结构是完全静态的。如果我们将模板划分为由这些结构指令分隔的嵌套“块”,那么每个“块”中的节点结构又会变成完全静态的。当我们在一个“块”内部更新节点时,我们不再需要递归遍历整棵树 – 因为“块”内的动态绑定可以在一个扁平数组(译者注:即一维数组)中被追踪到。通过将需要执行的树遍历运算减少一个数量级,这种优化规避了虚拟DOM的大部分开销。

第二,编译器会主动监测模板中的静态节点、静态子树甚至数据对象,并且把它们提取到结果代码中的渲染函数之外。这避免了在每个渲染函数中重新创建这些对象,极大的改善了内存使用,降低了垃圾回收频率。

第三,从标签元素的角度来说,编译器还会根据需要执行的更新类型为每个元素动态绑定生成一个优化标志。例如,一个有动态class和一些静态属性的元素会被标记为只需要进行类名检查。运行时会拾取这些提示并采取专门的快速更新策略。

结合这些技术,Vue 3占用的 CPU时间 还不到Vue 2的十分之一,极大地改善了我们的渲染更新基准测试性能

# 最小化包体积

框架的体积同样影响它的性能。这是web应用程序遇到的一个独特问题,因为资源需要在使用时下载,并且在浏览器解析完必要的JavaScript代码之前,应用无法产生交互。对于单页面应用程序来说尤其如此。尽管Vue一直以来是比较轻量的 – Vue 2xx版本的运行时使用gzip压缩后只有23KB,我们还是注意到两个问题:

第一,不是所有人都会用到框架的所有功能。例如,一个不需要使用transition组件的应用仍然需要付出下载和解析与transition有关代码的代价。

第二,随着我们不断增加新特性,框架也在不断增长。当我们在权衡新特性的利弊时,包的体积必须考虑在内。最终,我们倾向于只添加大多数用户会用到的功能。

理想情况下,用户应该能够在构建时删除那些未使用的框架特性相关的代码 – 也叫tree-shaking,只留下他们用到的东西。这也使得我们可以在不增加其他用户成本的情况下,为一部分用户提供有用的特性。

在Vue 3中,我们通过把大部分全局API和内置帮助程序(internal helpers)转移到ES模块中来实现这一点。这允许现代打包器静态地分析模块依赖关系,并删除与未使用的特性相关的代码。模板编译器也可以生成tree-shaking友好的代码,它只会在模板中实际使用了该特性时才导入与该特性相关的帮助程序。

框架中的一些部分永远不能被tree-shaken,因为它们对任何一个应用都是必要的。我们称这些不可缺少的部分的体积为基准体积。尽管增加了大量的新特性,但Vue 3的基准体积用gzip压缩后只有大约10KB - 比Vue 2的一半还小

# 解决对规模化的需求

我们还想提升Vue应对大型应用的能力。我们最初的Vue设计专注于较低的准入门槛和平缓的学习曲线。但是随着Vue的使用越来越广泛,我们意识到支持包含数百个模块以及由数十名开发者维护的大型项目是必要的。对这类项目,像TypeScript这样的类型系统,以及干净地组织可重用代码的能力是至关重要的,然而Vue 2在这方面的支持不够理想。

在Vue 3设计的早期阶段,我们尝试通过支持使用类编写组件来改进TypeScript集成。挑战在于,class所依赖的许多语言特性,例如类字段和修饰器,仍处于建议阶段。而在成为正式的JavaScript标准之前,这些特性仍然可能变化。这些问题所涉及的复杂性和不确定性让我们怀疑添加类API是否真的合理,因为它除了提供稍好的TypeScript集成之外,没有带来任何好处。

我们决定研究解决规模化问题的其他方法。受React Hooks的启发,我们考虑通过暴露更底层的响应式和组件生命周期API,来启用一种更自由的方式编写组件逻辑,我们称之为Composition API。与通过指定一长串option来定义组件不同,Composition API允许用户自由地像编写函数一样表达、组合和重用有状态组件逻辑,并且这些都提供了很好的TypeScript支持。

我们对这个想法感到兴奋。尽管Composition API设计出来是为了解决某些特定的问题,但在编写组件时只使用这类API来实现(译者注:指完全使用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 3社区将会创造什么了

# 总结

  • 利用新的语言特性

    • 使用 Proxy 代替 Object.defineProerty
  • diff 算法优化

  • 体积更小,对代码结构进行了优化,让其容易被 Tree shaking 优化

  • 增加 Composition API,提升Vue 组织复杂业务逻辑的能力

  • 更好得支持 TypeScript

The process: Making Vue 3 (opens new window)

Vue 3的设计过程(翻译自尤雨溪原文) (opens new window)

vue3新特性分析 (opens new window)