0%

Vue3.0的优化

源码优化

码的优化主要体现在使用 monorepo 和 TypeScript 管理和开发源码,这样做的目标是提升自身代码可维护性。

1. 更好的代码管理方式:monorepo

首先,源码的优化体现在代码管理方式上。Vue2.x的源码托管在src目录,然后依据功能拆分出了compiler(模版编译的相关代码)、core(与平台无关的通用运行时代码)、platforms(平台专有代码)、sfc(.vue单文件解析相关代码)、shared(共享工具代码)等目录

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

可以看出相对于2.x的源码管理方式,monorepo把这些模块拆分到不同的package中,每个package有格子的API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。

另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue.js 使用的,这样用户如果只想使用 Vue.js 3.0 的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue.js,减小了引用包的体积大小,而 Vue.js 2 .x 是做不到这一点的。

有类型的javascript:TypeScript

其次,源码的优化还体现在 Vue.js 3.0 自身采用了 TypeScript 开发。Vue.js 1.x 版本的源码是没有用类型语言的,小右用 JavaScript 开发了整个框架,但对于复杂的框架项目开发,使用类型语言非常有利于代码的维护,因为它可以在编码期间帮你做类型检查,避免一些因类型问题导致的错误;也可以利于它去定义接口的类型,利于 IDE 对变量类型的推导。

性能优化

性能优化一直是前端老生常谈的问题。那么对于 Vue.js 2.x 已经足够优秀的前端框架,它的性能优化可以从哪些方面进行突破呢?

1. 源码体积优化

首先是源码体积优化,我们在平时工作中也经常会尝试优化静态资源的体积,因为 JavaScript 包体积越小,意味着网络传输时间越短,JavaScript 引擎解析包的速度也越快。

那么,Vue.js 3.0 在源码体积的减少方面做了哪些工作呢?

  • 首先,移除一些冷门的 feature(比如 filter、inline-template 等);
  • 其次,引入 tree-shaking 的技术,减少打包体积。

tree-shaking 依赖 ES2015 模块语法的静态结构(即 import 和 export),通过编译阶段的静态分析,找到没有引入的模块并打上标记。

举个例子,一个 math 模块定义了 2 个方法 square(x) 和 cube(x) :

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

我们在这个模块外面只引入了 cube 方法:

1
2
import { cube } from './math.js'
// do something with cube

最终 math 模块会被 webpack 打包生成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
/* 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 模块被标记了, 然后压缩阶段会利用例如 uglify-js、terser 等压缩工具真正地删除这些没有用到的代码。

也就是说,利用 tree-shaking 技术,如果你在项目中没有引入 Transition、KeepAlive 等组件,那么它们对应的代码就不会打包,这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。

2.数据劫持优化

其次是数据劫持优化。Vue.js区别于React的一大特色是它的数据是响应式的,这个特性从Vue.js 1.x版本就一直伴随着。DOM是数据的一种映射,数据发生变化可以自动更新DOM

在vue内部,想实现这个功能是要付出一定代价的,那就是必须劫持数据的访问和更新。其实这点很好理解,当数据改变后,为了自动更新dom,那么就必须劫持数据的更新,也就是说当数据发生改变后能自动执行一些代码去更新DOM,那么问题来了,Vue怎么知道更新那一片dom?因为在渲染dom的时候访问了数据,我们可以对它进行访问劫持,这样就在内部建立了依赖关系,也就知道对应的dom是什么了。

Vue.js 1.x 和 Vue.js 2.x 内部都是通过 Object.defineProperty 这个 API 去劫持数据的 getter 和 setter,具体是这样的:

1
2
3
4
5
6
7
8
Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})

但这个API有一些缺陷,它必须预先知道要拦截的key是什么,所以它并不能检测对象属性的添加和删除。尽管vue为了解决这个问题提供了$set和$delete实例方法,但是对于用户来说,还是增加了一定的心智负担

另外Object.defineProperty 的方式还有一个问题,举个例子,比如这个嵌套层级比较深的对象:

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

由于 Vue.js 无法判断你在运行时到底会访问到哪个属性,所以对于这样一个嵌套层级较深的对象,如果要劫持它内部深层次的对象变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的。毫无疑问,如果我们定义的响应式数据过于复杂,这就会有相当大的性能负担。

为了解决上述 2 个问题,Vue.js 3.0 使用了 Proxy API 做数据劫持,它的内部是这样的:

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

由于它劫持的是整个对象,那么自然对于对象的属性的增加和删除都能检测到。

但要注意的是,Proxy API 并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归,这样无疑也在很大程度上提升了性能,我会在后面分析响应式章节详细介绍它的具体实现原理。

语法 API 优化:Composition API

除了源码和性能方面,Vue.js 3.0 还在语法方面进行了优化,主要是提供了 Composition API

1.优化逻辑组织

首先,是优化逻辑组织

在 Vue.js 1.x 和 2.x 版本中,编写组件本质就是在编写一个“包含了描述组件选项的对象”,我们把它称为 Options API,它的好处是在于写法非常符合直觉思维,对于新手来说这样很容易理解,这也是很多人喜欢 Vue.js 的原因之一。

Options API 的设计是按照 methods、computed、data、props 这些不同的选项分类,当组件小的时候,这种分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用 Options API 的时候,每一个关注点都有自己的 Options,如果需要修改一个逻辑点关注点,就需要在单个文件中不断上下切换和寻找。

Vue.js 3.0 提供了一种新的 API:Composition API,它有一个很好的机制去解决这样的问题,就是将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。

2.优化逻辑复用

其次,是优化逻辑复用

当我们开发项目变得复杂的时候,免不了需要抽象出一些复用的逻辑。在 Vue.js 2.x 中,我们通常会用 mixins 去复用逻辑,举一个鼠标位置侦听的例子,我们会编写如下函数 mousePositionMixin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

然后在组件中使用:

1
2
3
4
5
6
7
8
9
10
11
<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 的这两个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 }
}

这里我们约定 useMousePosition 这个函数为 hook 函数,然后在组件中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题。

Composition API 除了在逻辑复用方面有优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。