1.1 computed
在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
- 在前面的Options API中,我们是使用
computed选项
来完成。 - 在Composition API中,我们可以在 setup 函数中使用
computed函数
来编写一个计算属性。
如何使用computed函数呢?
- 方式一:接收一个getter函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。
- 方式二:接收一个具有 get 和 set 方法的对象,返回一个可变的(可读写)ref 对象。
1.1.1 computed基本使用
下面我们来看看computed函数的基本使用:接收一个getter函数。
首先使用Vue CLI新建一个01_composition_api
的Vue3项目,然后在01_composition_api
项目的src
目录下新建07_computed使用
文件夹,然后在该文件夹下分别新建:App.vue,ComputedAPI.vue
组件。
ComputedAPI.vue子组件,代码如下所示:
<template> <div> <!-- 2.使用fullName计算属性 --> <h4>{{fullName}}</h4> <button @click="changeName">修改firstName</button> </div> </template> <script> import { ref, computed } from 'vue'; export default { setup() { const firstName = ref("Kobe"); const lastName = ref("Bryant"); // 1.用法一: 传入一个getter函数。computed的返回值是一个ref对象 const fullName = computed(() => firstName.value + " " + lastName.value); const changeName = () => { // 3.修改firstName firstName.value = "James" } return { fullName, changeName } } } </script>
可以看到,我们使用了computed函数来定义了一个fullName计算属性,其中computed函数需要接收一个getter函数,我们在getter函数中对响应式的数据进行计算和返回。
App.vue根组件,代码如下所示(省略了组件注册的代码):
<template> <div class="app" style="border:1px solid #ddd;margin:4px"> App组件 <ComputedAPI></ComputedAPI> </div> </template> .....
然后我们修改main.js程序入口文件,将导入的App组件改为07_computed使用/App.vue
路径下的App组件。
保存代码,运行在浏览器的效果,如图10-16所示。计算属性可以正常显示,当点击修改firstName按钮时也可以响应式刷新页面。
图10-16 computed函数的基本使用
1.1.2 计算属性get和set方法
接着我们再来看看computed函数的get和set方法的使用:接收一个对象,里面包含 set
和 get
方法。
修改ComputedAPI.vue子组件,代码如下所示:
...... <script> import { ref, computed } from 'vue'; export default { setup() { const firstName = ref("Kobe"); const lastName = ref("Bryant"); // const fullName = computed(() => firstName.value + " " + lastName.value); // 1.用法二: 传入一个对象, 对象包含getter/setter const fullName = computed({ get: () => firstName.value + " " + lastName.value, // getter 方法 set(newValue) { // setter 方法 const names = newValue.split(" "); firstName.value = names[0]; lastName.value = names[1]; } }); const changeName = () => { // firstName.value = "James" // 3.修改fullName计算属性 fullName.value = "James Bryant"; } return { fullName, changeName } } } </script>
可以看到,我们使用了computed函数来定义了一个fullName计算属性,其中computed函数接收一个具有 get 和 set 方法的对象,我们在get方法中对响应式的数据进行计算和返回,在set方法中对传入的新值重新赋值给firstName和lastName响应式对象的值。
保存代码,运行在浏览器后。fullName计算属性可以正常显示,当点击修改firstName按钮时也可以响应式刷新页面。
2.1 watchEffect侦听
在前面的Options API中,我们可以通过watch选项
来侦听data,props或者computed的数据变化,当数据变化时执行某一些操作。
在Composition API中,我们可以使用watchEffect
和watch
来完成响应式数据的侦听。
- watchEffect用于自动收集响应式数据的依赖。
- watch需要手动指定侦听的数据源。
下面我们先来看看watchEffect函数的基本使用。
2.1.1 watchEffect基本使用
当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect
:
- 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖。
- 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行。
下面通过一个案例来学习watchEffect基本使用。我们在01_composition_api
项目的src
目录下新建08_watch使用
文件夹,然后在该文件夹下分别新建:App.vue,WatchEffectAPI.vue
组件。
WatchEffectAPI.vue子组件,代码如下所示:
<template> <div> <h4>{{age}}</h4> <button @click="changeAge">修改age</button> </div> </template> <script> import { ref, watchEffect } from 'vue'; export default { setup() { const age = ref(18); // watchEffect: 1.自动收集响应式的依赖 2.默认会先执行一次 3.获取不到新值和旧值 watchEffect(() => { console.log("age:", age.value); // 侦听age的改变, age发生变化后会再次执行 }); const changeAge = () => age.value++ return { age, changeAge } } } </script>
可以看到,我们在setup函数中调用了watchEffect函数,并给该函数传递了一个回调函数,传入的回调函数会被立即执行一次,并且在执行的过程中会收集依赖(收集age的依赖)。当收集的依赖发生变化时,watchEffect传入的回调函数又会再次执行。
App.vue根组件,代码如下所示:
<template> <div class="app" style="border:1px solid #ddd;margin:4px"> App组件 <WatchEffectAPI></WatchEffectAPI> </div> </template> .....
然后我们修改main.js程序入口文件,将导入的App组件改为08_watch使用/App.vue
路径下的App组件。
保存代码,运行在浏览器的效果,如图10-17所示。可以看到,默认会先执行一次打印age:18,当点击修改age按钮来改变age时,watchEffect侦听到age发生改变后,回调函数又会再次执行,并打印age:19。
图10-17 watchEffect基本使用
2.1.2 watchEffect停止侦听
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
比如在上面的案例中,我们age达到20的时候就停止侦听,WatchEffectAPI.vue子组件,代码如下所示:
.... <script> import { ref, watchEffect } from 'vue'; export default { setup() { const age = ref(18); // 1.stop是watchEffect返回值的函数,用来停止侦听 const stop = watchEffect(() => { console.log("age:", age.value); // 侦听age的改变 }); const changeAge = () => { age.value++ if (age.value > 20) { stop(); // 2.停止侦听age的变化 } } return {age, changeAge} } } </script>
保存代码,运行在浏览器后,可以看到默认会先执行一次打印age:18,当点击修改age按钮来改变age时,当age大于20的时候,由于调用了watchEffect返回的stop函数,watchEffect将会取消对age变量的侦听。
2.1.3 watchEffect清除副作用
什么是清除副作用呢?
- 比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。
- 那么上一次的网络请求应该被取消掉(类似前面讲的防抖),这个时候我们就可以清除上一次的副作用。
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
- 当副作用即将再次重新执行 或者 侦听器被停止 时会执行onInvalidate函数传入的回调函数。
- 我们可以在传入的回调函数中,执行一些清除的工作。
我们在08_watch使用
文件夹下新建:WatchEffectAPIClear.vue
组件。
WatchEffectAPIClear.vue子组件,代码如下所示(省略的template和上面案例一样):
...... <script> import { ref, watchEffect } from 'vue'; export default { setup() { const age = ref(18); watchEffect((onInvalidate) => { const timer = setTimeout(() => { console.log("模拟网络请求,网络请求成功~"); }, 2000) onInvalidate(() => { // 当侦听到age发生变化和侦听停止时会执行该这里代码,并在该函数中清除额外的副作用 clearTimeout(timer); // age发生改变时,优先清除上一次定时器的副作用 console.log("onInvalidate"); }) console.log("age:", age.value); // 侦听age的改变 }); const changeAge = () => age.value++ return {age,changeAge} } } </script>
可以看到,watchEffect函数传入的回调函数接收一个onInvalidate参数,onInvalidate也是一个函数,并且该函数也需要接收一个回调函数作为参数。
App.vue根组件,代码如下所示:
<template> <div class="app" style="border:1px solid #ddd;margin:4px"> App组件 <!-- <WatchEffectAPI></WatchEffectAPI> --> <WatchEffectAPIClear></WatchEffectAPIClear> </div> </template>
保存代码,运行在浏览器的效果,如图10-18所示。刷新页面,立马连续点击3次修改age,我们可以看到watchEffect函数侦听到age改变了3次,并在每次将重新执行watchEffect函数的回调函数时先执行了onInvalidate函数中的回调函数来清除副作用(即把上一次的定时器给清除了,所以只有最后一次的定时器没有被清除)。
图10-18 watchEffect清除副作用
2.1.4 watchEffect执行时机
在讲解 watchEffect执行时机之前,我们先补充一个知识:在setup中如何使用ref或者元素或者组件?
- 其实非常简单,我们只需要定义一个前面讲的ref对象,绑定到元素或者组件的ref属性上即可。
我们在08_watch使用
文件夹下新建:WatchEffectAPIFlush.vue
组件。
WatchEffectAPIFlush.vue子组件,代码如下所示(省略的template和上面案例一样):
<template> <div> <h4 ref="titleRef">哈哈哈</h4> </div> </template> <script> import { ref, watchEffect } from 'vue'; export default { setup() { // 1.定义一个titleRef来拿到h4元素的DOM对象(组件对象也是一样) const titleRef = ref(null); // 2.h4元素挂载完成之后会自动赋值到titleRef变量上,这里监听titleRef变量被赋值,并打印出来看 watchEffect(() => { console.log(titleRef.value); // 3.打印h4元素的DOM对象 }) return { titleRef } } } </script>
可以看到,我们先用ref函数定义了一个titleRef响应式变量,接着该变量在setup函数中返回,并绑定到h4元素的ref属性上(注意:不需要用v-bind指令来绑定)。当h4元素挂载完成之后会自动赋值到titleRef变量上。为了观察titleRef变量被赋值,这里我们使用watchEffect函数来侦听titleRef变量的改变,并打印出来。最后我们在App.vue根组件中导入和使用WatchEffectAPIFlush组件(和前面的操作基本一样,这里不再贴代码)。
保存代码,运行在浏览器的效果,如图10-19所示。刷新页面,我们会发现打印结果打印了两次。
- 这是因为setup函数在执行时就会立即执行传入的副作用函数(watchEffect的回调函数),这个时候DOM并没有挂载,所以打印为null。
- 而当DOM挂载时,会给titleRef变量的ref对象赋值新的值,副作用函数会再次执行,打印出对应的元素。
图10-19 ref获取元素对象
如果我们希望在第一次的时候就打印出来对应的元素呢?
- 这个时候我们需要改变副作用函数的执行时机。
- 它的默认值是pre,它会在元素
挂载
或者更新
之前执行。 - 所以我们会先打印出来一个空的,当依赖的titleRef发生改变时,就会再次执行一次,打印出元素。
我们可以设置副作用函数的执行时机,修改WatchEffectAPIFlush.vue子组件,代码如下所示:
...... <script> export default { setup() { ...... watchEffect(() => { console.log(titleRef.value); },{ flush: "post" // 修改执行时机,支持 pre, post, sync }) return { titleRef } } } </script>
这里的flush:"post"
是将推迟副作用的初始运行,直到组件的首次渲染完成才执行。当然flush
选项还接受 sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
保存代码,运行在浏览器后。刷新页面,我们会发现结果打印了1次(打印出元素)。
注意:Vue3.2+ 以后
watchPostEffect
是watchEffect
带有flush: 'post'
选项的别名。watchSyncEffect
是watchEffect
带有flush: 'sync'
选项的别名。
3.1 watch侦听
watch的API完全等同于组件watch选项
的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用。
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调。
与watchEffect的比较,watch允许我们:
- 懒执行副作用(第一次不会直接执行)。
- 更具体的说明当哪些状态发生变化时,触发侦听器的执行。
- 访问侦听状态变化前后的值。
3.1.1 侦听单个数据源
watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref)。
- 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref)。
下面通过几个案例来学习watch函数的使用。
案例一:watch侦听的数据源为一个getter函数。
我们在08_watch使用
文件夹下新建:WatchAPI.vue
组件。WatchAPI.vue子组件,代码如下所示:
<template> <div> <h4 >{{info.name}}</h4> <button @click="changeData">修改数据</button> </div> </template> <script> import { reactive, watch } from 'vue'; export default { setup() { const info = reactive({name: "coderwhy", age: 18}); // 1.侦听watch时,传入一个getter函数(该函数引用可响应式的对象) watch(() => info.name, (newValue, oldValue) => { // 侦听info对象中name的改变 console.log("newValue:", newValue, "oldValue:", oldValue); }) const changeData = () => { info.name = "kobe"; // 改变info对象中的name } return {changeData,info} } } </script>
可以看到,我们调用了watch函数来侦听info对象name属性的变化。其中watch函数需要接收两个参数,第一次参数是一个getter函数,该函数必须引用可响应式的对象。第二参数是侦听的回调函数,该函数会接收到一个新的值和一个旧的值,并在该函数中打印出新旧值。最后我们在App.vue根组件中导入和使用WatchAPI组件(不再贴代码)。
保存代码,运行在浏览器的效果,如图10-20所示。刷新页面,点击修改数据按钮来修改info中的name后,我们可以看到watch已经侦听到info中name发生了改变,并打印出新旧值。
图10-20 watch侦听的数据源为getter函数
案例二:watch侦听的数据源为reactive对象。
修改WatchAPI.vue子组件,代码如下所示:
...... <script> export default { setup() { const info = reactive({name: "coderwhy", age: 18}); // 1.侦听watch时,传入一个getter函数 // watch(() => info.name, (newValue, oldValue) => { // console.log("newValue:", newValue, "oldValue:", oldValue); // }) // 2.传入一个可响应式对象: reactive对象 watch(info, (newValue, oldValue) => { // reactive对象获取到的newValue和oldValue本身都是reactive对象 console.log("newValue:", newValue, "oldValue:", oldValue); }) const changeData = () => info.name = "kobe"; return {changeData,info} } } </script>
保存代码,运行在浏览器后刷新页面,点击修改数据按钮后,我们可以看到watch已经侦听到info中name发生了改变,并打印出新旧值(都为reactive对象)。
如果希望newValue和oldValue是一个普通的对象的话,我们可以这样侦听,代码如下所示:
<script> export default { setup() { ....... // 2.传入一个可响应式对象: reactive对象 // 如果希望newValue和oldValue是一个普通的对象,watch第一参数改成getter函数 watch(() => { return {...info} }, (newValue, oldValue) => { console.log("newValue:", newValue, "oldValue:", oldValue); }) ...... } } </script>
保存代码,运行在浏览器后刷新页面,点击修改数据按钮后,我们可以看到watch已经侦听到info中name发生了改变,并打印出新旧值(都为普通对象)。
案例三:watch侦听的数据源为ref对象。
修改WatchAPI.vue子组件,代码如下所示:
...... <script> export default { setup() { ..... const name = ref("codeywhy"); // watch侦听ref对象,ref对象获取newValue和oldValue是value值的本身 watch(name, (newValue, oldValue) => { console.log("newValue:", newValue, "oldValue:", oldValue); }) const changeData = () => name.value = "kobe"; return {changeData,info,name} } } </script>
保存代码,运行在浏览器后刷新页面,点击修改数据按钮后,我们可以看到watch已经侦听到name发生了改变,并打印出新旧值(都是name的value)。
3.1.2 侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
我们在08_watch使用
文件夹下新建:WatchAPIMult.vue
组件。WatchAPIMult.vue子组件,代码如下所示:
<template> <div> <h4 >{{info.name}} - {{name}}</h4> <button @click="changeData">修改数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue'; export default { setup() { // 1.定义可响应式的对象 const info = reactive({name: "coder", age: 18}); const name = ref("why"); const age = ref(20); // 2.侦听多数据源,参数一是一个数组:数组中可以有getter函数,ref对象,reactive对象 watch([() => ({...info}), name, age], ([newInfo, newName, newAge], [oldInfo, oldName, oldAge]) => { console.log(newInfo, newName, newAge); console.log(oldInfo, oldName, oldAge); }) const changeData = () => { info.name = "kobe"; name.value = "jack" } return {changeData,info,name} } } </script>
可以看到,我们调用了watch函数来侦听多个数据源。watch函数的第一个参数接收的是一个数组,该数组中是支持侦听getter函数,ref对象和reactive对象的数据源。接着我们给watch的第二个参数传入回调函数,该回调函数接收的新值和旧值都是数组类型,然后我们在该函数中分别打印了新值和旧值。最后我们在App.vue根组件中导入和使用WatchAPIMult组件(不再贴代码)。
保存代码,运行在浏览器的效果,如图10-21所示。刷新页面,点击修改数据按钮后,我们可以看到watch已经侦听到info中name和name都发生了改变,并打印出新旧值。
图10-21 watch侦听多数据源
3.1.3 侦听响应式对象
如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构。
侦听响应式对象在上面的案例二中已经介绍过,下面看看侦听响应式数组,代码如下所示:
const names = reactive(["abc", "cba", "nba"]); // 侦听响应式数组( 和对象的使用一样 ) watch(() => [...names], (newValue, oldValue) => { console.log(newValue, oldValue); }) const changeName = () => { names.push("why"); }
如果是侦听对象时,我们希望侦听是一个深层的侦听,那么依然需要设置 deep
为true:
- 也可以传入
immediate
立即执行。
我们在08_watch使用
文件夹下新建:WatchAPIDeep.vue
组件。WatchAPIDeep.vue子组件,代码如下所示:
<template> <div> <h4 >{{info.name}}</h4> <button @click="changeData">修改数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue'; export default { setup() { // 1.定义可响应式的对象 const info = reactive({ name: "coderwhy", age: 18, friend: { name: "kobe" } }); // 2.侦听响应式对象 watch(() => ({...info}), (newInfo, oldInfo) => { console.log(newInfo, oldInfo); }, { deep: true, immediate: true }) const changeData = () => info.friend.name = "james" return {changeData,info} } } </script>
可以看到,我们调用了watch函数来侦听一个对象。watch函数的第一个参数是一个getter函数,第二个参数传入回调函数,在该回调函数打印接收的新值和旧值,第三个参数一个watch的配置项。其中deep为true代表是一个深层的侦听,即当用户修改了info中friend对象的name也会被watch侦听到,如果为false则侦听不到。还有immediate为true代表watch的回调函数会先立即执行一次,当侦听到有数据变化时才再次执行该回调函数。最后我们在App.vue根组件中导入和使用WatchAPIDeep组件(不再贴代码)。
保存代码,运行在浏览器后。刷新页面,默认会先立即执行一次watch的回调函数,当点击修改数据按钮后,我们可以看到watch可以深层侦听info中firend对象的name发生了改变。