vue3.0的优化

x33g5p2x  于2022-03-14 转载在 其他  
字(5.8k)|赞(0)|评价(0)|浏览(353)

一、写在前面
Vue.js 从 1.x 到 2.0 版本,最大的升级就是引入了虚拟 DOM 的概念,它为后续做服务端渲染以及跨端框架 Weex 提供了基础。Vue.js 2.x 发展了很久,现在周边的生态设施都已经非常完善了,而且对于 Vue.js 用户而言,它几乎满足了我们日常开发的所有需求。你可能觉得 Vue.js 2.x 已经足够优秀,但是在 Vue.js 作者尤小右的眼中它还不够完美。在迭代 2.x 版本的过程中,小右发现了很多需要解决的痛点,比如源码自身的维护性,数据量大后带来的渲染和更新的性能问题,一些想舍弃但为了兼容一直保留的鸡肋 API 等;另外,小右还希望能给开发人员带来更好的编程体验,比如更好的 TypeScript 支持、更好的逻辑复用实践等,所以他希望能从源码、性能和语法 API 三个大的方面优化框架。
那么接下来,我们就一起来看一下 Vue.js 3.0 具体做了哪些优化。相信你学习完这篇文章,不仅能知道 Vue.js 3.0 的升级给我们开发带来的收益,还能学习到一些设计思想和理念,并在自己的开发工作中应用,获得提升。
二、源码优化:
2.1、更好的代码管理方式:monorepo
首先源码的优化体现在代码的管理方式上。vue2.x的源码管托在src目录中,然后依据功能拆分出了complier(模板编译的相关代码)core(与平台无关的通用运行时代码)platforms(平台专有代码)server(服务端渲染的相关代码)、sfc(.vue 单文件解析相关代码)、shared(共享工具代码) 等目录。

而到了vue3.0,整个源码是通过monorepo的方式维护的,根据功能不同的模块拆分到packages目录下面不同的子目录中。

可以看出相对于vue2.x的源码组织方式,monorepo把这些不同的package中,每一个package有各自的api,类型定义和测试,这样使得模块拆分更加细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。另外一点package可以独立于vue.js去使用,这样例如用户想要使用vue3.0的响应式,可以单独依赖reactive,而不必依赖整个vue.js,减少引用包的体积,而vue2.x却做不到这一点。
2.2、有类型的javascript——typescript
在vue1.x中当时没有采用类型语言。但是对于开发大型框架的时候,使用类型语言非常有利于IDE对于类型推导。所以尤雨溪在vue2.x使用的是Flow来进行开发,Flow是facebook出品的javascript的静态类型检查工具,但是flow对于一些复杂的场景flow支持的不是很好。所以在vue3.x中vue全面转向typescript,typescript提供了更好的类型检查,也支持复杂的类型推导。
三、性能优化
3.1、源码体积优化
在性能优化方面我们首先想到的是代码的体积,因为javascript包的体积越小,意味着网络传输的时间就越短,javascript引擎解析包的速度也就越快。vue3.0在源码体积上做了哪些工作呢?
1、首先,移除了一些冷门的api(例如:filter, inline-template)等
2、其次,引入tree-shaking的技术来减少打包的体积。
第一点非常容易理解,但是第二点该如何去理解呢?下面举一个例子,一个math模块中定义两个方法square(x)和cube(x)。

  1. export function square(x) {
  2. return x * x
  3. }
  4. export function cube(x) {
  5. return x * x * x
  6. }

然后我们在这个模块外边引入cube方法

  1. import { cube } from './math.js'

最终math模块会被webpack打包如下所示。

  1. /* 1 */
  2. /***/ (function(module, __webpack_exports__, __webpack_require__) {
  3. 'use strict';
  4. /* unused harmony export square */
  5. /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
  6. function square(x) {
  7. return x * x;
  8. }
  9. function cube(x) {
  10. return x * x * x;
  11. }
  12. });

我们可以看square上存在一个标记,所以当我们使用丑化代码工具来对js代码进行丑化时,这些存在标记的代码就会被去除,从而减少代码的体积。
在vue3.x中也是同样的道理,利用tree-shaking技术,如果项目中没有使用transition,和keep-alive等组件时,那么这些代码就不会进行打包,这样就间接达到了减少项目引入vue.js包体积的目的。
3.2、数据劫持优化
响应式是vue.js和react的差别,从vue1.x版本就一直伴随着。DOM是数据的一种映射,数据发生变化后就会自动更新DOM,用户只需要专注于数据的修改,没有其余的心智负担。
在vue.js内部如果想要实现响应式,则就会出现数据的劫持和更新,也就是当数据发生改变时,会自定执行一些代码去更新DOM。

如上图所示,当我们开始执行render时,会触发data中的getter方法,让后就会进行依赖收集,当数据发生改变的时候,此时就会触发notify来执行代码改变页面内容。(也就是重新渲染)。
在vue1.x和vue2.x内部是通过Object.defineProperty这个API来获取数据的gettersetter。具体是如下实现的。

  1. Object.defineProperty(data, 'a', {
  2. get(){
  3. },
  4. set() {
  5. }
  6. })

但是这个api只能检测get和set,不能检测给对象增加属性和删除属性,此时vue为了解决这个问题,出现了$set$delete实例方法。另外还存在一个问题。就是如果存在多个对象进行嵌套问题。例如:

  1. export default {
  2. data: {
  3. a: {
  4. b: {
  5. c: {
  6. d: 1
  7. }
  8. }
  9. }
  10. }
  11. }

由于vue.js无法判断你在运行时到底会访问那个属性,所以对于这样一个嵌套比较深的对象,就需要使用遍历吗,这无疑给性能增加了很大的负担。
为了解决如上问题,在vue3.0中使用来proxy api做数据劫持,它的内部是这样的。

  1. observed = new Proxy(data, {
  2. get() {
  3. // track
  4. },
  5. set() {
  6. // trigger
  7. },
  8. });

由于proxy劫持的是整个对象,那么对于对象的增加删除都是可以劫持到的。但是注意的是,Proxy api并不能检测到内部对象的变化,所以vue3.0的处理方式为在getter中去使用响应式递归,当真正需要响应式递归时,才会去递归,这样极大程度提升了性能问题。
3.3、编译优化
最后是编译优化,为了方便理解,下面为一张图

这里vue.js2.x从new Vue开始渲染DOM的流程,之前我们通过数据劫持来进行优化,下面我们可以对耗时较多的patch阶段进行优化。我们都知道vuejs2.x的数据更新并触发重新渲染的颗粒是组件级别的。

虽然vue能保证呢触发更新的组件最小化,但是单个组件内部仍然需要遍历该组件的整个vnode树,举一个例子,例如我们需要更新如下的组件。

  1. <template>
  2. <div id="content">
  3. <p class="text">static text</p>
  4. <p class="text">static text</p>
  5. <p class="text">{{message}}</p>
  6. <p class="text">static text</p>
  7. <p class="text">static text</p>
  8. </div>
  9. </template>

整个的diff算法是这样的:

可以看到,因为代码中只有一个动态节点,所以这里有很多的diff的遍历其实都是不需要的,也就是说导致vnode性能的跟模板大小正相关,跟动态节点的数量无关。当一些组件的整个模板中只有少量的动态节点时,这些遍历都是性能的浪费。对于上述例子中,理想状态只需要diff这个绑定message的p标签即可。vue3.0做到了,通过对编译阶段对静态模板进行分析,编译生层Block tree。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。
四、语法API的优化——Composition API
4.1、优化逻辑组织
在vue1.x和vue2.x时,我们使用的是Options api,但是在vue3.x时,我们使用的是Componsition api,因为在vue2.x时,使用Options api如果处于比较小型的项目中,可能逻辑还是可以分清的,但是如果处于大型的项目中就需要上下来回的切换代码。
如下图所示Options api 和 Componsition api对比

4.2、优化逻辑复用
当我们开发的项目比较复杂的时候,免不了需要抽离出来一些复用的路基,在vue2.x中我们通常会使用混入(mixins)去复用逻辑,举一个鼠标监听的例子,我们会编写如下代码。

  1. const mousePositionMixin = {
  2. data() {
  3. return {
  4. x: 0,
  5. y: 0
  6. }
  7. },
  8. mounted() {
  9. window.addEventListener('mousemove', this.update)
  10. },
  11. destroyed() {
  12. window.removeEventListener('mousemove', this.update)
  13. },
  14. methods: {
  15. update(e) {
  16. this.x = e.pageX
  17. this.y = e.pageY
  18. }
  19. }
  20. }
  21. export default mousePositionMixin

然后在组件中使用:

  1. <template>
  2. <div>
  3. Mouse position: x {{ x }} / y {{ y }}
  4. </div>
  5. </template>
  6. <script>
  7. import mousePositionMixin from './mouse'
  8. export default {
  9. mixins: [mousePositionMixin]
  10. }
  11. </script>

使用单个 mixin 似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。首先每个 mixin 都可以定义自己的 props、data,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突。另外对组件而言,如果模板中使用不在当前组件中定义的变量,那么就会不太容易知道这些变量在哪里定义的,这就是数据来源不清晰。但是 Vue.js 3.0 设计的 Composition API,就很好地帮助我们解决了 mixins 的这两个问题。
在vuejs3.x中使用hook来解决

  1. import { ref, onMounted, onUnmounted } from 'vue'
  2. export default function useMousePosition() {
  3. const x = ref(0)
  4. const y = ref(0)
  5. const update = e => {
  6. x.value = e.pageX
  7. y.value = e.pageY
  8. }
  9. onMounted(() => {
  10. window.addEventListener('mousemove', update)
  11. })
  12. onUnmounted(() => {
  13. window.removeEventListener('mousemove', update)
  14. })
  15. return { x, y }
  16. }

然后进行使用。

  1. <template>
  2. <div>
  3. Mouse position: x {{ x }} / y {{ y }}
  4. </div>
  5. </template>
  6. <script>
  7. import useMousePosition from './mouse'
  8. export default {
  9. setup() {
  10. const { x, y } = useMousePosition()
  11. return { x, y }
  12. }
  13. }
  14. </script>

我们可以看到,数据来源变得清晰了,即使去编写更多的hooks,也不会出现命名冲突的问题。
Componsition api除了在逻辑复用方面存在一些优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。这里还需要说明的是,Composition API 属于 API 的增强,它并不是 Vue.js 3.0 组件开发的范式,如果你的组件足够简单,你还是可以使用 Options API。
五、引入RFC:使得每一个版本改动可控
作为一个流行开源框架的作者,尤雨溪可能每天都受到很多的feature request。但是并不是社区一存在新的功能的需求,框架就会马上实现,因为随着 Vue.js 的用户越来越多,小右会更加重视稳定性,会仔细考虑所做的每一个可能对最终用户影响的更改,以及有意识去防止新 API 对框架本身实现带来的复杂性的提升。因此vuejs2.x版本开发到后期的阶段,尤雨溪就启动了RFC,他的全称为Request for comments。当社区有一些新需求的想法时,它可以提交一个 RFC,然后由社区和 Vue.js 的核心团队一起讨论,如果这个 RFC 最终被通过了,那么它才会被实现。到了vuejs3.0实现代码前就大规模启用 RFC,来确保他的改动和设计都是经过讨论并确认的,这样可以避免走弯路。Vue.js 3.0 版本有很多重大的改动,每一条改动都会有对应的 RFC,通过阅读这些 RFC,你可以了解每一个 feature 采用或被废弃掉的前因后果。

相关文章