Composition API
- 组件逻辑复用
- 逐步应用
- 与
Options API
共存
为什么要使用Composition API?
在使用Options API
时,我们必须反复在不同的配置项中跳跃进行代码阅读。
而使用Composition API
后,我们可以将组件的所有代码写在一个setup()
函数中,在其中我们可以自由地调整代码位置,让同一功能的代码放在一起。同时我们也可以抽离其中的函数放置在单独的文件中,让组件直接使用抽离出的函数,让组件代码更加简洁。
Composition API的入口——setup()
函数:
注意:
setup()
函数只是提供了响应式组件的替代方式,例如data()
,computed()
,methods()
等。像静态的components,props,emits等组件配置项,还是使用原来的方式,不过之后还是会有替代的方式。
示例:
组件MessageList.vue文件:
<template>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
</ul>
</template>
<script>
export default {
setup() {
const messages = [
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
];
return { messages };
},
};
</script>
主文件App.vue文件:
<template>
<main>
<div class="container">
<MessageList />
</div>
</main>
</template>
<script>
import MessageList from "./components/MessageList.vue";
export default {
components: {
MessageList,
},
setup() {},
};
</script>
如何在setup()
函数中定义响应式数据?
Vue提供了一组用于在setup()
函数中定义响应式数据的函数,这些响应性数据也可称为状态。
也就是说,随着用户使用,应用的响应式数据会根据事件的发生而发生变化,从而引起组件的刷新,也就是状态的改变。
ref()
函数:
ref()
函数接收一个参数,可以是任意类型,它会把他们包装成响应式数据,作用就是替代了data()
配置项:
import {ref} from 'vue';
const num = ref(0); //数字
const str = ref("字符串"); //字符串
const arr = ref([0,1,2]); //数组
const obj = ref({a: 1, b: 2}); //对象
注意:如果传给
ref()
函数的是一个对象,那么对象的所有属性包括嵌套的属性都会转换为响应式的属性。
在setup()
函数中,必须通过其value属性来访问其包装的响应式数据,但和原始数值不是全相等的,原理以后再说。而在模板中不需要使用value属性来访问。
示例:
<template>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
<button @click="messages = []"></button>
</ul>
</template>
<script>
import {ref} from "vue"
export default {
setup() {
// 使用ref()让其他依赖响应式数据的代码进行更新
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
console.log(messages.value);
return { messages };
},
};
</script>
reactive()
函数:
reactive()
函数与ref()
函数类似,但它只接收一个对象类型的函数作为参数。这里的对象类型是广义的,包括数组。
reactive()
函数在setup()
函数中可以直接访问,不需要使用value属性。使用
ref()
函数时,其内部也会调用reactive()
函数,把对象的所有属性转换为响应式数据,之后将转换后的值赋值给value属性。
如何选择ref()
函数和reactive()
函数呢?
通常情况下直接使用ref()函数,因为它能直接定义基本数据,而且使用ref()函数定义的数据比较分散,所以更容易抽离成可复用的;
reactive()
函数则适用于一次性定义多个响应式数据的情况,将它们放置在一个对象中,之后通过该对象修改和访问其中的属性,适合存放组件的配置属性或表单数据。
示例:
<template>
<div>
<h2>{{ options.title }}</h2>
<p>
用户:{{ options.user.name }},活跃:{{
options.user.active ? "是" : "否"
}}
</p>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
</ul>
<button @click="messages = []">删除全部</button>
<button @click="options.title = '这是标题'">修改标题</button>
<button @click="options.user.name = '李四'">修改用户</button>
</div>
</template>
<script>
import { ref, reactive } from "vue";
export default {
setup() {
// const messages = reactive([
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
// const options = ref({
const options = reactive({
title: "消息列表",
user: {
name: "张三",
active: true,
},
});
console.log(options);
return { messages, options };
},
};
</script>
computed()
函数:
示例:
<template>
<div>
<!-- v-model 可以直接绑定 ref,与 data 配置项等效 -->
<input type="text" placeholder="搜索消息" v-model="searchTerm" />
<ul>
<li v-for="msg in searchedMessages" :key="msg.id">{{ msg.content }}</li>
</ul>
</div>
</template>
<script>
import { ref, computed } from "vue";
export default {
setup() {
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
const searchTerm = ref("");
const searchedMessages = computed(() => {
if (searchTerm.value === "") return messages.value;
return messages.value.filter((msg) => {
return msg.content.includes(searchTerm.value);
});
});
console.log(searchedMessages.value);
return { searchTerm, searchedMessages };
},
};
</script>
watch()
函数:
监听响应式数据的变化:
-
直接监听响应式数据:
watch(searchTerm, (newVal, oldVal) => { console.log(messages.value); console.log("搜索词:", newVal, oldVal); });
-
监听解剖后的响应式数据:
// 直接监听 searchTerm 不可以 watch(searchTerm.value, (newVal, oldVal) => { console.log("搜索词:", newVal, oldVal); }); // 需要使用一个函数 watch( () => searchTerm.value, (newVal, oldVal) => { console.log("搜索词:", newVal, oldVal); } );
监听对象中基本类型的响应式属性:
示例:
<template>
<div>
<h2>{{ options.title }}</h2>
<p>
用户:{{ options.user.name }},活跃:{{
options.user.active ? "是" : "否"
}}
</p>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
</ul>
<button @click="options.title = '这是标题'">修改标题</button>
<button @click="options.user.name = '李四'">修改用户</button>
</div>
</template>
<script>
import { ref, reactive, computed, watch } from "vue";
export default {
setup() {
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
const options = ref({
// const options = reactive({
title: "消息列表",
user: {
name: "张三",
active: true,
},
});
// 监听浅层 Object 属性
watch(
() => options.value.title,
// () => options.title, // reactive
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
// 监听深层 Object 属性
watch(
() => options.value.user.name,
// () => options.user.name, // reactive
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
return { messages, options };
},
};
</script>
监听对象类型的响应式属性:
注意:直接监听value属性,
watch()
函数不会监听到变化。因为在监听整个对象的时候,watch()
比较的是对象的引用,每次修改对象的属性都不会创建新的对象,而是在原对象中进行修改,所以watch()
函数监听不到。数组同理。
-
配置
deep: true
:缺点:不能访问修改前的值
示例:
// with deep true,可以比对对象的属性 watch( () => options.value, (newVal, oldVal) => { console.log(newVal, oldVal, newVal === oldVal); // 相同的引用 }, { deep: true } );
-
使用扩展运算符
...
:示例:
export default { setup() { const messages = ref([ { id: 1, content: "这是一条消息提醒1" }, { id: 2, content: "这是一条消息提醒2" }, { id: 3, content: "这是一条消息提醒3" }, { id: 4, content: "这是一条消息提醒4" }, ]); const options = ref({ // const options = reactive({ title: "消息列表", user: { name: "张三", active: true, }, }); watch( () => ({ ...options.value }), (newVal, oldVal) => { console.log(newVal, oldVal, newVal === oldVal); // 相同的引用 }, { deep: true } ); return { messages, options }; }, }; </script>
只使用扩展运算符,并不会监听到子对象的值,如
user.name
。即使设置了deep: true
,监听到的对象也是相同的引用。因为
...
语法创建的对象是浅拷贝,只会复制顶层的属性,子对象会原封不动的把引用传给新的对象。可使用JSON相关的方法进行深拷贝:
() => JSON.parse(JSON.stringify(options.value)); // 对象转为字符串再转为对象
同时监听多个响应性数据:
watch()
函数支持使用数组进行监听:
// 同时监听多个响应性数据
watch(
[() => options.value.title, () => options.value.user.name],
(newVals, oldVals) => {
console.log(newVals, oldVals); // 其中的数据也是数组形式
}
);
watchEffect()
函数:
watchEffect()
和 watch()
的作用基本一样,用于监听响应式数据的变化,并根据变化做一些业务逻辑,例如请求远程服务数据。
区别:
watchEffect()
函数不用明确指定标签中的响应式数据,而是会根据回调函数中的代码自动判断。如果代码中用到了响应式数据,无论多少个,只要其中的一个发生了变化,watchEffect()
函数就会重新执行一次。watchEffect()
函数无论数据是否发生了变化,都会先执行一次回调函数。watchEffect()
不能访问修改前的值,回调函数中的响应式的值都是修改后的。
示例:
<template>
<div>
<h2>{{ options.title }}</h2>
<p>
用户:{{ options.user.name }},活跃:{{
options.user.active ? "是" : "否"
}}
</p>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
</ul>
<button @click="options.title = '这是标题'">修改标题</button>
<button @click="options.user.name = '李四'">修改用户</button>
</div>
</template>
<script>
import { ref, watchEffect } from "vue";
export default {
setup() {
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
const options = ref({
// const options = reactive({
title: "消息列表",
user: {
name: "张三",
active: true,
},
});
watchEffect(() => {
console.log(options.value.title);
console.log(options.value.user.name);
});
return { messages, options };
},
};
</script>
watch()
&watchEffect()
清理收尾工作:
watchEffect()
函数:
watchEffect((onInvalidate) => {
console.log(options.value.title);
console.log(options.value.user.name);
onInvalidate(() => {
console.log("做一些清理操作...");
});
});
watch()
函数:
watch(
() => options.value.title,
(newVal, oldVal, onInvalidate) => {
console.log(newVal, oldVal);
onInvalidate(() => {
console.log("做一些清理操作...");
});
}
);
注意:
onInvalidate()
函数会在下次监听代码执行前执行
传递和访问Props属性:
如果需要在setup()
函数中访问Props属性,需要在setup()
函数中设置参数:
示例:
子组件MessageList.vue文件:
<template>
<li>{{ msg }}</li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";
export default {
props: ["msg"],
setup(props) {
console.log(props.msg);
return {};
},
};
</script>
父组件MessageListItem.vue文件:
<template>
<div>
<ul>
<MessageListItem
v-for="msg in messages"
:key="msg.id"
:msg="msg.content"
></MessageListItem>
</ul>
</div>
</template>
<script>
import { ref, watch, watchEffect } from "vue";
import MessageListItem from "./MessageListItem.vue";
export default {
components: { MessageListItem },
setup(props) {
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
return { messages };
},
};
</script>
转换非响应性props为响应性:
setup()
函数中的props属性整体是响应性的,相当于使用reactive()
或使用ref()
创建的对象的value属性,故可以使用watch()
或watchEffect()
监听props中的属性变化。
但如果父组件传递给子组件的数据是非响应性的,如果使用解构语法拆解出来,那么是不会被监听到变化的,同样地类似computed()响应式属性也不会生效。
须调用Vue中的
toRefs()
函数,之后再解构。
示例:
<template>
<li>{{ msg }}</li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";
export default {
props: ["msg"],
setup(props) {
const { msg } = toRefs(props);
// const msg = toRefs(props, 'msg');
watch(msg, (newMsg) => {
console.log(newMsg);
});
return {};
},
};
</script>
ref/reactive创建的数据在Props中的响应性:
注意:如果父组件传递的数据类型为JavaScript基本类型,例如字符串、数字、布尔类型等,他们在通过属性传递后就会失去响应性,在子组件中需要使用
toRefs()
函数进行转换。
换言之,只有对象和数组类型的数据在传递的时候会保留其响应性。
使用
ref()
函数创建的数据在传递时,只会传递其value属性,在子组件中即为Proxy类型。
在setup()
函数中定义methods:
直接定义function:
示例:
<template>
<li>{{ msg }} <button @click="removeMessage(id)">删除</button></li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";
export default {
props: ["msg", "id"],
setup(props) {
// 无参数
// function removeMessage() {
// console.log("删除消息");
// }
// // 有参数
function removeMessage(id) {
console.log("删除消息", id);
}
return { removeMessage };
},
};
</script>
Emit自定义事件:
因为无法从
setup()
函数中访问this
属性,setup()
接收第二个参数context,其中一个属性就是emit函数。
context参数本身不是响应性的,所以可以直接使用解构赋值。
示例:
子组件MessageListItem.vue文件:
<template>
<!-- <button @click="removeMessage(id)">删除</button> -->
<li>{{ msg }} <button @click="this.$emit('remove', id)">删除</button></li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";
export default {
props: ["msg", "id"],
emits: ["remove"],
setup(props) {
// setup(props, { emit }) {
// 有参数
// function removeMessage(id) {
// emit("remove", id);
// }
return {};
// return { emit };
},
};
</script>
父组件MessageList.vue文件:
<template>
<div>
<ul>
<MessageListItem
v-for="msg in messages"
:key="msg.id"
:id="msg.id"
:msg="msg.content"
@remove="removeMessage"
></MessageListItem>
</ul>
</div>
</template>
<script>
import { ref, watch, watchEffect } from "vue";
import MessageListItem from "./MessageListItem.vue";
export default {
components: { MessageListItem },
setup(props) {
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
function removeMessage(id) {
// console.log(id);
messages.value = messages.value.filter((msg) => msg.id !== id);
}
return { messages, removeMessage };
},
};
</script>
生命周期钩子:
与Options API
不同,Composition API
中的生命周期钩子需要加上on前缀,后面对应的生命周期首字母大写:
onMounted(()=>{
//业务代码
})
没有beforeCreate和created对应生命周期钩子的原因是
setup()
函数本身就是在这两个生命周期期间执行的,所以在这两个生命周期的自定义业务逻辑,直接在setup()
函数中编写即可。
示例:
<template>
<div>
<div v-if="loading">loading...</div>
<ul v-else>
<MessageListItem
v-for="msg in messages"
:key="msg.id"
:msg="msg.content"
></MessageListItem>
</ul>
</div>
</template>
<script>
import { ref, watch, watchEffect, onMounted } from "vue";
import MessageListItem from "./MessageListItem.vue";
export default {
components: { MessageListItem },
setup(props) {
const messages = ref([]);
const loading = ref(false);
onMounted(() => {
loading.value = true;
setTimeout(() => {
messages.value = [
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
];
loading.value = false;
}, 2000);
});
return { messages, loading };
},
};
</script>
Provide和Inject:
-
普通数据:
provide('propName', value); inject('propName');
-
响应式数据:
如果使用provide提供的数据本身是响应性的,那么inject接收的数据就是响应性的;
如果不是,则可以调用
ref()
,reactive()
,toRef()
,toRefs()
转换为响应性数据:<template> <div class="card"> <!-- <MovieItem :title="movie.title" :description="movie.description" /> --> <MovieItem :description="movie.description" /> </div> </template> <script> import MovieItem from "./MovieItem.vue"; import { ref, provide, toRef } from "vue"; export default { components: { MovieItem, }, setup() { const movie = ref({ title: "电影", description: "这是一段电影的描述", }); provide("movie", movie); // provide("title", toRef(movie.value, "title")); setTimeout(() => { movie.value.title = "电影-修改"; }, 1500); return { movie }; }, }; </script>
获取Template Ref:
即相当于在Options API
中获取DOM和Vue组件实例:
示例:
<template>
<input type="text" v-model="inputText" ref="inputControl" />
</template>
<script>
import { onMounted, ref } from "vue";
export default {
setup() {
const inputText = ref("");
const inputControl = ref(null);
onMounted(() => {
inputControl.value.focus();
});
return { inputText, inputControl };
},
};
</script>
获取非props属性:
使用attrs
可以获取在props中未明确定义的属性:
示例:
子组件MessageList.vue文件:
<template>
<div>
<ul>
<MessageListItem
v-for="msg in messages"
:key="msg.id"
:msg="msg.content"
></MessageListItem>
</ul>
</div>
</template>
<script>
import { ref, watch, watchEffect, isRef } from "vue";
import MessageListItem from "./MessageListItem.vue";
export default {
components: { MessageListItem },
setup(props, { attrs }) {
const messages = ref([
{ id: 1, content: "这是一条消息提醒1" },
{ id: 2, content: "这是一条消息提醒2" },
{ id: 3, content: "这是一条消息提醒3" },
{ id: 4, content: "这是一条消息提醒4" },
]);
console.log(attrs);
console.log(attrs.class);
console.log(attrs["data-title"]);
// 拆解出来,不再具有响应性
// const { test } = attrs;
// watchEffect(() => {
// console.log(test, " in MessageList.vue");
// });
watchEffect(() => {
console.log(attrs.test, " in MessageList.vue");
});
// console.log(attrs);
// console.log(attrs.class);
return { messages };
},
};
</script>
父组件App.vue文件:
<template>
<main>
<div class="container">
<MessageList class="messageList" :test="test" data-title="消息列表" />
</div>
</main>
</template>
<script>
import MessageList from "./components/MessageList.vue";
import { ref } from "vue";
export default {
components: {
MessageList,
},
setup() {
const test = ref("test");
setTimeout(() => {
test.value = "changed";
}, 2000);
return { test };
},
};
</script>
使用script setup进一步简化组件代码:
使用配置:
<script setup>
</script>
script setup中定义的函数及变量等,可以直接在模板中使用,无需手动return。
script setup可以使用import导入库或其他组件,并在模板中直接使用,无需配置。
script setup中如果需要使用props,则可以调用
defineProps( [ "propsName"] )
函数。script setup中如果需要使用emits,则可以调用
defineEmits( [ "emitsName"] )
函数。script setup中如果需要使用slots和attrs:
import { useSlots, useAttrs} from 'vue'; const slots = useSlots(); const attrs = useAttrs();