Vue3.0新特性

Composition API

在Vue2.x中,一个功能的相关逻辑可能要分别写在data、methods、computed、watch中。就像下图所展示的一样:

这导致每次同一功能的代码逻辑时,都要在不同函数之间跳来跳去。这种情况会随着代码量的增多而变得愈发严重,变得可读性极差。

最佳的代码逻辑应该像下图这样,每种颜色代表一个功能:

这早在vue2.x就给出过解决方案,那便是mixin,但mixin也同样存在着各种问题,如:

  • 命名冲突

  • 暴露出来的成员作用不明

  • 逻辑重用到其他component中时,问题频出

于是vue3.x给出了新的解决方案,那就是CompasitionAPI。

  1. setup

setup是实现CompasitionAPI的入口函数,本质上是vue的一个生命周期函数,它的执行顺序在beforeCreate和created之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {

beforeCreate() {

console.log('beforeCreate');

},

created() {

console.log('created');

},

setup() {

console.log('setup');

}

}

输出结果:

1
2
3
4
5
setup

beforeCreate

created

setup有两个传入参数,分别是:

  1. props

props是响应式的,当新的props传入,会及时更新。但千万不能使用ES6结构props,这样会导致props失去响应性,比如下面这段代码:

1
2
3
4
5
6
7
setup(props, context) {

const { name } = props;

console.log(name);

}

如果非要解构的同时保持响应性,就需要用到后文提到的toRefs。

  1. context

context代替了vue2.x中的this对象,提供了attrs、slot和emit,对应this中的$attr属性,slot插槽,$emit发射事件三个成员。

  1. reactive、ref与toRefs

在vue2.x中,所有数据都必须定义在data
中。而在vue3.x中,数据可以在代码中通过reactive和ref进行定义。其中ref可以用来保存所有类型的数据,包括对象,想如下代码这样:

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
import { ref } from 'vue';

export default {

setup() {

const obj = ref({ count: 1, name: "张三" });

setTimeout(() => {

obj.value.count = obj.value.count + 1;

obj.value.name = "李四";

}, 1000);

return {

obj

};

}

}

而reactive只能保存对象,不能保存基本数据类型,所以一般会用reactive来保存对象,用ref来保存基本数据类型。

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
<template>

<div>

<p>{{ year }}</p>

<p>姓名:{{ user.nickname }}</p>

<p>年龄:{{ user.age }}</p>

</div>

</template>

<script>

import { ref, reactive } from 'vue';

export default {

const year = ref(0);

const user = reactive({ nickname: 'Mike', age: 26, gender: '男' });

setInterval(() => {

year.value ++;

user.age ++;

}, 1000);

return {

year,

user

}

}

</script>

不过在html中每次都要通过user来访问属性,会比较麻烦。但是如果直接解构user,就会让user丧失响应性。因此这时就需要用到toRefs:

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
<template>

<div>

......

<p>姓名: {{ nickname }}</p>

<p>年龄: {{ age }}</p>

</div>

</template>

<script>

......

return {

year,

toRefs(user)

}

</script>
  1. 生命周期函数

下面是vue3.x的生命周期函数完整流程图:

通过下图的对比,可以很清楚地看到两个版本之间的变动:

具体有以下变动:

  1. beforeCreate和created被setup代替,但依然可以使用

  2. destroy改名为unmount

  3. 大部分钩子前面会加on

  4. 新增了两个调试用的钩子:onRenderTriggered和onRenderTracked。

  1. watch和watchEffect

watch用来监听数据的变化,当数据变化时它会立刻触发。

watch有三个参数:source,callback和options。

其中source是要侦听的数据,

callback是数据变化时会触发的回调

options则可以设置deep、immediate和flush选项。

  1. 侦听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
import { reactive, toRefs, watch } from 'vue';

export default {

setup() {

const state = reactive({ nickname: 'xiaofan', age: 23 });

setTimeout(() => {

state.age ++;

}, 1000);

watch(() => state.age,

(newVal, oldVal) => {

console.log('新值:' + newVal + ',旧值:' + oldVal);

});

return {

...toRefs(state)

}

}

}

可以注意到,当侦听reactive的数据时,需要把数据放到一个function之中

  1. 侦听ref数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { ref, watch } from 'vue';

export default {

const year = ref(0);

setTimeout(() => {

year.value ++;

}, 1000);

watch(year, (newVal, oldVal) => {

console.log('新值:' + newVal + ',旧值:' + oldVal);

});

}

侦听ref数据不需要包一层function

  1. 侦听多个数据
1
2
3
4
5
6
7
8
9
watch([() => year, state.age],

([newVal1, newVal2], [oldVal1, oldVal2]) => {

console.log('新值1:' + newVal1 + ',旧值1:' + oldVal1);

console.log('新值2:' + newVal2 + ',旧值2:' + oldVal2);

});
  1. 侦听复杂的嵌套对象
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
const state = reactive({

room: {

id: 100,

attrs: {

size: '140平米 ',

type: '两室一厅'

}

}

});

watch(

() => state.room,

(newVal, oldVal) => {

console.log('新值:' + newVal + ',旧值:' + oldVal);

},

{ deep: true }

);

当deep是true时,才可以深入数据内部进行侦听,否则size属性变化了,watch是不会被触发。

  1. stop停止侦听

watch运行之后,会返回一个停止函数,保存下来调用即可:

1
2
3
4
5
6
7
const stopWatch = watch(...);

setTimeout(() => {

stopWatch();

}, 3000);

watchEffect跟watch功能上的是一样的,区别在于:

  1. 不需要传入以来

  2. 会执行一次来自动收集依赖

  3. 只获取变化后的值,无法得知变化前的值

因此相对少用一点。

响应式

vue2.x中响应式会莫名失效的根源就在于使用Object.defineProperty来实现功能。

使用Object.defineProperty来实现响应式有两个问题:

  1. Object.defineProperty只能劫持对象的属性,并且需要遍历对象的每一个属性,如果属性也是个对象,那就需要进行深度遍历。

  2. Object.defineProperty对新增属性需要手动进行Observe。

对象新增属性时,Object.defineProperty就需要对对象重新进行遍历,以劫持新增的属性。所以才有了$set

在vue3.x中,已经弃用了Object.defineProperty,使用Proxy代理对象,所以不存在这些烦恼。

Teleport

Teleport意即传送门,可以把组件挂在到指定的位置,并保持内部属性值不变。

案例如下:

  1. 首先在index.html中挂一个占位符,id定为dialog,等待挂载:
1
2
3
4
5
6
7
<body>

<div class="app"></div>

<div class="dialog"></div>

</body>
  1. 接着实现一个Dialog组件
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
<template>

<teleport to="#dialog">

<div class="dialog">

<div class="dialog_wrapper">

<div class="dialog_header" v-if="title">

<slot name="header">

<span>{{ title }}</span>

</slot>

</div>

</div>

<div class="dialog_content">

<slot></slot>

</div>

<div class="dialog_footer">

<slot name="footer"></slot>

</div>

</div>

</teleport>

</template>

其中teleport的to属性要跟上一步div的id保持一致,并在前面加个#。

  1. 在Header组件中渲染Dialog组件
1
2
3
4
5
6
7
8
9
<div class="header">

...

<navbar />

<Dialog v-if="dialogVisible"/>

</div>

这时,原本嵌套在Header组件中的Dialog组件,就被渲染到index.html下面,与app同级。这样做就可以很方便地处理该组件的定位、z-index和样式。同时内部属性值dialogVisible也不会重置。

Suspense

在vue2.x,如果想给一个尚未加载的组件添加加载条,可以这么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>

<div>

<div v-if="!loading">

...

</div>

<div v-if="loading">

加载中...

</div>

</div>

</template>

而在vue3.x中,提供了Suspense来实现这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Suspense>

<template #default>

<async-component></async-component>

</template>

<template #fallback>

<div>

Loading...

</div>

</template>

</Suspense>

Suspense实际上就是提供了两个slot插槽,刚开始会渲染#fallback中的内容,达到某个条件后就会变成#default。

这个功能最早来自vue2.x的插件vue-async-manager。

最后实现async-component组件:

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
<template>

<div>

<h4>这是一个异步加载组件</h4>

<p>用户名:{{ user.nickname }}</p>

<p>年龄:{{ user.age }}</p>

</div>

</template>

<script>

import axios from 'axios';

export default {

async setup() {

const rawData = await axios.get('http://xxxx.com/user');

return {

user: rawData.data

}

}

}

</script>

只要异步方法setup获得数据,Suspense就会将async-component组件渲染出来。

片段Fragment

vue2.x中,组件只允许有一个根节点:

1
2
3
4
5
<template>

<div>...</div>

</template>

但在vue3.x中就没有这个限制:

1
2
3
4
5
6
7
<template>

<span></span>

<span></span>

</template>

更好的Tree-shaking

在vue2.x中,如果想调用nextTick,就必须像下面这样:

1
2
3
4
5
6
7
import Vue from "vue";

Vue.nextTick(() => {

...

})

vue3.x则在Tree-shaking的基础上重构所有API,让我们可以直接调用全局API:

1
2
3
4
5
6
7
import { nextTick } from 'vue';

nextTick(() => {

...

});

自定义指令

vue3.x中对自定义指令的钩子函数进行语义化修改,如下图所示:

新语义案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from 'vue';

const app = createApp({});

app.directive('focus', {

mouted(el) {

el.focus();

}

});

实际使用:

1
<input v-slot />

slot插槽

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<slot name="content" :data="data"></slot>

export default {

data() {

return {

data: ["走过来人来人往", "sadasdasd", "d32e324"]

}

}

}

vue2.x中具名插槽和作用域插槽分别使用slot和slot-scoped的实现,vue3.x则可以将它们合并起来:

1
2
3
4
5
<template v-slot:content="scoped">

<div v-for="item in scoped.data">{{ item }}</div>

</template>

可以简写为:

1
2
3
4
5
<template #content="{data}">

<div v-for="item in data">{{ item }}</div>

</template>

v-model

vue3.x中v-model有以下变化:

  • 在自定义组件上使用v-model时,属性以及事件的默认名称都变了

  • 属性:value => modelValue

  • 事件:input => update:modelValue

  • v-bind的.sync修饰符和组件的model选项被弃用,取而代之的是v-model的参数

  • 支持同时定义多个v-model

  • 可以自定义v-model修饰符

  1. 如何修改v-model中的默认属性和事件

在vue2.x的版本中,使用v-model相当于绑定value值,并触发input事件

1
<MyInput v-model="name" />

相当于:

1
<MyInput :value="name" @input="name = $event" />

这时如果有开发者不想使用value来传递值,也不想触发input,那就可以像这样子定义:

1
2
3
4
5
6
7
8
9
10
11
export default {

model: {

prop: 'title',

event: 'change'

}

}

这样定义,其实就相当于:

1
<MyInput :title="name" @change="name = $event" />

而在vue3.x版本中,直接这样定义即可:

1
<MyInput v-model:title="name" />

这相当于:

1
<MyInput :title="name" @update:title="name = $event" />
  1. 如何实现prop的双向绑定

有些场景,我们需要实现对组件属性的双向绑定,比如一个弹窗组件,在组件的内部跟外部都可以控制visible属性的显示跟隐藏。

在vue2.x版本中,官方推荐用update:propName跟.sync配合使用来实现该功能:

第一步是在内部emit update:title函数

1
this.$emit('update:title', '哈哈哈哈');

接着在外部监听这个事件:

1
<MyInput :title="name" @update:title="name = $event" />

官方提供了.sync修饰符来简化这个监听:

1
<MyInput :title.sync="name" />

而在vue3.x版本中,我们可以:

1
<MyInput v-model:title="name" v-model:content="info" />

v-model在内部已经实现了对prop的双向绑定

异步组件

vue3.x中可以使用defineAsyncComponent异步加载组件:

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
<template>

<AsyncPage />

</template>

<script>

import { defineAsyncComponent } from 'vue';

export default {

components: {

// 无配置项的异步组件

AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),

// 有配置项的异步组件

AsyncPageWithOptions: defineAsyncComponent({

loader: () => import('./NextPage.vue'),

delay: 200,

timeout: 3000,

errorComponent: () => import('./ErrorComponent.vue'),

loadingComponent: () => import('./LoadingComponent.vue')

})

}

}

</script>

参考:

Vue3.0 新特性以及使用经验总结

Vue 3 到底有什么不同:v-model 升级了


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!