vue.js 3.0主要做了哪些优化
Vue.js
从1.x
到2.0
版本,最大的升级就是引入了虚拟DOM
的概念,它为后续做服务端渲染以及跨端框架Weex
提供了基础。
Vue.js 2.0的不足之处:
1. 源码自身的维护性
数据量大后带来的渲染和更新的性能问题
2. 兼容性
为了兼容一直保留的鸡肋API
1. 源码优化
对于Vue.js框架本身开发的优化,它的目的事让代码更易于开发和维护。
源码的优化主要体现在使用monorepo
和TypeScript
管理和开发源码,这样做的目的是提升自身代码可维护性
1. 更好的代码管理方式:monorepo
- 相对于Vue.js 2.x的源码组织方式,monorepo把这些模块拆分到不同的package中
每个package有各自的API、类型定义和测试
这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确
开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性 - package(比如reactivity响应式库)是可以独立于Vue.js使用的
这样用户如果只想使用Vue.js 3.0的响应式能力,可单独依赖这个响应式库而不用去依赖整个Vue.js,减小了引用包的体积大小
2. 有类型的Javscript:TypeScript
- 编码期间做类型检查
避免一些应为类型问题导致的错误 - 有利于它去定义接口的类型,利于IDE对变量类型的推导
Flow
- Flow是Facebook出品的JavaScript静态类型检查工具,它可以以
非常小的成本
对已有的JavaScript代码迁入,非常灵活,这也是Vue.js 2.0当初选型它是一方面的考量 - Flow对于一些复杂场景类型的检查,支持并不好
2. 性能优化
1. 源码体积的优化
- 移除一些冷门的feature
Eg:filter、inline-template - 引入tree-shaking的技术,减少打包体积
tree-shaking
依赖ES2015
模块语法的静态结构(即import
和export
),通过编译阶段的静态分析,找到没有引入的模块并打上标记
Eg:
math.js
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
模块被标记了, 然后压缩阶段会利用例如uglify-js
、terser
等压缩工具真正地删除这些没有用到的代码。
也就是说,利用tree-shaking
技术,如果你在项目中没有引入Transition
、KeepAlive
等组件,那么它们对应的代码就不会打包,这样也就间接达到了减少项目引入的Vue.js
包体积的目的。
2. 数据劫持优化
- 实现DOM功能,必须劫持数据的访问和更新
当数据改变后,为了自动更新DOM,就必须劫持数据的更新,也就是说,当数据发生改变后能自动执行一些代码去更新DOM - Vue.js怎么知道更新哪一片DOM呢?
因为在渲染DOM的时候访问了数据,我们可以对它进行访问劫持,这样就在内部建立了依赖关系,也就知道数据对应的DOM是什么了
Vue.js 1.x
和Vue.js 2.x
内部都是通过 Object.defineProperty
这个API
去劫持数据的getter
和setter
,具体是这样的:
Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})
但这个API
有一些缺陷,它必须预先知道要拦截的key
是什么,所以它并不能检测对象属性的添加和删除。尽管Vue.js
为了解决这个问题提供了$set
和$delete
实例方法,但是对于用户来说,还是增加了一定的心智负担。
另外Object.defineProperty
的方式还有一个问题,举个例子,比如这个嵌套层级比较深的对象:
export default {
data: {
A: {
B: {
C: {
D: 1
}
}
}
}
}
由于Vue.js
无法判断你在运行时到底会访问到哪个属性,所以对于这样一个嵌套层级较深的对象,如果要劫持它内部深层次的对象变化,就需要递归遍历这个对象,执行Object.defineProperty
把每一层对象数据都变成响应式的。毫无疑问,如果定义的响应式数据过于复杂,这就会有相当大的性能负担。
为了解决上述 2 个问题,Vue.js 3.0
使用了Proxy API
做数据劫持,它的内部是这样的:
observed = new Proxy(data, {
get() {
// track
},
set() {
// trigger
}
})
由于它劫持的是整个对象,那么自然对于对象的属性的增加和删除都能检测到。
但要注意的是,Proxy API
并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在getter
中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归,这样无疑也在很大程度上提升了性能
3.编译优化
除了数据劫持部分的优化,我们可以在耗时相对较多的patch
阶段想办法,通过在编译阶段优化编译的结果,实现运行时patch过程的优化。Vue.js 2.x
的数据更新并触发重新渲染的粒度是组件级的;
Block tree
Block tree
是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,每个区块只需要一个Array
来追踪自身的动态节点- 借助
Block tree
,Vue.js
将vnode
更新性能由与模块整体大小相关提升为与动态内容的数量相关
除此之外,Vue.js 3.0
在编译阶段还包含了对Slot
的编译优化、事件侦听函数的缓存优化,并且在运行时重写了diff
算法
4.语法API优化:Composition API
1. 优化逻辑组织
在Vue.js 1.x
和2.x
版本中,编些组件本质就是在编写一个‘包含了描述组件选项的对象’,称为Options API
Option API
Option API
的设计是按照methods
,computed
,data
,props
这些不同的选项分类- 当组件小的时候,这些分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用Options API的时候,每个关注点都有自己的Options,如果要修改一个逻辑关注点,就需要在单个文件中不断上下切换和寻找
在Vue.js 3.0
中,提供了一中新的API:Composition API,就是将某个关注点相关的代码全都放在一个函数里面,这样当需要修改一个功能时,就不再需要在文件中跳来跳去
2.优化逻辑复用
当开发项目变得复杂的时候,免不了需要抽象出一些复用的逻辑。
在Vue.js 2.x
中,我们通常会用 mixins 去复用逻辑,举一个鼠标位置侦听的例子,我们会编写如下函数 mousePositionMixin:
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
都可以定义自己的props
、data
,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突 - 对组件而言,如果模板中使用不在当前组件中的变量,那么就不会太容易知道这些变量在哪里定义的,这就是数据来源不清晰
但是Vue.js 3.0
设计的Composition API
,就很好地帮助我们解决了mixins
的这两个问题。
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
函数,然后在组件中使用:
<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>
- 除了在逻辑方面有优势,也会有更好的类型支持
- 因为它们都是一些函数,在调用函数时,自然有的类型就被推导出来了,不像
Options API
所有的东西都是使用this - Composition API对tree-shaking友好,代码也更容易压缩
Composition API
属于API
的增强,它并不是Vue.js 3.0
组件开发的范式,如果组件足够简单,还是可以使用Options API
5. 引入RFC
:使每个版本改动可控
RFC(Request For Comments),旨在为新功能进入框架提供一个一致且受控的路径
Vue.js 3.0
中大规模启用RFC(Request For Comments),了解每一个feature采用或被废弃的原因