Vue3的优化

语法API优化

Vue3的合成型API(Composition API)

Vue2使用选项类型API(Options API)
Vue3使用合成型API(Composition API)

Vue核心团队将Composition API描述为“一组基于功能的附加API,可以灵活地组合组件逻辑”。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!--   vue2 -->
<template>
<div class='form-element'>
<h2> {{ title }} </h2>
<input type='text' v-model='username' placeholder='Username' />
<input type='password' v-model='password' placeholder='Password' />
<button @click='login'>Submit</button>
<p> Values: {{ username + ' ' + password }} </p>
</div>
</template>
<script>
export default {
props: {
title: String
},
data () {
return {
username: '',
password: ''
}
},
mounted () {
console.log('title: ' + this.title)
},
computed: {
lowerCaseUsername () {
return this.username.toLowerCase()
}
},
methods: {
login () {
this.$emit('login', {
username: this.username,
password: this.password
})
}
}
}
</script>

vue Composition文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!-- vue3 -->
<template>
<div class='form-element'>
<h2> {{ state.title }} </h2>
<input type='text' v-model='state.username' placeholder='Username' />
<input type='password' v-model='state.password' placeholder='Password' />
<button @click='login'>Submit</button>
<p> Values: {{ state.username + ' ' + state.password }} </p>
</div>
</template>
<script>
import { reactive, onMounted, computed, ref, watch } from 'vue'

export default {
props: {
title: String
},
// 组件实例被创建时,初始化props的解析后立刻调用。在生命周期中,它是在 beforeCreate 之前。
// 第一个参数是props.第二个参数是 context 对象(attrs, emit, slots),它代替了 vue2 中通过 this 进行一些操作。
setup (props, { emit }) {
const state = reactive({
username: '',
password: '',
users: []
lowerCaseUsername: computed(() => state.username.toLowerCase())
})
// 关于生命周期钩子函数,如果在setup()使用,只需要导入 onXxxx(名字和原来vue2中的生命周期中名字一致)
// 新增两个调试狗子onRenderTracked onRenderTriggered
onMounted(() => {
console.log('title: ' + props.title)
})

const getUserRepositories = async () => {
users.push(state.username)
}

// 在用户prop的响应式引用上设置一个侦听器
watch(state.username, getUserRepositories)

const counter = ref(0)
console.log(counter.value) // 0
const login = () => {
emit('login', {
username: state.username,
password: state.password
})
}

return {
login,
state
}
}
}
</script>

上面的setup它只是一个函数,向模板返回属性和函数。就是这样。我们在这里声明所有的反应式属性、计算属性、watchers和生命周期钩子,然后返回它们,这样它们就可以在模板中使用。没有从setup函数中返回的东西将不能在模板中使用

作用及意义

1.优化逻辑组织

Options API 的设计是按照 methods、computed、data、props 这些不同的选项分类,当组件小的时候,这种分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用 Options API 的时候,每一个关注点都有自己的 Options,如果需要修改一个逻辑点关注点,就需要在单个文件中不断上下切换和寻找。
Composition API,它有一个很好的机制去解决这样的问题,就是将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。

2.优化逻辑复用

当我们开发项目变得复杂的时候,免不了需要抽象出一些复用的逻辑。在 Vue.js 2.x 中,我们通常会用 mixins 去复用逻辑

  1. mixin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 定义一个混入对象
    var myMixin = {
    created: function () {
    this.hello()
    },
    methods: {
    hello: function () {
    console.log('hello from mixin!')
    }
    }
    }
    // 定义一个使用混入对象的组件
    var Component = Vue.extend({
    mixins: [myMixin]
    })
    var component = new Component() // => "hello from mixin!"

    使用mixin最大的缺点是我们不知道到底在组件内加了什么,导致命名冲突数据来源不清晰。。

  2. solt插槽
    solt虽然可以插入代码但是只能在标签内使用,局限性较大。

使用合成API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//  a.js
import { ref } from 'vue'
export default function useCounter() {
const count = ref(0)
function increment () { count.value++ }

return {
count,
incrememt
}
}
// b.vue
import useCounter from './a.js'
export default {
setup () {
const { count, increment } = useCounter()
return {
count,
increment
}
}
}

组件的更新

teleport 组件

teleport 组件它只是单纯的把定义在其内部的内容转移到目标元素中,在元素结构上不会产生多余的元素,当然也不会影响到组件树,它相当于透明的存在。

1
2
3
<teleport to="body">
<Dialog ref="dialog"></Dialog>
</teleport>

把 dialog 组件渲染的这部分 DOM 挂载到 body 下面,不会受到父级样式的影响。
为什么要有这个组件?
为了有更好的代码组织体验。比如:有时,组件模板的一部分在逻辑上属于此组件,但从技术角度来看(如:样式化需求),最好将模板的这一部分移动到 DOM 中的其他位置。例如我们常用的弹窗、悬浮框、全局提示。

Suspense 异步组件

它会暂停你的组件渲染,在异步组件加载完成并完全渲染之前 suspense 会先显示 #fallback 插槽的内容 。

1
2
3
4
5
6
7
8
<Suspense>
<template >
<Suspended-component />
</template>
<template #fallback>
Loading...
</template>
</Suspense>

模板指令更新

  1. 组件上 v-model 用法已更改
    在 2.x 里,使用 v-model 等同于向组件传递一个 value 属性,同时监听一个 input 事件:
    1
    2
    3
    4
    5
    <ChildComponent v-model="pageTitle" />

    <!-- 是以下代码的简写: -->

    <ChildComponent :value="pageTitle" @input="pageTitle = $event" />
    Vue2只支持绑定一个值,绑定多个值需要使用v-bind.sync
    <ChildComponent :foo.sync="bar"></ChildComponent>
    <ChildComponent :foo="bar" @update:foo="val => bar = val"></ChildComponent>
    在3.x中
    1
    2
    3
    4
    5
    6
    7
    8
    <ChildComponent v-model="pageTitle" />

    // 是以下代码的简写

    <ChildComponent
    :modelValue="pageTitle"
    @update:modelValue="pageTitle = $event"
    />
    要修改一个model的名称的话,现在我们可以给 v-model 传递一个 参数 以取代此前的 model 选项:
    1
    2
    3
    <ChildComponent v-model:title="pageTitle" />
    //是以下代码的简写:
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    还支持自定义修饰符
  2. <template v-for> 和非 - v-for 节点上 key 用法已更改
  3. 在同一元素上使用的 v-if 和 v-for 优先级已更改。(3.x 版本中 v-if 总是优先于 v-for 生效。)
  4. v-bind=”object” 现在排序敏感
    1
    2
    3
    4
    <!-- template2.x -->
    <div id="red" v-bind="{ id: 'blue' }"></div>
    <!-- result -->
    <div id="red"></div>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- template3.x -->
    <div id="red" v-bind="{ id: 'blue' }"></div>
    <!-- result -->
    <div id="blue"></div>

    <!-- template3.x -->
    <div v-bind="{ id: 'blue' }" id="red"></div>
    <!-- result -->
    <div id="red"></div>
  5. v-for 中的 ref 不再注册 ref 数组

其他改变

  1. destroyed 生命周期选项被重命名为 unmounted
  2. beforeDestroy 生命周期选项被重命名为 beforeUnmount
  3. prop default 工厂函数不再有权访问 this 是上下文
  4. 自定义指令 API 已更改为与组件生命周期一致
  5. data 应始终声明为函数(Vue2是可以写为object的)
  6. 来自 mixin 的 data 选项现在可简单地合并
  7. attribute 强制策略已更改
  8. 一些过渡 class 被重命名
  9. 组建 watch 选项和实例方法 $watch 不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。
  10. <template> 没有特殊指令的标记 (v-if/else-if/else、v-for 或 v-slot) 现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容。
  11. 在 Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分。

移除的 API

  1. keyCode 支持作为 v-on 的修饰符
  2. $on,$off 和 $once 实例方法
  3. 过滤(filter)
  4. 内联模板 attribute
  5. $destroy 实例方法。用户不应再手动管理单个 Vue 组件的生命周期。

源码优化

源码管理方式

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

Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了 compiler(模板编译的相关代码)、core(与平台无关的通用运行时代码)、platforms(平台专有代码)、server(服务端渲染的相关代码)、sfc(.vue 单文件解析相关代码)、shared(共享工具代码) 等目录:

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

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

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

TypeScript

重构 Vue2.0 的时候,使用了 Flow 工具,但是在 Vue.js 3.0 的时候抛弃 Flow 转而采用 TypeScript 重构了整个项目
原因主要是TypeScript提供了更好的类型检查,能支持复杂的类型推导;

性能优化

源码体积优化

  1. 移除一些冷门的 feature(比如 filter、inline-template 等);

  2. 在 Vue 3 中,全局和内部 API 都经过了重构, 加强对 tree-shaking 技术的支持,减少打包体积。
    如果在项目中没有引入 Transition、KeepAlive 等组件,那么它们对应的代码就在webpack打包时会被标记,然后压缩阶段会利用例如 uglify-js、terser 等压缩工具真正地删除这些没有用到的代码。这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。

数据劫持优化

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
}
})

它必须预先知道要拦截的 key 是什么,所以它并不能检测对象属性的添加和删除。尽管 Vue.js 为了解决这个问题提供了 $set 和 $delete 实例方法,但是对于用户来说,还是增加了一定的心智负担。
其次就是如果对象嵌套层级过多的话,如果要劫持内部属性的话就需要递归整个对象,对性能也有一部分影响。

reactive –> Proxy

Vue.js 3.0 使用了 Proxy API 做数据劫持,主要通过 reactive 创建响应式数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function reactive (target) {
// 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
if (target && target.__v_isReadonly) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if (!isObject(target)) {
// 目标必须是对象或数组类型
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
return target
}
if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
// target 已经有对应的 Proxy 了
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// 只有在白名单里的数据类型才能变成响应式
if (!canObserve(target)) {
return target
}
// 利用 Proxy 创建响应式
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
// 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
return observed
}

// 其实就是劫持对象进行一些操作
const mutableHandlers = {
get, // 访问对象属性会触发 get 函数;
set, // 设置对象属性会触发 set 函数;
deleteProperty, // 删除对象属性会触发 deleteProperty 函数;
has, // in 操作符会触发 has 函数;
ownKeys //通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。
}

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* isReactive */) {
// 代理 observed.__v_isReactive
return !isReadonly
}
else if (key === "__v_isReadonly" /* isReadonly */) {
// 代理 observed.__v_isReadonly
return isReadonly;
}
else if (key === "__v_raw" /* raw */) {
// 代理 observed.__v_raw
return target
}
const targetIsArray = isArray(target)
// arrayInstrumentations 包含对数组一些方法修改的函数
// 如果 target 是数组且 key 命中了 arrayInstrumentations,则执行对应的函数
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 求值
const res = Reflect.get(target, key, receiver)
// 内置 Symbol key 不需要依赖收集
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
// 依赖收集
!isReadonly && track(target, "get" /* GET */, key)
return isObject(res)
? isReadonly
?
readonly(res)
// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
: reactive(res)
: res
}
}

const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function (...args) {
// toRaw 可以把响应式对象转成原始数据
const arr = toRaw(this)
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集
track(arr, "get" /* GET */, i + '')
}
// 先尝试用参数本身,可能是响应式数据
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果失败,再尝试把参数转成原始数据
return arr[key](...args.map(toRaw))
}
else {
return res
}
}
})

/**
每次 track ,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。
*/
// 是否应该收集依赖
let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每个 target 对应一个 depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep 集合
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
// 我们的目的是实现响应式,就是当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数。
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}

set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const set = /*#__PURE__*/ createSetter()

function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}

const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}

/**
trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。
*/
// 原始数据对象 map
const targetMap = new WeakMap()
function trigger(target, type, key, newValue) {
// 通过 targetMap 拿到 target 对应的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回
return
}
// 创建运行的 effects 集合
const effects = new Set()
// 添加 effects 的函数
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// SET | ADD | DELETE 操作之一,添加对应的 effects
if (key !== void 0) {
add(depsMap.get(key))
}
const run = (effect) => {
// 调度执行
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
// 直接运行
effect()
}
}
// 遍历执行 effects
effects.forEach(run)
}

关于proxy

  1. 由于它劫持的是整个对象,那么对于对象的属性的增加和删除都能检测到。
  2. Proxy API 并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归,这样无疑也在很大程度上提升了性能

diff算法优化

静态节点标记

它通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破

Slot的编译优化

Vue2.x中,如果有一个组件传入了slot,那么每次父组件更新的时候,必定会强制使子组件update,造成性能的浪费。这是由于2.x中,组件的插槽会被当成组件的一个普通children,因此在2.x里面的处理就是只要一个component中传入了slot,那么如果父组件更新,必定会update子组件。
Vue3优化了Slot的生成,父子组件可以单独更新。将所有的slot统一编译一个函数,当我们要将一个slot传给子组件的时候,将这个函数传给子组件,而调用这个函数,是子组件的行为,所以依赖的变化,就成为了子组件的而不是父组件的,所以当依赖变动时,我们只需要重新渲染子组件,而不必再去渲染父组件了

事件侦听函数的缓存优化

比如对onclick进行一个缓存处理,当第一次渲染的时候,因为不存在_cache[1],所以vue会自动生成一个内联函数,给cache[1]赋值_cache[1] = () => {},使我们能自动去调用组件上最新的onclick。在后续的更新中,我们只需要从缓存_cache[1]中去读取同一个函数即可,而既然是同一个函数,那也没有被更新的必要了,所以@click也会被看成静态的。
这种优化在要给组件传入一个函数时尤为明显,如果不使用事件监听缓存的话,那么在父组件更新的时候,子组件就会跟着更新。而通过事件监听缓存机制,我们传给子组件的函数,它会在调用时动态地去找到组件中最新的那个函数,不需要对子组件进行更新。

ssr渲染优化

在SSR渲染时,会在服务端尽可能地将静态部分处理为字符串返回。如果存在动态节点,也会竟可能放在字符串里面,动态数据编译成模板字符串推入。