Options API的弊端
在vue2中,我们编写组件的方式是Options API:
- Options API的一大特点就是在对应的属性中编写对应的功能模块
- 比如data定义数据、methods定义方法、computed定义计算属性、watch监听属性,包括生命周期钩子
但这种代码有一个很大的弊端:
- 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中
- 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
- 尤其对于那些一开始没有编写这个组件的人来说,这个组件的代码是难以阅读和理解的(对于维护的人来说)
认识Composition API
为了使用Composition API,我们需要有一个可以实际使用它的地方
在vue组件中,这个地方就是setup函数
setup其实也是组件的一个选项,类似于data、methods等,但是这个选项非常强大,可以替代之前的大部分选项。
setup的参数
我们先来研究setup函数的参数,它主要有两个参数:
- 第一个参数:props
- 第二个参数:context
props就是原本的那个props,但是在setup中无法访问this,所以需要显示的传递一个props:
- 定义props类型的数据,还是和之前一样
- 使用也是,直接{{ message }}使用即可
- 如果想要在setup中使用props,不可以通过this来获取
- 而是直接作为参数传递来使用
另一个参数context,也可以称之为SetupContext,它里面包含三个属性:
- attrs:所有的非props的attribute
- slots:父组件传递过来的插槽,一般使用是在“template”模板中,这里传递是为了通过“render”函数使用,开发中不常用
emit:和props一样,无法通过this访问(例如:this.$emit),所以需要额外传递
setup函数的返回值
setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
setup的返回值可以在模板template中被使用
- 也就是说我们可以通过setup的返回值来替代data选项
```vue
Home Page
{{ message }}
{{ title }}{{ counter }}
可以看到,我们直接在setup中定义了“counter”用于在页面显示,同时我么还定义了“increment”方法来更改counter的值,然后在setup中通过return返回一个对象,对象中包含counter和increment,然后在template中直接使用,不过这里的increment事件触发后,虽然counter的值发生了变化,但是在页面上没有更新。
<a name="IORfB"></a>
# Reactive API
如果想给setup中定义的数据提供响应式的特性,那么我们可以使用reactive函数:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644848085292-56f62fdf-fde2-4cdc-a915-33488325f945.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=97&id=u7542f057&margin=%5Bobject%20Object%5D&name=image.png&originHeight=97&originWidth=356&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9526&status=done&style=none&taskId=ubfc838cf-121e-497a-bb45-85349e4d0ad&title=&width=356)<br />为什么使用reactive函数就可以变成响应式数据呢?
- 这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行以来收集;
- 当数据发生改变时,所有收集到的以来都会进行对应的响应式操作
- 我们vue2中用的data选项,也是在内部通过reactive实现了响应式,不过没有对外暴露出来
使用时我们先将state返回<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644848458606-92f43ac4-4e7b-4f44-ac6d-ae9889e52bf4.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=166&id=u18f8b7d3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=166&originWidth=317&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9218&status=done&style=none&taskId=uf8168669-2a0c-44ea-907d-36296abb90c&title=&width=317)<br />然后通过state.counter获取到已经实现响应式的数据<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644848490615-0608661f-db5b-462b-afef-e117a472fb33.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=90&id=ub5366e96&margin=%5Bobject%20Object%5D&name=image.png&originHeight=90&originWidth=319&originalType=binary&ratio=1&rotation=0&showTitle=false&size=7030&status=done&style=none&taskId=ubc8a24a9-d6d1-4051-b4ac-c001549eebd&title=&width=319)
<a name="Y4AzE"></a>
# Ref API
reactive API对传入的类型是有限制的,他要求我们必须传入一个对象或者数组:
- 如果我们传入一个基本数据类型会报一个警告
这个时候Vue3给我们提供了另外一个API:ref API
- ref会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是ref名称的来源
- 它内部的值是在ref的value属性中被维护的
有两个注意事项:
- 在模板中使用ref的值时,vue会自动帮我们进行解包处理,所以不需要在模板中通过ref.value的方式来使用
- 但是在setup函数内部,它依然是一个ref引用,所以需要使用ref.value的方法修改ref的值
我们可以这样使用Ref API<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644849486165-447ec45b-c0a7-490f-8203-0da573aaa03b.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=31&id=ue20ffaa9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=31&originWidth=335&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5016&status=done&style=none&taskId=ua7abae3f-6453-43de-b8fc-0b7a0bd3715&title=&width=335)<br />在increment函数中,想要改变counter的值,需要通过counter.value,这是因为通过ref处理后,counter的值保存在counter.value中<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644849521803-4b8b8000-d2af-4936-bc42-337f720d08e6.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=93&id=u26684cee&margin=%5Bobject%20Object%5D&name=image.png&originHeight=93&originWidth=378&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8589&status=done&style=none&taskId=ub8aa8cc4-2b7d-4bbb-8ea1-c93e8d765b7&title=&width=378)<br />返回没有别的区别<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644849584224-d7019773-8959-40f2-bfea-a110f2d4f667.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=132&id=u81cda250&margin=%5Bobject%20Object%5D&name=image.png&originHeight=132&originWidth=185&originalType=binary&ratio=1&rotation=0&showTitle=false&size=6350&status=done&style=none&taskId=u1c67d7d4-7270-41d8-a0e2-d3e881e9261&title=&width=185)<br />在使用时就不需要通过counter.value了,因为template模板中做了解包的处理,会自动获取ref对象的value值<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644849634188-9238f4ce-10cf-43d4-8da5-0aff9c955faa.png#clientId=uc354e1bf-0d2b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=102&id=u01f10cd3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=102&originWidth=235&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5813&status=done&style=none&taskId=ub8d71c7a-1f15-49de-a57c-f2aad9eada2&title=&width=235)
<a name="Sl9fH"></a>
# 认识readonly
我们通过reactive或者ref可以获取到一个响应式的对象,但是在某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
- vue3为我们提供了readonly方法
- readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,就个一个proxy的set方法被劫持,并且不能对其进行修改)
下面的代码中我们将info做了readonly处理,然后点击按钮触发updateState函数,会发现无法修改info中的值,并且会报警告
```vue
<template>
<div>
{{ readonlyInfo.name }}
<button @click="updateState">按钮</button>
</div>
</template>
<script>
import { readonly, reactive } from "vue";
export default {
setup() {
const info = reactive({
name: "zx",
});
const readonlyInfo = readonly(info);
const updateState = () => {
readonlyInfo.name = "coder";
};
return {
readonlyInfo,
updateState,
};
},
};
</script>
Reactive判断的API
isProxy
检查对象是否是由reactive或readonly创建的proxy
isReactive
检查对象是否由reactive创建的响应式代理:
如果该代理是readonly创建的,但包裹了由reactive创建的另一个代理,它也会返回true
isReadonly
-
toRaw
fanhuireactive或readonly代理的原始对象(不建议保留对原始对象的持久引用)
shallowReactive
创建一个响应式代理,它跟踪其自身property的响应性,但不执行嵌套对象的深层响应式转换(生曾还是原生对象)
shallowReadonly
创建一个proxy,使其自身的property为只读,但不执行嵌套对象的深度只读转换(深层不受影响,还是可读可写的)
toRefs
如果我们使用结构语法获取reactive包裹的值,那么无论之后怎么修改取出的值,还是修改原本的state对象,数据都不再是响应式的。
但是开发中我们也许会用到reactive中的某个值,如何保证结构出来的值是响应式的呢?
- vue提供了toRefs函数,可以将reactive返回的对象中的值转成ref
这种做法相当于对state.xxx和ref.value之间建立了链接,修改任何一个都会引起另一个的改变。
<template>
<div>
<h2>{{ name }}</h2>
<h2>{{ age }}</h2>
<button @click="changeAge">+1</button>
</div>
</template>
<script>
import { reactive, toRefs } from "vue";
export default {
setup() {
const info = reactive({
name: "zx",
age: 18,
});
let { name, age } = toRefs(info);
const changeAge = () => {
age.value++;
};
return {
name,
age,
changeAge,
};
},
};
</script>
在页面中使用直接使用暴露的变量名即可,但是修改时因为是ref的原因,所以需要修改value值,或者我们直接修改Info.age也是会同时影响到age并且页面会做出响应。
toRef
toRef作用和toRefs是一样的,区别在于toRef只能转换一个属性,而不是像toRefs一样可以转换多个,用法也稍有区别,因为是转换对象中的某个属性,所以是接收两个参数,一个是要转换的属性所在的对象,还有就是要转换的元素。
<template>
<div>
<h2>{{ age }}</h2>
<button @click="changeAge">+1</button>
</div>
</template>
<script>
import { reactive, toRef } from "vue";
export default {
setup() {
const info = reactive({
name: "zx",
age: 18,
});
let age = toRef(info, "age");
const changeAge = () => {
age.value++;
};
return {
age,
changeAge,
};
},
};
</script>
ref其他的API
unref
如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
- 如果参数是一个ref,则返回内部值,否则返回参数本身
这是 val = isRef(val) ? val.value : val 的语法糖函数
isRef
-
shallowRef
-
tiggerRef
-
customRef
这是一个开发中很少用到的API。
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制: 它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数
- 并且返回一个带有 get 和 set 的对象
我们通过customRef来实现一个debounce(节流)的操作:
import { customRef } from "vue";
// 自定义ref
export default function (value) {
let timer = null;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, 1000);
},
};
});
}
按照我目前的理解,我觉得这是一个带有vue响应式系统的hook。
- 它类似于hook的功能
- 内置了customRef和track,trigger等vue响应式相关的API
setup中使用computed
vue3中,可以在setup中使用computed函数,computed函数接收两种传值,一种是接收一个函数
还有一种是接收一个对象const fullName = computed(() => firstName.value + lastName.value);
用法和在vue2中没有区别。const fullName = computed({ get: () => firstName.value + " " + lastName.value, set(newValue) { const names = newValue.split(" "); firstName.value = names[0]; lastName.value = names[1]; }, });
但是需要注意一点,computed函数返回的是一个ref对象,所以如果想要修改fullName,需要像修改别的ref一样通过fullName.valueconst changeName = () => { firstName.value = "James"; };
setup中使用watch
在vue2的options API中,我们可以通过watch来监听data或者props的数据变化,当数据变化时进行一些操作。
在composition API中,我们可以使用watchEffect和watch来完成响应式数据的监听
- watchEffect用于自动收集响应式数据的依赖
- watch需要我们手动指定监听器
举例说明:
<template>
<div>
<h2>{{ name }}-{{ age }}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { watchEffect, ref } from "@vue/runtime-core";
export default {
setup() {
const name = ref("why");
const age = ref(18);
const changeName = () => (name.value = "kobe");
const changeAge = () => age.value++;
watchEffect(() => {
console.log("name:", name.value);
console.log("age:", age.value);
});
return {
name,
age,
changeName,
changeAge,
};
},
};
</script>
上面的代码,当我们触发changeName或者changeAge事件时,因为name和age在watchEffect中出现,那么就会自动触发,不需要我们像在vue2中那样监听。
它的执行原理是watchEffect会自动监听响应式的数据,当他们发生改变,就会回调watchEffect函数。
停止监听
监听是要占据浏览器资源的,有时我们可能会在某些条件下停止监听,那么可以这么做:
<template>
<div>
<h2>{{ name }}-{{ age }}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { watchEffect, ref } from "@vue/runtime-core";
export default {
setup() {
const name = ref("why");
const age = ref(18);
const stop = watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => (name.value = "kobe");
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
};
return {
name,
age,
changeName,
changeAge,
};
},
};
</script>
watchEffect函数有一个返回值,我们可以接收这个返回值,并且在我没不想要继续监听的时候调用这个函数,比如上面代码就是在age.value>25的时候,我们调用了stop()函数,监听就停止了。
watchEffect清除副作用
什么是清除副作用?
- 比如我们在开发中需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。
- 那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用。
```vue
{{ name }}-{{ age }}
我们通过setTimeout模拟网络请求,如果我们不处理,那么会在2S后打印“网络请求成功”,而我们在onInvalidate函数中执行了clearTimeout(timer),当我们再次触发watchEffect后,就清除了timer。在真实的开发中,我们并没有取消之前的那次网络请求,因为请求已经发送出去了,我们已经无法控制了,但是我们可以选择不接收,timer中的console就是模拟请求成功后返回的数据,我们可以只在我们获取想要的请求数据时再做别的操作(更改数据之类的)。
<a name="scHeM"></a>
# setup中使用ref
vue2中,我们想要获取dom或者组件,可以通过this.refs.xxx来实现(vue不推荐这种行为),但是在composition API中,没有this,我们怎么获取ref呢?
```vue
<template>
<div>
<h2 ref="title">哈哈哈</h2>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const title = ref(null);
console.log(title.value);
return {
title,
};
},
};
</script>
vue提供了ref函数,用于生成ref,使用过程是,在setup中定义一个变量,这个变量通过ref函数生成,初始传值为null,然后return暴露出去,template中通过ref=”title”实现ref的绑定,但是这个时候我们无法获取title的value值,因为还没有挂载。如果想要能够获取到ref,需要在setup中使用生命周期函数,或者是 使用watchEffect。
但是使用watchEffect会有一个问题,执行时机的问题:
会因为初始就执行,获取到最开始的null值,不过可以解决
watchEffect除了接收第一个函数参数外,还可以接收第二个参数,watchEffect的配置对象,其中有个参数flush,用于处理监听的时机,默认值“pre”,和上面不写效果一致,如果写成“post”,就会在dom挂载完成后在执行watchEffect的回调。
watch的使用
watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用
- 默认情况下它是惰性的,只有当被侦听的数据源发生变化时才会执行回调
与watchEffect的比较,watch允许我们:
- 懒执行副作用(第一次不会直接执行)
- 更具体的说明当那些状态发生变化时,出发侦听器
- 可以访问侦听器状态变化前后的值(newValue,oldValue)
watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref)
- 直接写入一个可响应式的对象,ref(如果是一个reactive的对象的侦听,需要进行某些转换)
-
侦听多个数据源
侦听多个数据源其实并没有什么复杂的地方,就是在watch中侦听的时候使用数组,接收新值旧值的时候使用数组接收即可。watch([info, name], ([newInfo, newName], [oldInfo, oldName]) => { console.log(newInfo, newName, oldInfo, oldName); });
侦听器的其他选项
在vue2中watch会有deep之类的选项,compositionAPI中同样可以使用
watch接收的第二个参数就是一些watch的配置项,可以在里面配置deep,immediate等值,效果和vue2一致。watch(name, (newValue, oldValue) => { console.log(newValue, oldValue); },{ deep:true });
生命周期钩子
setup用来替代data、methods、computed、watch等选项,也可以替代生命周期。
-
那么在setup中如何使用生命周期呢?
- 可以使用直接导入的 onX 函数注册生命周期钩子
原本vue2的生命周期,除了beforeCreate和created以外,都以名称前加on的形式存在,使用时引入即可,和vue2中使用方法一致。而beforeCreate和created两个生命周期,则直接在setup中书写即可。
import { onMounted, onUnmounted, onUpdated } from "@vue/runtime-core";
export default {
setup() {
onMounted(() => {
console.log("App Mounted2");
});
onMounted(() => {
console.log("App Mounted1");
});
onUpdated(() => {
console.log("App Updated");
});
onUnmounted(() => {
console.log("App Unmounted");
});
}
}
provide和inject
在vue3中使用provide和inject也与vue2有些不同。vue3中我们借助于provide和inject函数来实现。
<template>
<home />
<button @click="increment">+1</button>
</template>
<script>
import { provide, ref } from "vue";
import Home from "./Home.vue";
export default {
components: { Home },
setup() {
const name = ref("zx");
let counter = ref(100);
provide("name", name);
provide("counter", counter);
const increment = () => {
counter.value += 1;
};
return {
name,
counter,
increment,
};
},
};
</script>
在父组件中,我们通过引入的provide函数来对外暴露变量“provide(“name”, name);”,provide函数的第一个参数是key值,用来标识暴露的变量的名称,第二个参数是value,是暴露的真正的值,如果需要暴露多个,就调用多次provide函数。
<template>
<div>
<h2>{{ name }}-{{ counter }}</h2>
</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
const name = inject("name");
const counter = inject("counter");
return {
name,
counter,
};
},
};
</script>
在子组件中我们使用inject函数来接收provide的暴露结果,“inject(“name”,defaultValue)”,inject函数其实也可以接收两个参数,第一个是provide暴露的key值,第二个则是默认参数,当我们无法获取到key值对应的value值时,可以使用默认参数。
单向数据流
vue要求单向数据流,也就是父组件提供的数据,子组件不能修改,只有父组件能够修改。但是上面的代码违背了单向数据流规则,因为父组件provide的数据是ref类型的,所以我们在子组件中也可以获取并且修改,为了解决这个问题,就需要用到前面学习的readonly函数。
provide("name", readonly(name));
provide("counter", readonly(counter));
像这样通过readonly处理一下再传给子组件,子组件中就无法修改值,而只能展示。
useCounter的封装
接下来我们通过一个案例来看一下compositionAPI到底有多好用
<template>
<div>
<h2>当前计数:{{ counter }}</h2>
<h2>计数*2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import useCounter from "./hooks/useCounter";
export default {
setup() {
const { counter, doubleCounter, increment, decrement } = useCounter();
return {
counter,
doubleCounter,
increment,
decrement,
};
},
};
</script>
import { ref, computed } from "vue";
export default function () {
const counter = ref(0);
const doubleCounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doubleCounter,
increment,
decrement,
};
}
我们可以将逻辑代码抽取到单独的文件中,这个文件包含一般意义上的vue2中的data中的变量,methods中的方法,computed中的计算函数等等,实现逻辑以后再暴露出去,我们就可以通过引入的方式来使用,这样把各个部分的代码抽离的实现方式会使我们的主文件代码更加简洁,而逻辑代码抽到一起也使维护更加简单,更加优雅。
顶层编写setup
一个vue的实验中的特性,以往编写vue的逻辑代码需要在script中通过export导出一个对象来实现,而这个新特性允许我们去掉这一部分,还有setup最后的return也可以省略。
<script>
import { ref } from "@vue/reactivity";
export default {
setup() {
const counter = ref(0);
const increment = () => counter.value++;
return {
counter,
increment,
};
},
};
</script>
上面的是常规的写法,可以借助新特性如下编写:
<script setup>
import { ref } from "@vue/reactivity";
const counter = ref(0);
const increment = () => counter.value++;
</script>