setup函数
现在我们进入到Vue3项目里,学习使用setup函数,它就是使用Composition API的入口。
注意点:
setup函数是Vue3中新增的函数,它是我们在编写组件时,使用Composition API的入口。同时它也是Vue3中新增的一个生命周期函数,会在beforeCreate之前调用。因为此时组件的data和methods还没有初始化,因此在setup中是不能使用this的。所以Vue为了避免我们错误的使用,它直接将setup函数中的this修改成了undefined。并且,我们只能同步使用setup函数,不能用async将其设为异步。
简单使用
首先,给一个setup函数的使用简例:
<template>
<div id="app">
<p>{{ name }}</p>
</div>
</template>
<script>
// 不要忘记import
import { ref } from 'vue'
export default {
setup(){
const name = ref('王路飞')
return { name }
}
}
</script>
这里ref函数的作用就是创建并返回一个响应式引用。此时需要注意的是,这个name并不会返回一个字符串类型的值,而是一个响应式对象。而通过return返回的对象中的属性,就都可以在模板中使用了。
如果我们想要在setup中使用methods的用法,我们需要做如下操作:
<template>
<div id="app">
<p>{{ name }}</p>
<p>{{ age }}</p>
<button @click="addOne">加一</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup(){
const name = ref('王路飞')
const age = ref(17)
function addOne(){
age.value++
}
return {name, age, addOne}
}
}
</script>
刚才说到,在setup里是无法使用this的,并且age不是一个number类型数据,而是一个响应式对象,所以无法直接改变它的值。那么修改方法就是使用value。同理,如果我们想修改name的值,也需要使用value的方式。
如果我们想要在setup中使用computed的用法,我们需要做如下操作:
<template>
<div id="app">
<p>姓名:{{ name }}</p>
<p>年龄:
<button @click="changeAge(-1)">-</button>
{{ age }}
<button @click="changeAge(1)">+</button>
</p>
<p>出生年份:{{year}}</p>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup(){
const name = ref('王路飞')
const age = ref(17)
const year = computed(() => {
return 2020 - age.value
})
function changeAge(val){
age.value += val
}
return {name, age, changeAge, year}
}
}
</script>
在这种情况下,如果我们想修改year的值,无法通过year.value的方式获取,我们需要使用getter和setter。
<template>
<div id="app">
<p>姓名:{{ name }}</p>
<p>年龄:
<button @click="changeAge(-1)">-</button>
{{ age }}
<button @click="changeAge(1)">+</button>
</p>
<p>出生年份:
<button @click="changeYear(-1)">-</button>
{{ year }}
<button @click="changeYear(1)">+</button>
</p>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup(){
const name = ref('王路飞')
const age = ref(17)
const year = computed({
get: () => {
return 2020 - age.value
},
set: val => {
age.value = 2020 - val
}
})
function changeAge(val){
age.value += val
}
function changeYear(val){
year.value += val
}
return {name, age, changeAge, year, changeYear}
}
}
</script>
现在通过上面的内容,我们知道,我们想要使用这些值,需要使用ref,获取值还要.value,而且这些响应式对象还需要通过return返回。先不说这样写大家觉得累不累,但至少如果响应式对象很多(当然一定会很多),那么return里要写的内容可太长了。所以这样操作显然还是比较繁琐的。除此以外,ref函数只能监听简单类型的变化,不能监听复杂类型的变化,比如对象和数组。
综上所述,Vue就提供了另一种定义响应式对象的方式就是:允许我们定义一个响应式对象,然后把我们想使用的值都放在对象内,当作对象属性。而如果我们想这么用,那我们需要先导入一个函数,reactive,它的作用就是创建并返回一个响应式对象。经过筛减,我们的代码现在如下所示:
<template>
<div id="app">
<p>姓名:{{ data.name }}</p>
<p>年龄:
<button @click="changeAge(-1)">-</button>
{{ data.age }}
<button @click="changeAge(1)">+</button>
</p>
<p>出生年份:
<button @click="changeYear(-1)">-</button>
{{ data.year }}
<button @click="changeYear(1)">+</button>
</p>
</div>
</template>
<script>
import { reactive,computed } from 'vue'
export default {
setup(){
const data = reactive({
name: '王路飞',
age: 17,
year: computed({
get: () => {
return 2020 - data.age;
},
set: val => {
data.age = 2020 - val;
}
})
});
function changeAge(val){
data.age += val;
}
function changeYear(val){
data.year += val;
}
return { data, changeAge, changeYear };
}
}
</script>
需要注意的是,之前的age.value,现在变成了data.age,这部分一定要注意观察区别。也就是说用这种方式,除了methods现在所有的内容都放在了data里,方便我们的查阅。而且在template里,之前只写age,现在也要写成data.age。
reactive的注意点
reactive参数必须是对象(json / arr),否则无法实现响应式。
setup(){
let state = reactive(123);
function myFn(){
state = 666; //由于在创建响应式数据的时候传递的不是一个对象,所以无法实现响应式
console.log(state); //输出666,但是页面无变化
}
return { state, myFn };
}
setup(){
let state = reactive({
age: 17
});
function myFn(){
state.age = 666;
console.log(state); //输出666,页面变化
}
return { state, myFn };
}
setup(){
let state = reactive([1,3,5]);
function myFn(){
state[0] = 100;
console.log(state); // 页面变化
}
return { state, myFn };
}
并且在console.log(state)后,我们可以发现Proxy,那也就证明了,Vue3中的响应式数据是通过ES6的Proxy来实现的。
如果给reactive传递了其他对象,默认情况下修改对象,界面不会自动更新。如果想更新,可以通过重新赋值的方式。
setup(){
let state = reactive({
time: new Date()
});
function myFn(){
// 直接修改,页面不会更新:
state.time.setDate(state.time.getDate() + 1 );
console.log(state.time); // 日期变更,页面无变化
// 重新赋值
const newTime = new Date(state.time.getTime());
newTime.setDate(state.time.getDate() + 1);
state.time = newTime;
console.log(state.time); // 日期变更,页面更新
}
return { state, myFn };
}
ref的注意点
虽然刚才提到ref用起来比较繁琐,但reactive也有一些问题。由于刚才证明了reactive必须传递一个对象,所以导致在企业开发中,如果我们只想让某个变量实现响应式的时候会非常麻烦,所以之后就需要考虑该如何选择使用reactive和ref。一定需要注意:ref函数只能监听简单类型的变化,不能监听复杂类型的变化,比如对象和数组。
但实际上ref底层的本质其实是reactive,系统会自动根据我们给ref传入的值将它转换成ref(xx) -> reactive({value:xx})。所以这也就是为什么,我们从ref获取值,需要用value的原因了。
setup(){
let age = ref(17)
function myFn(){
age.value = 20;
}
return { age,myFn };
}
而我们在template里不需要使用age.value的原因是Vue会自动帮我们添加.value。
Vue判断响应式对象是否为ref的方法:其中有一个__v_isRef
值,它会记录该数据到底是ref还是reactive。
除此以外,Vue2时也有个ref:
<template>
<div id="app">
<div ref="box">我是div</div>
</div>
</template>
<script>
export default {
setup(){
console.log(this.$refs.box)
}
}
</script>
但直接在setup里这么使用,是不行的,它无法识别,因为没有定义。
那么在其中,可以这么去用:
<template>
<div id="app">
<div ref="box">我是div</div>
</div>
</template>
<script>
import { ref,onMounted } from 'vue'
export default { setup(){
let box = ref(null); // reactive({value: null})
onMounted(()=>{console.log('onMounted',box.value); //到了相应生命周期才会执行,结果:<div>我是div<div>
});
console.log(box.value); //虽然放在后侧也会先执行,结果:null
return { box };
}
}
</script>
递归监听和非递归监听
默认情况下,无论是ref还是reactive都是递归监听。递归监听就是无论在响应式对象中有多少层内容,每一层它都会去进行Proxy监听。因此递归监听的问题就是,如果数据量比较大,它会非常消耗性能。
let state = reactive({
a: 'a',
b: {
c: 'c',
d: {
e: 'e'
}
}
})
而对于非递归监听,它用到的是shallowRef和shallowReactive。
let state = shallowReactive({
a: 'a',
b: {
c: 'c',
d: {
e: 'e'
}
}
})
这时我们可以看到,除了第一层以外,都没有了proxy监听,也就无法对除了第一层以外的这些内容进行更新:
但是对于ref有些特殊:
setup(){
let state = shallowRef({
a: 'a',
b: {
c: 'c',
d: {
e: 'e'
}
}
})
function myFn(){
state.value.a = '1'
state.value.b.c = '3'
state.value.b.d.e = '5'
console.log(state)
console.log(state.value)
console.log(state.value.b)
console.log(state.value.b.d)
}
return { state,myFn };
}
测试一下可以发现,虽然我们对state.value.a进行了修改,但是在页面中却没有发生更新,难道ref连第一层都不能监听了?
其实是因为,刚才提到ref和reactive的转换关系,ref(xx) -> reactive({value:xx}),同理的,shallowRef(xx) -> shallowReactive({value:xx})。所以第一层的监听是state.value。并且我们需要如下面代码这样修改才能监听到变化,因为我们之前提到ref是无法监听复杂类型数据的变化的,那就只能通过重新赋新对象的方式了:
setup(){
let state = shallowRef({
a: 'a',
b: {
c: 'c',
d: {
e: 'e'
}
}
})
function myFn(){
state.value = {
a: '1',
b: {
c: '3',
d: {
e: '5'
}
}
}
console.log(state)
console.log(state.value)
console.log(state.value.b)
console.log(state.value.b.d)
}
return { state,myFn };
}
而且通过这种方式,就不仅能修改第一层a的值,甚至能修改之后所有内容的值。所以简单来说,shallowRef貌似没什么限制效果。除此以外,我们还可以看到,shallow的值已经是true了:
那既然我们为其设置非递归监听,却还能修改其他层级的数据,那我们之后修改其他层级的数据只能通过重新创建新对象的方式来实现吗?那么Vue就为我们提供了一种方式:triggerRef。这个triggerRef就会查看之前state中发生变化的数据,然后主动帮我们更新。
<script>
import { shallowRef,triggerRef } from 'vue'
export default {
setup(){
let state = shallowRef({
a: 'a',
b: {
c: 'c',
d: {
e: 'e'
}
}
})
function myFn(){
state.value.b.d.e = '5'
triggerRef(state)
}
return { state,myFn };
}
}
</script>
虽然之前所有内容都是成对出现,但这里是没有triggerReactive的,也就是说,非递归监听下,我们就真没办法修改其中任意一层的数据了,更何况递归监听下,修改响应式对象的数据是很简单的,不需要这种方式的存在。
所以综上所述,一般情况下我们使用 ref和reactive即可,只有在需要监听的数据量比较大的时候,才考虑使用shallowRef和shallowReactive。
toRefs
ref复制, 修改响应式数据,不会影响以前的数据,界面会更改。
toRef引用, 修改响应式的数据,会影响以前的数据,界面不会更新。
toRef的使用场景
如果想让响应式数据和原始数据关联起来。
并且更新响应式数据后,不想视图更新;那么就可以使用toRef
小结:
ref和toRef的区别
(1). ref本质是拷贝,修改响应式数据不会影响原始数据;toRef的本质是引用关系,修改响应式数据会影响原始数据
(2). ref数据发生改变,界面会自动更新;toRef当数据发生改变是,界面不会自动更新
(3). toRef传参与ref不同;toRef接收两个参数,第一个参数是哪个对象,第二个参数是对象的哪个属性
除此以外,代码还能再简洁一点,就是需要导入toRefs函数,这个函数的作用就是将一个响应式函数的对象,转变为普通的对象。但是这个普通的对象里的内容,又都是响应式对象,所以还需要解构,也就是应该这么使用: …toRefs(data)。
这么做之后,在template里就不需要写data了,完整代码如下:
<template>
<div id="app">
<p>姓名:{{ name }}</p>
<p>年龄:
<button @click="changeAge(-1)">-</button>
{{ age }}
<button @click="changeAge(1)">+</button>
</p>
<p>出生年份:
<button @click="changeYear(-1)">-</button>
{{ year }}
<button @click="changeYear(1)">+</button>
</p>
</div>
</template>
<script>
import { reactive,computed,toRefs } from 'vue'
export default {
setup(){
const data = reactive({
name: '王路飞',
age: 17,
year: computed({
get: () => {
return 2020 - data.age;
},
set: val => {
data.age = 2020 - val;
}
})
});
function changeAge(val){
data.age += val;
}
function changeYear(val){
data.year += val;
}
return { ...toRefs(data), changeAge, changeYear };
}
}
</script>
再在此的基础上,我们还能把data部分的数据拿出来,写在一个函数中,这样就更方便我们区分各个功能模块了:
<script>
import { reactive,computed,toRefs } from 'vue'
export default {
setup(){
const { data, changeAge, changeYear } = test();
return { ...toRefs(data), changeAge, changeYear };
}
}
function test(){
const data = reactive({
name: '王路飞',
age: 17,
year: computed({
get: () => {
return 2020 - data.age;
},
set: val => {
data.age = 2020 - val;
}
})
});
function changeAge(val){
data.age += val;
}
function changeYear(val){
data.year += val;
}
return { data, changeAge, changeYear };
}
</script>
那这样一来,我们就还可以把test函数从这个vue组件当中提取出来,放在一个js文件当中集中管理相关功能了。
toRef
上面用到了toRefs,但其实还有一个toRef,功能一样。
首先先看个结论:
setup(){
let obj = {name: 'zs'};
let state = ref(obj.name);
function myFn(){
state.value = 'ls';
console.log(state.value); // 输出ls,页面更新
console.log(obj.name); // 输出zs
}
return { state,myFn };}
从这里可以发现,如果利用ref将某一个对象中的属性变成响应式的数据,那我们修改响应式的数据是不会影响到原始数据的。
但当我们使用toRef将某一个对象中的属性变成响应式的数据,那我们修改响应式的数据是会影响到原始数据的。但是此时就不会触发页面的更新了。
setup(){
let obj = {name: 'zs'};
let state = toRef(obj, 'name');
function myFn(){
state.value = 'ls';
console.log(state.value); // 输出ls,页面不更新
console.log(obj.name); // 输出ls,页面不更新
}
return { state,myFn };
}
所以ref和toRef的区别是:
ref是对原始数据的复制,修改响应式数据不会影响原始数据,同时数据发生改变,界面就会自动更新。
toRef是对原始数据的引用,修改响应式数据会影响原始数据,但数据发生改变,界面不会自动更新。
toRef的应用场景就是:响应式数据和原始数据相关联,但我们又不想更新数据后更新界面,那么就可以使用toRef。
所以toRefs还可以这么用:
<template>
<div id="app">
<p>{{name}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { toRefs } from 'vue'
export default {
setup(){
let obj = {name: 'zs', age: 17};
let state = toRefs(obj);function myFn(){
state.name.value = 'ls';
state.age.value = '16';
}
return { ...toRefs(state), myFn };
}
}
</script>
customRef
customRef的作用是返回一个ref对象,可以显示地控制依赖追踪和触发响应。
<template>
<div id="app">
<p>{{age}}</p>
<button @click="myFn">按钮</button></div>
</template>
<script>
import { customRef } from 'vue'
function myRef(value){
return customRef((track, trigger)=>{
return{
get(){
track(); //告诉Vue这个数据需要追踪变化
return value;
},
set(newValue){
value = newValue;
trigger(); //告诉Vue触发界面更新
}
}
});
}
export default {
setup(){
let age = myRef(18);
function myFn(){
age.value += 1;
}
return { age,myFn };
}
}
</script>
那么什么时候考虑选用customRef呢?
如果我们想要去获取json数据,我们需要这么使用:(自己在public中建了一个data.json)
setup(){
let state = ref([]);
fetch('../public/data.json')
.then((res)=>{
return res.json();
})
.then((data)=>{
console.log(data);
state.value = data;
})
.catch((err)=>{
console.log(err);
})
return { state };
}
(这里补充一下,我之前使用@vue/cli的时候,这个方式去调用json数据会报错。但是根据报错信息去检查时,发现自己写的没有问题,所以在这里沉思一段时间,查阅资料也没找到解决方法。后来改用Vite创建项目,发现问题直接解决,暂时不知道原因,但之前提到过Vite是Vue作者开发的一款意图取代webpack的工具,也许Vite中帮我们处理了json数据。因此如果使用@vue/cli遇到json数据报错,却找不到原因的话,可以考虑使用Vite创建项目进行尝试)
言归正传,之前提到过,setup是不能异步使用的,也就是在这里不能用async和await去获取json数据,那么之后获取更多数据,就要在其中写这么多的回调函数显然不方便管理。这时,我们可以考虑使用customRef:
<script>
import { customRef } from 'vue'
function myRef(value){
return customRef((track, trigger)=>{
fetch(value)
.then((res)=>{
return res.json();
})
.then((data)=>{
console.log(data);
value = data;
trigger();
})
.catch((err)=>{
console.log(err);
})
return{
get(){
track(); //告诉Vue这个数据需要追踪变化
return value;
},
set(newValue){
value = newValue;
trigger(); //告诉Vue触发界面更新
}
}
});
}
export default {
setup(){
let state = myRef('../public/data.json');
return { state };
}
}
</script>
参数props和context
setup函数,它其实是可以设置一些参数的,一个叫做props,一个叫做context。
这个props参数是用来获取在组件中定义的props的。如下所示:
export default {
props:{
title: String
},
setup(props){
const data = reactive({
name: '王路飞',
age: 17,
year: computed({
get: () => {
return 2020 - data.age;
},
set: val => {
data.age = 2020 - val;
}
})
});
function changeAge(val){
data.age += val;
console.log(props.title)
}
function changeYear(val){
data.year += val;
}
return { ...toRefs(data), changeAge, changeYear };
}
}
需要注意的是props.title,也就是通过props传递过来的值都只是可读的,我们无法修改。并且我们定义的所有props都是响应式的,我们可以监听props的值,一旦发生变化我们就做出相应。比如我们想监听title的值,我们需要使用watch,如下代码:
<script>
import { reactive,computed,toRefs,watch } from 'vue'
export default {
props:{
title: String
},
setup(props){
const data = reactive({
name: '王路飞',
age: 17,
year: computed({
get: () => {
return 2020 - data.age;
},
set: val => {
data.age = 2020 - val;
}
})
});
function changeAge(val){
data.age += val;
console.log(props.title)
}
function changeYear(val){
data.year += val;
}
watch(() => props.title, (newTitle, oldTitle) => {
console.log(newTitle,oldTitle)
})
return { ...toRefs(data), changeAge, changeYear };
}
}
</script>
对于第二个参数context,我们之前说到在setup里是不能使用this的,但如果我们有些功能需要使用this,我们就可以使用context来获取attribute,获取插槽,或者发送事件。比如:
setup(props, context){
const data = reactive({
name: '王路飞',
age: 17,
year: computed({
get: () => {
return 2020 - data.age;
},
set: val => {
data.age = 2020 - val;
}
})
});
function changeAge(val){
data.age += val;
console.log(props.title)
}
function changeYear(val){
data.year += val;
}
watch(() => props.title, (newTitle, oldTitle) => {
console.log(newTitle,oldTitle);
context.emit('title-change');
})
return { ...toRefs(data), changeAge, changeYear };
}
它们的使用方法和以前还是一样,比如使用emit,在父组件中依然根据这里的名称,去用on来接收就可以了。
toRaw
我们现在知道,在setup函数里,只有响应式的数据发生改变,页面才有可能发生更新,也就是在setup函数里一个普通对象发生改变,是无论如何都不能引起页面更新的。那现在我们去做这些操作:
setup(){
let obj = {name: 'zs', age: 17};
let state = reactive(obj);
console.log(obj === state); // false
function myFn(){
// 对obj操作无法让页面发生更新,但是会修改obj数据
obj.name = 'ls'
console.log(obj);
// 对state操作才能让页面发生更新,同时会修改obj数据
// state.name = 'ls';
// console.log(state);
}
return { state,myFn };}
那么这里的obj和state是什么关系呢?
它们是引用关系,state的本质是一个Proxy对象,在这个Proxy对象中引用了obj。
说到toRaw的作用,它会从reactive或ref中得到原始数据(引用)。也就是说:
let obj = {name: 'zs', age: 17};
let state = reactive(obj);
let obj2 = toRaw(state);
// let state = ref(obj);
// let obj2 = toRaw(state.value);
console.log(obj === state); // false
console.log(obj === obj2); // true
那么这么做有什么用?
因为ref和reactive每次修改都会被追踪,都会更新UI界面,所以如果有一些操作不需要追踪,不需要更新界面,那么这个时候toRaw的作用就体现出来了,因为它拿到原始数据,对原始数据进行修改就不会被追踪了,从而让性能提升。
除此以外还有一个markRaw。如果某数据永远都不想被追踪,就可以使用markRaw。
let obj = {name: 'zs', age: 17};
obj = markRaw(obj);
let state = reactive(obj);
readonly
readonly和它的名字一样,数据只能是只读的,并且是递归只读的。也就是它里面所有层级都是只读的。比如下面代码:
<template>
<div id="app">
<p>{{state.name}}</p>
<p>{{state.attr.age}}</p>
<p>{{state.attr.height}}</p>
<button @click="myFn">按钮</button>
</div>
</template>
<script>
import { readonly,isReadonly,shallowReadonly } from 'vue'
export default {
setup(){
let state = readonly({name: 'ls', attr:{age: 18,height: 1.88}});
function myFn(){
state.name = 'zs';
state.attr.age = 16;
state.attr.height = 1.66;
}
return {state, myFn};
}
}
</script>
当我们试图去修改state的数据时,就会出现提示:
shallowReadonly就可以让只有第一层是只读的:
setup(){
let state = shallowReadonly({name: 'ls', attr:{age: 18,height: 1.88}});
function myFn(){
state.name = 'zs';
state.attr.age = 16;
state.attr.height = 1.66;
console.log(state);
}
return {state, myFn};
}
虽然设置shallowReadonly只有第一层是只读的,但是对其它内容进行修改,却无法让视图发生更新。
isReadonly就很简单了:
function myFn(){
console.log(isReadonly(state));
}
对于只读类型数据,第一时间就可以想到const。const和readonly的区别:
我们知道如果const声明了一个对象,对象内部数据再重新赋值,依然是可以修改的,那么const做到的其实是赋值保护,即不能给变量重新赋值。但readonly是属性保护,即不能给属性重新赋值。
了解了Composition API之后,需要说的是,它的本质其实就是Vue2.x的Option API。setup函数return时,它就会把响应式对象写入到data里,把methods方法写入到methods里,其他功能都是同理。