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按钮时也可以响应式刷新页面。
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。
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函数中的回调函数来清除副作用(即把上一次的定时器给清除了,所以只有最后一次的定时器没有被清除)。
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对象赋值新的值,副作用函数会再次执行,打印出对应的元素。
如果我们希望在第一次的时候就打印出来对应的元素呢? -
这个时候我们需要改变副作用函数的执行时机。
-
它的默认值是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次(打印出元素)。
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发生了改变,并打印出新旧值。
案例二: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都发生了改变,并打印出新旧值。
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发生了改变。
4.1 组件生命周期钩子
我们前面说过 setup
可以用来替代 data
、 methods
、 computed
、watch
等等这些选项,也可以替代 生命周期钩子
。
那么setup中如何使用生命周期函数呢?
- 可以使用直接导入的
onxxx
函数注册生命周期钩子。
我们在01_composition_api
项目的src
目录下新建09_生命周期钩子
文件夹,然后在该文件夹下新建:app.vue
组件。
app.vue根组件,代码如下所示:
<template>
<div><button @click="increment">点击+1:{{counter}}</button></div>
</template>
<script>
import { onmounted, onupdated, onunmounted, ref } from 'vue';
export default {
setup() {
const counter = ref(0);
const increment = () => counter.value++
// 生命周期钩子函数 (同一个生命周期函数可以存在多个)
onmounted(() => {
console.log("app mounted1");
})
onmounted(() => {
console.log("app mounted2");
})
onupdated(() => {
console.log("app onupdated");
})
onunmounted(() => {
console.log("app onunmounted");
})
return {counter,increment}
}
}
</script>
可以看到,在app组件中注册了onbeforemount
、onmounted
、onupdated
和onunmounted
生命周期函数,其中onmounted
生命周期函数我们注册了两次。
然后我们修改main.js程序入口文件,将导入的app组件改为09_生命周期钩子/app.vue
路径下的app组件。
保存代码,运行在浏览器的效果,如图10-22所示。刷新页面,控制台会打印app onbeforemount
、app mounted1
、app mounted2
,每当点击一次按钮会打印一次app onupdated。这里就不一一演示组件的销毁和其它的生命周期函数了。
那么compostion api提供了哪些生命周期函数呢?并且compostion api的生命周期函数和options api的生命周期函数有什么对应关系呢?请看下表:
我们会发现compostion api
没有提供 beforecreate
和created
生命周期函数,而是直接使用setup
函数来代替了(setup函数会在beforecreate之前调用),如图10-23所示。
5.1 provide/inject
事实上我们之前还学习过provide
和inject
,composition api也可以替代之前的 provide
和 inject
的选项。
5.1.1 provide函数
我们可以通过 provide
函数来给子组件或者子孙组件提供数据:
- 可以通过
provide
函数来定义每个 property。 provide
函数可以传入两个参数:- name:提供的属性名称。
- value:提供的属性值。
下面我们来通过一个案例来学习一下provide函数的使用。我们在01_composition_api
项目的src
目录下新建10_provide和inject
文件夹,然后在该文件夹下新建:app.vue
组件。
app.vue根组件,代码如下所示:
<template>
<div class="app" style="border:1px solid #ddd;margin:4px">
app组件
<div>{{name}} - {{age}}</div>
<div>{{counter}}</div>
<button @click="increment">app组件+1</button>
</div>
</template>
<script>
import { provide, ref } from 'vue';
export default {
setup() {
// 1.定义普通数据
const name = "coderwhy";
const age = 18;
// 2.定义响应式数据
let counter = ref(100);
// 3.给子组件或者子孙组件提供数据
provide("name", name);
provide("age", age); // 提供普通数据(只能读,不能修改)
provide("counter", counter); // 提供响应式数据
const increment = () => counter.value++;
return {name,age,increment,counter}
}
}
</script>
可以看到,在setup函数中调用了provide函数来给子组件或者子孙组件提供了name与age
普通数据和counter
响应式数据。其中提供的普通数据是只读不能修改,提供的响应式数据默认是可读可修改,并且是响应式的。
然后我们修改main.js程序入口文件,将导入的app组件改为10_provide和inject/app.vue
路径下的app组件。
保存代码,运行在浏览器的效果,如图10-24所示。可以看到在自己本组件中能正常显示,点击按钮也能实现响应式刷新页面。那有些同学会问provide不是给子组件或者子孙组件提供数据吗?那么子组件和子孙组件如何获取?那我们继续来学习下一小节的inject函数。
5.1.2 inject函数
在后代组件中可以通过inject
来注入需要的属性和对应的值:
- 可以通过
inject
函数来注入需要的内容。 inject
可以传入两个参数:- 要 inject 的 property 的 name。
- 默认值。
上面案例的app父组件已经完成数据的提供,那么它的子组件和孙子组件怎么获取提供的数据呢?要想获取父组件通过provide提供的数据,子组件或者孙子组件需要通过inject函数来获取。
接着我们在10_provide和inject
文件夹下新建:home.vue
组件。
home.vue子组件,代码如下所示:
<template>
<div style="border:1px solid #ddd;margin:8px">
home组件
<div>{{name}} - {{age}}</div>
<div>{{counter}}</div>
<button @click="homeincrement">home组件+1</button>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
// 1.获取父组件provide提供的数据( 子组件和孙子组件获取的代码是一模一样的)
const name = inject("name");
const age = inject("age");
const counter = inject("counter");
const homeincrement = () => counter.value++;
return {name,age, counter,homeincrement}
}
}
</script>
可以看到,该组件在setup函数中通过inject函数来注入父组件或者祖父组件使用provide函数提供的数据。其中name与age
是注入普通对象(只读不能修改),counter
则是响应式对象(可读可修改)。接着当点击button时,我们在子组件中修改了父组件提供的counter值。
修改app组件,代码如下所示:
<template>
<div class="app" style="border:1px solid #ddd;margin:4px">
app组件......
<home/>
</div>
</template>
<script>
import home from './home.vue';
export default {
components: { home },
......
}
</script>
保存代码,运行在浏览器的效果,如图10-25所示。当我们点击app组件的按钮来在父组件修改counter时,app组件和home组件的counter都同步变化,当我们点击home组件的按钮来在子组件修改counter时,app组件和home组件的counter也是同步变化。这就说明父组件提供的响应式数据,子组件不但能获取到,还保持了响应式。
5.1.3 共享响应式属性
1.共享响应式的数据
为了增加 provide 值和 inject 值之间的响应性,其实我们可以在 provide 值时使用 ref
和 reactive
对象。其中ref
对象上面已经演示了,这里再看一下如何提供reactive响应式数据,代码如下所示:
// app父组件
let counter = ref(100)
let info = reactive({
name: "why",
age: 18
})
// 1.提供响应式数据
provide("counter", counter)
provide("info", info)
// 2.修改响应式数据
const changeinfo = () => {
info.name = "coderwhy"
}
// 子组件(孙子组件)注入父组件(祖父组件)提供的响应式数据
const info = inject("counter");
const info = inject("info");
2.修改响应式property
因为父组件可以通过provide提供响应式数据给子组件,该响应式数据默认是可以在父组件被修改,也可以在子组件被修改。如果子组件也可以修改父组件提供的响应式数据,那么我们就很难追踪响应数据到底是在哪被修改的,为了保证单向数据流,我们一般建议:
- 如果我们需要修改响应的数据,那么最好是在数据提供的位置来修改(如上案例应在app中修改counter)
- 其实我们还可以将修改数据的方法进行共享,在后代组件中进行调用(如上案例不应在home中直接修改counter)。
- 有时候为了避免子组件修该父组件提供的数据,我们可以借助readonly函数,如下代码所示。
provide("info", readonly(info); // 子组件注入时只能读,不能修改
provide("counter", readonly(counter); // 子组件注入时只能读,不能修改
6.1 composition api综合练习
前面我们已经学习了setup
、reactive
、ref
、computed
、watcheffect
、watch
、provide
、inject
等等composition api,那下面将通过一个composition api的综合练习来巩固一下组合api的使用以及代码逻辑的封装(即hook函数的封装)。其中该综合练习包含以下功能:
- 计数器案例的实现。
- 修改网页的标题。
- 完成一个监听界面滚动位置。
在使用composition api之前,我们先看看用options api是如何实现该功能。
我们在01_composition_api
项目的src
目录下新建11_compositionapi综合练习
文件夹,然后在该文件夹下分别新建:app.vue
,optionsapiexample.vue
组件。
optionsapiexample.vue子组件,代码如下所示:
<template>
<div>
<!--1.计数器案例 -->
<div>当前计数: {{counter}}</div>
<div>当前计数*2: {{doublecounter}}</div>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
export default {
data() {
return{
// 1.2计数器案例的逻辑代码
counter:100
}
},
computed: {
// 1.3计数器案例的逻辑代码
doublecounter() {
return this.counter * 2
}
},
methods: {
// 1.4计数器案例的逻辑代码
increment() {
this.counter++;
},
decrement() {
this.counter--;
}
}
}
</script>
可以看到,该案例我们仅实现了计数器的案例。为了保证代码的简洁易懂,其它修改网页标题和监听页面滚动的代码逻辑这里暂时先不实现(后面直接用组合api来实现)。最后我们在app.vue根组件中导入和使用optionsapiexample组件(不再贴代码)。
然后我们修改main.js程序入口文件,将导入的app组件改为11_compositionapi综合练习/app.vue
路径下的app组件。
保存代码,运行在浏览器的效果,如图10-26所示。
下面我们再用composition api来实现该功能。我们在11_compositionapi综合练习
文件夹下新建:compositionapiexample.vue
组件。
compositionapiexample.vue子组件,代码如下所示:
<template>
<div>
<!-- 1.计数器案例 -->
<div>当前计数: {{counter}}</div>
<div>当前计数*2: {{doublecounter}}</div>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
// 1.1计数器案例的逻辑代码
const counter = ref(100);
const doublecounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doublecounter,
increment,
decrement
}
}
}
</script>
可以看到,该案例我们仅实现了计数器的案例。其它修改网页标题和监听页面滚动的代码逻辑这里也暂时先不实现。最后我们在app.vue根组件中导入和使用compositionapiexample组件(不再贴代码)。
保存代码,运行在浏览器的效果和options api实现的效果一模一样。通过这两个案例,我们可以发现:
- options api的特点就是在对应的属性中编写对应的功能模块
- 但options api有一个很大的弊端是对应的代码逻辑被拆分到各个属性中,当组件变得更大、复杂时,同一个功能的逻辑会被拆分的很分散(如上面的计数器功能逻辑被拆分到各个选项中),不利于代码的阅读和理解。
- composition api的特点是能将同一个逻辑关注点相关的代码收集在一起,方便代码的封装和复用,也更利于代码的阅读和理解。
- composition api用了比较多的函数,用起来稍微比options api复杂一点,但是函数式编程对ts支持更友好。
对比完options api和composition api编写计数器案例的优缺点之后,下面我们来看看如何对composition api编写的代码逻辑进行封装和复用。在options api编写方式中,我们已知道代码逻辑的封装和复用可以使用mixin混入,那在composition api中我们可以将关注点相关的代码逻辑封装到一个函数中,该函数我们一般会使用usexx
来命名(社区默认准寻的规范),并且以usexx
开头的函数我们称之为自定义hook函数。
6.1.1 usecounter
认识hook函数之后,下面我们来把上面计数器案例的代码逻辑封装到一个usecounter
的hook函数中。
我们在11_compositionapi综合练习
文件夹下新建:hooks/usecounter.js
文件。
usecounter.js文件封装usecounter hook函数,代码如下所示:
import { ref, computed } from 'vue';
export default function usecounter() {
// 1.1计数器案例的逻辑代码
const counter = ref(100);
const doublecounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doublecounter,
increment,
decrement
}
}
可以看到,我们在该文件中默认导出一个函数(也支持匿名函数),在该函数中我们把compositionapiexample组件实现计数器案例的代码逻辑全部抽取过来了。
接着修改compositionapiexample组件,代码如下所示:
<script>
import usecounter from './hooks/usecounter'
export default {
setup() {
// 1.计数器案例的代码逻辑抽取到usecounter hook 中了
const {counter, doublecounter, increment, decrement} = usecounter()
return {counter, doublecounter, increment, decrement}
}
}
</script>
可以看到,该组件之前实现计数器案例的逻辑代码已经抽取到了usecounter函数中,这时我们只要导入usecounter函数,并在setup中调用该函数便可以拿到返回的响应式数据和事件函数,然后直接返回给模板使用。保存代码,运行在浏览器的效果和没抽取前一模一样。
6.1.2 usetitle
实现完计数器案例之后,下面我们接着再compositionapiexample组件中来实现修改网页标题的功能。修改compositionapiexample组件,代码如下所示:
<script>
export default {
setup() {
.....
// 2.修改网页的标题案例
const titleref = ref("coder");
document.title = titleref.value// 更新网页标题
return {counter, doublecounter, increment, decrement}
}
}
</script>
可以看到,只在compositionapiexample中的setup函数中添加两行代码即可以。保存代码,运行在浏览器的效果,如图10-27所示。已经将网页的标题修改为coder。
像这种修改网页标题的代码逻辑可能在其它组件中还会再次使用到,那么我们就可以将该功能封装到一个hook函数中。我们在11_compositionapi综合练习
文件夹下新建:hooks/usetitle.js
文件。
usetitle.js文件封装usetitle hook函数,代码如下所示:
import { ref, watch } from 'vue';
// 使用匿名函数,并该函数需接收一个参数
export default function(title = "默认的title") {
const titleref = ref(title);
// 侦听titleref变化,一旦被修改就更新
watch(titleref, (newvalue) => {
document.title = newvalue
}, {
immediate: true // 侦听的回调函数先执行一次
})
return titleref
}
修改compositionapiexample组件,代码如下所示:
<script>
.....
import usetitle from './hooks/usetitle'
export default {
setup() {
.....
// 2.修改网页的标题案例
const titleref = usetitle("coder");
settimeout(() => {
// 3秒后修改titleref的值,usetitle函数的watch侦听到会修改标题
titleref.value = "why
}, 3000);
return {counter, doublecounter, increment, decrement}
}
}
</script>
可以看到,我们先导入usetitle函数,接着在setup中调用usetitle函数初始化标题为coder,然后过了2秒之后将标题修改为why。保存代码,运行在浏览器后。网页的标题在3秒后有coder修改为why。
6.1.3 usescrollposition
实现完修改网页的标题之后,我们接着继续再compositionapiexample组件中来实现监听页面滚动位置的功能。修改compositionapiexample组件,代码如下所示:
<template>
<div>
.....
<!-- 3.显示页面滚动位置 -->
<p style="width: 3000px;height: 5000px;">
width:3000px height:5000px的,模拟页面滚动
</p>
<div style="position: fixed;top:20px;right:20px">
<div >scrollx: {{scrollx}}</div>
<div >scrolly: {{scrolly}}</div>
</div>
</div>
</template>
<script>
.....
export default {
setup() {
......
// 3.监听页面滚动
const scrollx = ref(0);
const scrolly = ref(0);
document.addeventlistener("scroll", () => {
scrollx.value = window.scrollx;
scrolly.value = window.scrolly;
});
return {counter, doublecounter, increment, decrement, scrollx, scrolly}
}
}
</script>
可以看到,我们先在template中编写宽和高超出屏幕大小的p元素(模拟页面可滚动),接着在setup函数监听了页面的滚动,并在该回调函数中给scrollx和scrolly变量赋当前滚动的值。最后在return函数中返回scrollx和scrolly变量给temlpate来显示当前滚动的位置。保存代码,运行在浏览器的效果,如图10-28所示。上下滚动页面的时候,页面的右上角上能显示当前滚动位置值
那如果该功能也会被再次使用到,我们依然可以将该功能封装到一个hook函数中。我们在11_compositionapi综合练习
文件夹下新建:hooks/usescrollposition.js
文件。
usescrollposition.js文件封装usescrollposition hook函数,代码如下所示:
import { ref } from 'vue';
// 自定义 usescrollposition hook函数
export default function usescrollposition() {
const scrollx = ref(0);
const scrolly = ref(0);
document.addeventlistener("scroll", () => {
scrollx.value = window.scrollx;
scrolly.value = window.scrolly;
});
return {scrollx, scrolly} // 返回ref响应式数据
}
修改compositionapiexample组件,代码如下所示:
.......
<script>
import usecounter from './hooks/usecounter'
import usetitle from './hooks/usetitle'
import usescrollposition from './hooks/usescrollposition'
export default {
setup() {
// 1.计数器案例(可直接解构,如果返回的是reactive对象则不能直接解构使用)
const {counter, doublecounter, increment, decrement} = usecounter()
// 2.修改网页标题案例
const titleref = usetitle("coder");
settimeout(() => {
titleref.value = "why"
}, 3000);
// 3.监听页面滚动位置案例 (可直接解构,因为hook函数返回对象属性是ref对象)
const { scrollx, scrolly } = usescrollposition();
return {counter, doublecounter, increment, decrement, scrollx, scrolly}
}
}
</script>
可以看到,我们先导入usescrollposition函数,接着在setup中调用usescrollposition函数来获取到当前滚动的值。如果滚动页面了,usescrollposition函数里会监听到并修改scrollx和scrolly响应式变量的值,同时更新页面。保存代码,运行在浏览器后。滚动网页时可以发现页面上右上角的scrollx和scrolly能显示当前滚动的位置。
7.1 script setup语法
当我们在编写单文件组件(即.vue文件)的时候,除了 <script>
语法,其实vue3还支持<script setup>
语法,它方便我们在script顶层来编写setup相关的代码。setup script语法的代码看起来简单了很多,开发效率大大的提高。该语法是在2020-10-28号提出,在vue3.2版本之前它还只是一个实验性功能,但是到了vue3.2版本<script setup>
语法已从实验状态毕业,现在被认为是稳定的了。
<script setup>
是在单文件组件 (sfc) 中使用组合式 api 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 typescript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 ide 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
7.1.1 基本使用
我们来看一下用<script setup>
语法是如何使用的:
- 要使用这个语法,需要将
setup
attribute 添加到<script>
代码块上。 - 里面的代码会被编译成组件
setup()
函数的内容。 - 这意味着与普通的
<script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。 - 当使用
<script setup>
的时候,任何在<script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用。
下面我们使用<script setup>
语法来编写计数器案例,我们在01_composition_api
项目的src
目录下新建12_script_setup顶层编写方式
文件夹,然后在该文件夹下分别新建:app.vue
,scriptsetupexample.vue
组件。
scriptsetupexample.vue子组件,代码如下所示:
<template>
<div>
<h4>当前计数: {{counter}}</h4>
<button @click="increment">+1</button>
</div>
</template>
// 1.script setup语法的顶层编写方式
<script setup>
// 2.ref、counter、increment是在顶层绑定,所以都能在模板中直接使用
import { ref } from 'vue';
const counter = ref(0);
const increment = () => counter.value++;
</script>
可以看到,该组件使用了<script setup>
语法的顶层编写方式,在顶层绑定了ref
、counter
、increment
,所以都能在模板中直接使用他们。最后我们在app.vue根组件中导入和使用scriptsetupexample组件(不再贴代码)。
然后我们修改main.js程序入口文件,将导入的app组件改为12_script_setup顶层编写方式/app.vue
路径下的app组件。
保存代码,运行在浏览器的效果,如图10-29所示。已实现计数器案例。
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定都能在模板中直接使用。例如:声明的普通变量,响应式变量,函数,import 引入的内容(包含函数,对象,组件,动态组件,指令等等)。当是响应式状态时需要明确使用响应式 apis 来创建。和从 setup()
函数中返回值一样,ref 值在模板中使用的时候会自动解包,如下代码所示:
<template>
<mycomponent />
<component :is="foo" />
<h4 v-my-directive>this is a heading</h1>
<div>{{ capitalize('hello') }}</div>
<button @click="count++">{{ count }}</button>
<div @click="log">{{ msg }}</div>
</template>
// script setup语法的顶层的绑定( 下面声明的绑定都可以直接在模板中使用 )
<script setup>
import mycomponent from './mycomponent.vue' // 声明绑定组件
import foo from './foo.vue' // 声明绑定动态组件
import { mydirective as vmydirective } from './mydirective.js' // 声明绑定指令
import { capitalize } from './helpers' // 声明绑定工具函数
import { ref } from 'vue' // 声明绑定ref函数
const count = ref(0) // 声明绑定响应式变量
const msg = 'hello!' // 声明绑定普通变量
function log() { // 声明绑定函数
console.log(msg)
}
</script>
上面代码列举了在 <script setup>
中常用的顶层的绑定。上面代码所声明的组件,函数,指令等这里就不一一实现了。大家只要知道在<script setup>
中顶层的绑定会被暴露给模板使用就可以了。
7.1.2 defineprops和defineemits
上面我们已经学会了<script setup>
语法的基本使用,那么在这种语法下,我们应该如何定义props和如何发出事件呢?在 <script setup>
中必须使用 defineprops
和 defineemits apis
来声明 props
和 emits
,它们具备完整的类型推断并且在 <script setup>
中是直接可用的(vue3.2版本以后不需要导入)。
我们在12_script_setup顶层编写方式
文件夹下新建:definepropsemitapi.vue
组件。
definepropsemitapi.vue子组件,代码如下所示:
<template>
<div style="border:1px solid #ddd;margin:8px">
<div>definepropsemitapi组件</div>
<p>{{message}}</p>
<button @click="emitevent">发射emit事件</button>
</div>
</template>
// vue3.2以后defineprops和defineemits不需要导入(当前项目vue安装的版本是:3.2.29)
<script setup>
// 1.定义props属性(等同于options api的props选项)
const props = defineprops({
// message: string,
message: {
type: string,
default: "默认的message"
}
})
// 2.注册需要触发的emit事件
const emit = defineemits(["increment"]);
// 3.点击 发射emit事件 按钮的回调
const emitevent = () => {
console.log('子组件拿到父组件传递进来的message:' + props.message)
emit('increment', 1) // 触发 increment 事件,传递参数:1
}
</script>
可以看到,我们使用defineprops
函数来给组件定义了message属性,使用defineemits
函数来给组件注册了increment事件,并返回emit函数。当点击button时,先打印父组件传递进来的message,然后使用emit函数来触发事件。
接着修改app组件,代码如下所示:
<template>
<div class="app" style="border:1px solid #ddd;margin:4px">
app组件
<!-- <scriptsetupexample></scriptsetupexample> -->
<definepropsemitapi message="app传递过来的message" @increment="getcounter"/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import scriptsetupexample from './scriptsetupexample.vue';
import definepropsemitapi from './definepropsemitapi.vue';
const getcounter = (number)=> console.log('app 组件拿到子组件传递过来的number:' + number)
</script>
可以看到,我们先导入definepropsemitapi
组件,接着在template中使用该组件时,给它传递了message属性和监听了increment事件 。
保存代码,运行在浏览器后点击发射emit事件
按钮,便会调用emitevent函数,控制台输出如图10-30所示。
有关于defineprops
和 defineemits
函数,我们还需要注意的是:
defineprops
和defineemits apis
都是只在<script setup>
中才能使用的编译器宏。他们不需要导入且会随着<script setup>
处理过程一同被编译掉。defineprops
接收与props
选项相同的值,defineemits
也接收emits
选项相同的值。defineprops
和defineemits
在选项传入后,会提供恰当的类型推断。- 传入到
defineprops
和defineemits
的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内。
7.1.3 defineexpose
使用 <script setup>
语法的组件是默认关闭的,即通过模板 ref
或者 $parent
链获取到的组件的公开实例,该实例是不会暴露任何在 <script setup>
中声明的绑定。所以为了在 <script setup>
语法组件中明确要暴露出去的属性,我们需要使用 defineexpose
编译器宏。
我们在12_script_setup顶层编写方式
文件夹下新建:defineexposeapi.vue
组件。
defineexposeapi.vue子组件,代码如下所示:
<template>
<div style="border:1px solid #ddd;margin:8px">
defineexposeapi 组件
</div>
</template>
<script setup>
import { ref } from 'vue'
const age = 18 // 普通数据
const name = ref('coderwhy') // 响应式数据
const showmessage = ()=>{console.log('showmessage方法')} // 方法
// 该组件暴露出去的属性( age,name,showmessage )
defineexpose({age,name,showmessage})
</script>
可以看到,我们在该组件中定义了age,name和showmessage方法,然后通过defineexpose api将这3个属性暴露出去。
接着修改app组件,代码如下所示(省略的代码已注释):
<template>
<div class="app" style="border:1px solid #ddd;margin:4px">app组件
.....
<defineexposeapi ref="defineexposeapi"></defineexposeapi>
</div>
</template>
<script setup>
import { ref, watcheffect } from 'vue'
....
import defineexposeapi from './defineexposeapi.vue';
// 获取defineexposeapi组件的实例和该组件暴露的属性
const defineexposeapi = ref(null)
watcheffect(()=>{
console.log(defineexposeapi.value) // 组件的实例
console.log(defineexposeapi.value.name) // 响应式数据
console.log(defineexposeapi.value.age)
defineexposeapi.value.showmessage()
}, {flush:"post"})
....
</script>
可以看到,我们用ref定义了defineexposeapi
变量,并绑定到defineexposeapi
组件的ref属性上来获取该组件的实例。然后在watcheffect函数中获取该组件实例和该组件暴露出来的:name,age和showmessage
属性。
保存代码,运行在浏览器后,控制台输出如图10-31所示。即父组件app可以访问到子组件暴露出来的name,age和showmessage
属性。
7.1.4 useslots和useattrs
在学习setup函数时,该函数主要有两个参数:props和context,其中context里面包含slots,attrs,emit
三个属性。那在 <script setup>
中应该如何拿到slots,attrs
属性?虽然在 <script setup>
使用 slots 和 attrs 的情况应该是很罕见的(因为可以在模板中通过$slots
和$attrs
来访问它们)。在你的确需要使用它们的罕见场景中,可以分别用 useslots
和useattrs
两个辅助函数。代码如下所示:
<script setup>
import { useslots, useattrs } from 'vue'
const slots = useslots() // 拿到该组件的插槽,等于setup函数中的context.slots
const attrs = useattrs() // 拿到该组件所有的属性,等于setup函数中的context.attrs
</script>
useslots
和 useattrs
是真实的运行时函数(需要导入后使用),它会返回与 setupcontext.slots
和 setupcontext.attrs
等价的值,同样也能在普通的组合式 api 中使用。
发表评论