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

这导致每次同一功能的代码逻辑时,都要在不同函数之间跳来跳去。这种情况会随着代码量的增多而变得愈发严重,变得可读性极差。
最佳的代码逻辑应该像下图这样,每种颜色代表一个功能:

这早在vue2.x就给出过解决方案,那便是mixin,但mixin也同样存在着各种问题,如:
命名冲突
暴露出来的成员作用不明
逻辑重用到其他component中时,问题频出
于是vue3.x给出了新的解决方案,那就是CompasitionAPI。
- 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有两个传入参数,分别是:
- props
props是响应式的,当新的props传入,会及时更新。但千万不能使用ES6结构props,这样会导致props失去响应性,比如下面这段代码:
1 2 3 4 5 6 7
| setup(props, context) {
const { name } = props;
console.log(name);
}
|
如果非要解构的同时保持响应性,就需要用到后文提到的toRefs。
- context
context代替了vue2.x中的this对象,提供了attrs、slot和emit,对应this中的$attr属性,slot插槽,$emit发射事件三个成员。
- 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>
|
- 生命周期函数
下面是vue3.x的生命周期函数完整流程图:

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

具体有以下变动:
beforeCreate和created被setup代替,但依然可以使用
destroy改名为unmount
大部分钩子前面会加on
新增了两个调试用的钩子:onRenderTriggered和onRenderTracked。
- watch和watchEffect
watch用来监听数据的变化,当数据变化时它会立刻触发。
watch有三个参数:source,callback和options。
其中source是要侦听的数据,
callback是数据变化时会触发的回调
options则可以设置deep、immediate和flush选项。
- 侦听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之中
- 侦听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 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 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是不会被触发。
- stop停止侦听
watch运行之后,会返回一个停止函数,保存下来调用即可:
1 2 3 4 5 6 7
| const stopWatch = watch(...);
setTimeout(() => {
stopWatch();
}, 3000);
|
watchEffect跟watch功能上的是一样的,区别在于:
不需要传入以来
会执行一次来自动收集依赖
只获取变化后的值,无法得知变化前的值
因此相对少用一点。
响应式
vue2.x中响应式会莫名失效的根源就在于使用Object.defineProperty来实现功能。
使用Object.defineProperty来实现响应式有两个问题:
Object.defineProperty只能劫持对象的属性,并且需要遍历对象的每一个属性,如果属性也是个对象,那就需要进行深度遍历。
Object.defineProperty对新增属性需要手动进行Observe。
对象新增属性时,Object.defineProperty就需要对对象重新进行遍历,以劫持新增的属性。所以才有了$set
在vue3.x中,已经弃用了Object.defineProperty,使用Proxy代理对象,所以不存在这些烦恼。
Teleport
Teleport意即传送门,可以把组件挂在到指定的位置,并保持内部属性值不变。
案例如下:
- 首先在index.html中挂一个占位符,id定为dialog,等待挂载:
1 2 3 4 5 6 7
| <body>
<div class="app"></div>
<div class="dialog"></div>
</body>
|
- 接着实现一个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保持一致,并在前面加个#。
- 在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();
}
});
|
实际使用:
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修饰符
- 如何修改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" />
|
- 如何实现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 升级了