Vue.js 3.0 一些优化总结
目的
- 了解Vue.js 3.0 的升级给我们开发带来的收益
- 学习到一些设计思想和理念,并在自己的开发工作中应用以获得提升
Vue3会带来些什么?
- 更快:重构虚拟dom(编译优化),Proxy响应式对象
- 更小:Tree shaking
- 更易于维护:TS、代码管理和Composition API
源码优化
源码的优化主要体现在使用 monorepo 和 TypeScript 管理和开发源码,提升了代码可维护性。具体变化如下:
更好的代码管理方式:monorep
Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了:
- compiler(模板编译的相关代码)
- core(与平台无关的通用运行时代码)
- platforms(平台专有代码)
- server(服务端渲染的相关代码)
- sfc(.vue 单文件解析相关代码)
- shared(共享工具代码)
Vue.js 3.0整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中:
模块被拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样做的好处有以下几点:
- 模块拆分更细化,职责划分更明确
- 模块之间的依赖关系也更加明确
- 开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性
- 一些 package(reactivity响应式库等)是可以独立于 Vue.js 使用的。可单独依赖无需引用整个Vue.js
TypeScript
JavaScript 是动态类型语言,它的灵活性有目共睹,但是过于灵活的副作用是很容易就写出非常隐蔽的隐患代码,在编译期甚至看上去都不会报错,但在运行阶段就可能出现各种奇怪的 bug。
对于复杂的框架项目开发,使用类型语言非常有利于代码的维护,因为它可以在编码期间帮你做类型检查,避免一些因类型问题导致的错误。也可以去定义类和接口,便于IDE进行类型推断。
Vue.js 3.0 自身采用了 TypeScript 进行开发。Vue2.x采用FaceBook出品的 JavaScript 静态类型检查工具Flow进行管理。但是 Flow 对于一些复杂场景类型的检查,支持得并不好,例如如下代码:
const propOptions: any = vm.$options.props
Flow 无法正确推导出 vm.$options.props 的类型,就不得不使用any进行强制声明。另外,社区里有人吐槽过Flow团队存在烂尾的可能。而TS有两个明显的优势:
- 有更好的类型检查,能支持复杂的类型推导
- TypeScript 的生态更加成熟和活跃,生命力更强
性能优化
源码体积优化
Vue.js 3.0 为了减少源码体积:
- 移除一些冷门的 feature
- 引入 tree-shaking 的技术,减少打包体积。
一些不常用的feature,例如filter:
<template>
<div>{{millisecond | format}}</div>
</template>
<script>
export default {
props : {
millisecond : 1516101106
},
filter: {
format(value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
}
// 3.x 中,过滤器已删除,不再支持。可以使用计算属性或调用方法替换他们
<template>
<h1>{{ format }}</h1>
</template>
<script>
export default {
props : {
millisecond : 1516101106
},
computed: {
format(value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
}
</script>
再来看看 tree-shaking。它的原理很简单:tree-shaking 依赖静态的 ES2015 模块化语法(import
和 export
导入导出)。通过编译阶段的静态分析,找到没有引入的模块并打上标记,之后在压缩阶段删除掉那些没用的代码。例如,有如下工具类util.js:
export function funcA() {
//do something
}
export function funB() {
//do something
}
我们在main.js里引用它:
import {funcA} from './util.js';
funcA();
在使用了tree-shaking之后最终打包后的util.js会变成下面的模样:
export function funcA() {
}
数据劫持优化
Vue3.0之前是通过Object.defineProperty 劫持数据的 getter 和 sette:
Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})
通过Object.defineProperty使普通对象变为响应式对象,核心原理就是在getter中收集依赖,在sette中触发事件。但是,该方法有个缺点:在进行劫持之前必须知道所要拦截的属性key是什么,因此,它不无法拦截对象的属性的增加和删除。用如下代码举例:
<div id="app">
<p>{{message.a}}</p>
<p>{{message.b}}</p>
</div>
<script>
const app = new Vue({
el: "#app",
data: {
message: {
a:""
}
},
mounted() {
//this.message.a = "A"
this.message.b = "B"
//this.$set(this.message,"b", "B")
}
})
</script>
如果单独使用 this.message.b = "B"
是无法触发页面更新的,Vue.js 为了解决这个问题提供了 set和set 和 set和delete 实例方法。但是这个明显给开发者带来了代码维护上的负担。
另外在面对嵌套比较深的对象时,Object.defineProperty需要通过递归遍历这个对象,把每一层的对象数据都变成响应式的,删减完整源码后大致如下
export function observe (value: any, asRootData: ?boolean): Observer | void {
ob = new Observer(value)
return ob
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//...
},
set: function reactiveSetter (newVal) {
//...
}
})
}
defineReactive
函数拿到 obj
的属性描述符,然后对子对象递归调用 observe
方法,这样就保证了无论 obj
的结构有多深,它的所有子属性也能变成响应式的对象,这样无论我们修改和使用任何属性,都能触发 getter 和 setter。最后利用 Object.defineProperty
去给 obj
的属性 key
添加 getter 和 setter。毫无疑问——如果定义的响应式数据过于复杂,这就会有相当大的性能负担。
Vue3.0 使用了 Proxy API 做数据劫持,其官方定义如下:
-
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
它劫持的是整个对象,实现结构结构如下:
observed = new Proxy(data, {
get() {
// track
},
set() {
// trigger
}
})
然而,虽然对于对象的属性的增加和删除Proxy都能检测到,但是并不能监听到内部深层次的对象变化,VUE3采用的方式依然是递归。但是,是在 getter 中去递归响应式,这样只有真正被访问到的内部对象才会变成响应式,而不是无差别全部递归。完整源码删减后如下:
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
// 求值
const res = Reflect.get(target, key, receiver)
// 依赖收集
!isReadonly && track(target, "get" /* GET */, key)
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
另外,Vue3使用了ref/reactive 替换了data中的变量。这样不仅使得变量定义更加灵活多变,而且避免了不必要的数据被转换为响应式数据。
语法优化:Composition API
Vue3之前,一直使用的是Options API,可以认为我们编写组件的工作就是对组件的配置项进行描述。 通过按照 methods、computed、data、props 等这些不同的选项进行分类,当组件小的时候,这种分类方式一目了然这样编写的代码简单直观,易于上手。
<script>
export default {
data() {
return {
balance: 0,
amount: 0
}
},
computed: {
balanceString() {
return `Account Balance: ${this.balance}`;
}
},
methods: {
addBalance() {
this.balance += this.amount
this.amount = 0;
},
subtractBalance() {
this.balance -= this.amount
this.amount = 0;
}
},
mounted() {
console.log('Application mounted');
},
}
</script>
但是在较大的组件中,可能包含有多个逻点,每一个关注点的相关代码分散在不同的配置项中,当想要需要修改一个逻辑时,就需要在单个文件中不断上下切换和寻找。Composition API的优化项之一就是解决这个问题。
Compositions API 提供Options API 欠缺的两种能力:
- 将相关代码片段组合在一起
- 可组合型使得复用代码更加的轻松方便
下面是使用了Compositions API的一段代码
<script setup>
import { onMounted, ref } from 'vue';
const amount = ref(0);
const balance = ref(0);
const balanceString = computed(() => `Account Balance: ${balance.value}`);
const addBalance = () => {
balance.value += amount.value;
amount.value = 0;
}
const subtractBalance = () => {
balance.value -= amount.value;
amount.value = 0;
}
onMounted(() => {
console.log('Application mounted');
});
</script>
在业务量较小时,无法直观的体现出组合式Api的优势。我们用蓝黄橙三色分别表示三个业务点,下图可以直观的反映出二者在编码上的不同:
其次,是优化逻辑复用。假如我们需要在组件内引入一个人员管理数据模块,在Vue2中可以通过mixin实现,首先编写如下代码:
const userList = {
data() {
return {
users: ['HU',"Liao","SI","ZHOU","DENG","WEI"]
}
},
methods: {
list() {
return this.users
},
addUser(user) {
this.users.add(user)
},
removeUser(user) {
this.users.remove(user)
}
}
}
export default userList
然后在组件中使用:
<template>
<div>
All user:{{ list }}
</div>
</template>
<script>
import userListMixin from './userList'
export default {
mixins: [userListMixin]
}
</script>
虽然使用起来非常方便,但是这种方法有两个问题:
- 命名冲突 :每个mixin 都可以定义自己的 props、data,它们之间是没有关联的,很容易定义相同的变量,导致命名冲突
- 数据来源不清晰:在模板中使用不在当前组件中定义的变量,不太容易知道这些变量在哪里定义
使用Compositions API 来实现上述功能:
export const useUserList = () => {
const list = ref('HU',"Liao","SI","ZHOU","DENG","WEI"]);
const addUser = (user) => {
list.value.push(user);
};
const remove = (user) => {
list.value.remove(user);
};
return {
list,
addUser,
removeUser,
};
};
<template>
<div>
All user : {{ users }}
</div>
</template>
<script>
import useMousePosition from './useUserList'
export default {
setup() {
const { list:users } = useUserList()
return { users }
}
}
</script>
</script>
整个数据来源清晰了,还可以通过解构任意命名,可以有效的避免命名冲突和数据来源不明确的问题。
最后,总结一下Composition API相对与Options API的优势:
- 有助于编写干净的代码,使得代码的可读性更高
- 可以将同一逻辑的代码片段组合在一起
- 让代码复用变的更轻松方便
转载自:https://juejin.cn/post/7208082990810153021