vue3.0的优化

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

一、写在前面
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)。

export function square(x) {
	return x * x
}
export function cube(x) {
	return x * x * x
}

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

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

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

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
  'use strict';
  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
  function square(x) {
    return x * x;
  }
  function cube(x) {
    return x * x * x;
  }
});

我们可以看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。具体是如下实现的。

Object.defineProperty(data, 'a', {
	get(){
	
	},
	set() {
	
	}
})

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

export default {
  data: {
    a: {
      b: {
        c: {
          d: 1
        }
      }
    }
  }
}

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

observed = new Proxy(data, {
  get() {
    // track
  },
  set() {
    // trigger
  },
});

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

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

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

<template>
  <div id="content">
    <p class="text">static text</p>
    <p class="text">static text</p>
    <p class="text">{{message}}</p>
    <p class="text">static text</p>
    <p class="text">static text</p>
  </div>
</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)去复用逻辑,举一个鼠标监听的例子,我们会编写如下代码。

const mousePositionMixin = {
  data() {
    return {
      x: 0,
      y: 0
    }
  },
  mounted() {
    window.addEventListener('mousemove', this.update)
  },
  destroyed() {
    window.removeEventListener('mousemove', this.update)
  },
  methods: {
    update(e) {
      this.x = e.pageX
      this.y = e.pageY
    }
  }
}
export default mousePositionMixin

然后在组件中使用:

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
  mixins: [mousePositionMixin]
}
</script>

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

import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

然后进行使用。

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
  import useMousePosition from './mouse'
  export default {
    setup() {
      const { x, y } = useMousePosition()
      return { x, y }
    }
  }
</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 采用或被废弃掉的前因后果。

相关文章