目录
1_认识CompositionAPI
1.1_Options API的弊端
在Vue2中,编写组件的方式是Options API:
- Options API的一大特点就是在对应的属性中编写对应的功能模块;
- 比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
但是这种代码有一个很大的弊端:
- 当实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
- 当组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
- 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
下面来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
- 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
- 并且当处理单个逻辑关注点时,需要不断的跳到相应的代码块中;
如果能将同一个逻辑关注点相关的代码收集在一起会更好。 这就是Composition API想要做的事情,以及可以帮助完成的事情。 也有人把Vue CompositionAPI简称为VCA。
1.2_认识Composition API
为了开始使用Composition API,需要有一个可以实际使用它(编写代码)的地方。在Vue组件中,这个位置就是 setup
函数;
setup是组件的另外一个选项:
- 只不过这个选项强大到可以用它来替代之前所编写的大部分其他选项;
- 比如methods、computed、watch、data、生命周期等等;
2_Setup函数的基本使用
2.1_setup函数的参数
setup函数的参数,它主要有两个参数:
- 第一个参数:props
- 第二个参数:context
参数 props
是父组件传递过来的属性,会被放到props对象中,在setup中如果需要使用,直接通过props参数获取
- 在props选项中定义类型;
- 在template中依然是可以使用props中的属性,比如message;
- 在setup函数中想要使用props,不可以通过 this 去获取;
- 因为props有直接作为参数传递到setup函数中,所以可以直接通过参数来使用即可;
参数context
,称之为SetupContext,它里面包含三个属性:
- attrs:所有的非prop的attribute;
- slots:父组件传递过来的插槽;
- emit:当组件内部需要发出事件时会用到emit(因为不能访问this,所以不可以通过 this.$emit发出事件);
2.2_setup函数的返回值
setup是一个函数,也有返回值,其用处?
-
setup的返回值可以在模板template中被使用;
-
即可以通过setup的返回值来替代data选项;
-
甚至可以返回一个执行函数来代替在methods中定义的方法
对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;
3_Reactive API
在setup中定义的数据提供响应式的特性,那么可以使用reactive的函数.
const test = reactive({
aaa : "hhh",
bbb : 3344
})
reactive的大致原理
- 当使用reactive函数处理的数据之后,数据再次被使用时就会进行依赖收集;
- 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
- 事实上,编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
3.1_Vue 的单向数据流
在 Vue.js 中,数据驱动是其核心原则之一,而单项数据流又是其中的一个重要概念。它指的是 Vue 组件中数据的传递方式只能从父组件到子组件,不允许反向传递。
单向数据流的作用
-
简化数据流动:单项数据流可以帮助更好地理解数据流动的方向,避免了数据流动的混乱和复杂性。
-
提高代码可维护性:由于数据流是单向的,因此数据发生变化时只需要查找受影响的组件即可,提高了代码的可维护性。
-
增强代码可预测性:由于数据流是单向的,所以可以预测数据会如何流动,从而更准确地追踪问题、排查错误。
在 Vue 中,单项数据流是通过父组件向子组件传递 props 参数实现的。父组件将数据作为 props 属性传递给子组件,子组件通过 props 接收父组件传递的数据,并在组件内部使用。
3.2_readonly
通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
- Vue3为提供了
readonly
的方法; - readonly会返回原始对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
在开发中常见的readonly方法会传入三个类型的参数:
- 类型一:普通对象;
- 类型二:reactive返回的对象;
- 类型三:ref的对象;
readonly使用过程中,有如下规则:
-
readonly返回的对象都是不允许修改的;
-
但是经过readonly处理的原来的对象是允许被修改的;
-
比如 const info = readonly(obj),info对象是不允许被修改的;
-
当obj被修改时,readonly返回的info对象也会被修改;
-
但是不能去修改readonly返回的对象info;
-
readonly在传递给其他组件数据时,希望其他组件使用传递的内容,但是不允许它们修改时,就可以使用readonly了;
3.3_Reactive判断的API
isProxy
: 检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
- 检查对象是否是由 reactive创建的响应式代理:
- 如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
isReadonly
: 检查对象是否是由 readonly 创建的只读代理。
toRaw
: 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
: 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
: 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
4_ref
reactive API对传入的类型是有要求的,必须传入的是一个对象或者数组类型。如果传入一个基本数据类型(String、Number、Boolean)会报一个警告;
所以Vue3给提供了另外一个API:ref API
- ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
- 它内部的值是在ref的 value 属性中被维护的;
这里有两个注意事项:
- 在模板中引入ref的值时,Vue会自动帮助进行解包操作,所以并不需要在模板中通过 ref.value 的方式来使用;
- 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,依然需要使用 ref.value的方式;
4.1_toRefs
使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的:
const state = reactive({
name = "hhh",
age = 18
})
const {
name , age} = state
那么有没有办法让解构出来的属性是响应式的呢?
- Vue为提供了一个
toRefs
的函数,可以将reactive返回的对象中的属性都转成ref; - 那么再次进行结构出来的 name 和 age 本身都是 ref的;
const {
name,age } = toRefs(state)
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;
toRef
只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
const name = toRef(state,'name')
4.2_ref其他的API
unref
- 如果想要获取一个ref引用中的value,那么也可以通过unref方法:
- 如果参数是一个 ref,则返回内部值,否则返回参数本身;
- 这是 val = isRef(val) ? val.value : val 的语法糖函数;
isRef : 判断值是否是一个ref对象。
shallowRef : 创建一个浅层的ref对象;
triggerRef : 手动触发和 shallowRef 相关联的副作
5_computed
在Options API了解过计算属性computed,当某些属性是依赖其他状态时,可以使用计算属性来处理
- 在前面的Options API中,是使用computed选项来完成的;
- 在Composition API中,可以在 setup 函数中使用 computed 方法来编写一个计算属性;
如何使用computed呢?
- 方式一:接收一个
getter
函数,并为 getter 函数返回的值,返回一个不变的 ref 对象; - 方式二:接收一个具有
get
和set
的对象,返回一个可变的(可读写)ref 对象;
<template>
<h2>{
{
fullname }}</h2>
<button @click="setFullname">设置fullname</button>
<h2>{
{
scoreLevel }}</h2>
</template>
<script>
import {
reactive, computed, ref } from 'vue'
export default {
setup() {
// 1.定义数据
const names = reactive({
firstName: "kobe",
lastName: "bryant"
})
// 原始的
// const fullname = computed(() => {
// return names.firstName + " " + names.lastName
// })
const fullname = computed({
set: function(newValue) {
const tempNames = newValue.split(" ")
names.firstName = tempNames[0]
names.lastName = tempNames[1]
},
get: function() {
return names.firstName + " " + names.lastName
}
})
console.log(fullname)
function setFullname() {
fullname.value = "coder hhh"
console.log(names)
}
// 2.定义score
const score = ref(89)
const scoreLevel = computed(() => {
return score.value >= 60 ? "及格": "不及格"
})
return {
names,
fullname,
setFullname,
scoreLevel
}
}
}
</script>
6_setup中使用ref
在setup中使用ref获取元素或者组件,只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
<template>
<!-- 1.获取元素 -->
<h2 ref="titleRef">我是标题</h2>
<button ref="btnRef">按钮</button>
<button @click="getElements">获取元素</button>
</template>
<script>
import {
ref} from 'vue'
export default {
setup() {
const titleRef = ref()
const btnRef = ref()
function getElements() {
console.log(titleRef.value)
}
return {
titleRef,
btnRef,
getElements
}
}
}
</script>
7_生命周期钩子
setup 可以用来替代 data 、 methods 、 computed 等等这些选项,也可以替代 生命周期钩子。
setup中使用生命周期函数,直接导入的 onX
函数注册生命周期钩子;
8_Provide/Inject使用
8.1_Provide
Composition API也可以替代之前的 Provide 和 Inject 的选项。
provide来提供数据。通过 provide
方法来定义每个 Property;
可以传入两个参数:
- name:提供的属性名称;
- value:提供的属性值;
8.2_Inject函数
在 后代组件 中可以通过 inject
来注入需要的属性和对应的值:
传入两个参数:
- 要 inject 的 property 的 name;
- 默认值;
8.3_数据的响应式
为了增加 provide 值和 inject 值之间的响应性,可以在 provide 值时使用 ref 和 reactive。
父组件
<template>
<div>AppContent: {
{
name }}</div>
<button @click="name = 'kobe'">app btn</button>
<show-info></show-info>
</template>
<script>
import {
provide, ref } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
const name = ref("hhh")
provide("name", name)
provide("age", 18)
return {
name
}
}
}
</script>
子组件
<template>
<div>ShowInfo: {
{
name }}+{
{
age }}+{
{
height }} </div>
</template>
<script>
import {
inject } from 'vue'
export default {
// inject的options api注入, 那么依然需要手动来解包
// inject: ["name", "age"],
setup() {
const name = inject("name")
const age = inject("age")
const height = inject("height", 1.88)
return {
name,
age,
height
}
}
}
</script>
9_watch/watchEffect
9.1_侦听数据的变化
在Options API中,可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作。
在Composition API中,可以使用watchEffect和watch来完成响应式数据的侦听;
watchEffect
:用于自动收集响应式数据的依赖;watch
:需要手动指定侦听的数据源;
9.2_Watch的使用
watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并且执行其回调函数;
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
如果想进行深层的侦听,需要设置 deep 为true, 也可以传入 immediate 立即执行;
<template>
<button @click="message='hello message'">修改message的值</button>
</template>
<script setup>
import {
ref,watch } from 'vue'
// 新值
const message = ref("hhh new message")
// 侦听
watch(message , (newValue , oldValue)=>{
console.log(newValue,oldValue)
},{
// immediate立即执行
immediate:true,
// deep深层侦听启动
deep:true
})
return{
message,
btn
}
</script>
侦听器还可以使用数组同时侦听多个源
<template>
<button @click="message = 'hello message'">修改message的值</button>
<button @click="btn = 'hello btn'">修改btn的值</button>
</template>
<script setup>
import {
ref, watch } from 'vue'
// 新值
const message = ref("hhh new message")
const btn = ref("hello new bth")
// 侦听单个值
watch(message, (newValue, oldValue) => {
console.log(newValue, oldValue)
}, {
// immediate立即执行
immediate: true,
// deep深层侦听启动
deep: true
})
// 侦听多个值
watch([message, btn], (newValue, oldValue) => {
console.log(newValue, oldValue)
})
return{
message,
btn
}
</script>
9.3_watchEffect
当侦听到某些响应式数据变化时,希望执行某些操作,这个时候可以使用 watchEffect。
一个案例:
- 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
- 其次,只有watch的依赖发生变化时,watchEffect传入的函数才会再次执行;
watchEffect的停止侦听
如果某些情况下,希望停止侦听,这时可获取watchEffect的返回值函数,调用该函数即可。
<template>
<div>
<h2>当前计数: {
{
counter }}</h2>
<button @click="counter++">+1</button>
<button @click="name = 'kobe'">修改name</button>
</div>
</template>
<script>
import {
watchEffect,ref } from 'vue'
export default {
setup() {
const counter = ref(0)
const name = ref("hhh")
// 1.watchEffect传入的函数默认会直接被执行
// 2.在执行的过程中, 会自动的收集依赖(依赖哪些响应式的数据)
const stopWatch = watchEffect(() => {
console.log("-------", counter.value, name.value)
// 判断counter.value > 10
if (counter.value >= 10) {
stopWatch()
}
})
return {
counter,
name
}
}
}
</script>
10_抽取hook
以之前的计数器案例代码为例
<template>
<h2>About计数: {
{
counter }}</h2>
<button @click="increment">+1</button>
<button @clcik="decrement">-1</button>
</template>
<script>
import {
ref } from 'vue'
export default {
setup() {
const counter = ref(0)
function increment() {
counter.value++
}
function decrement() {
counter.value--
}
return {
counter,
increment,
decrement
}
}
}
</script>
假设有多个页面都用到计数器scrip的逻辑代码,那么每个页面都写一遍这个代码的话,就会重复很多遍,效率很低。因此,打算抽取出script的逻辑代码,作为一个单独的js文件,放入hooks文件夹中,然后引入到各个需要的页面中。
抽取代码如下,文件路径/hooks/useCounter.js
import {
ref } from 'vue'
export default function useCounter() {
const counter = ref(0)
function increment() {
counter.value++
}
function decrement() {
counter.value--
}
return {
counter,
increment,
decrement
}
}
比如这个页面调用计数器逻辑代码
<template>
<h2>Home计数: {
{
counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</template>
<script>
// 引入
import useCounter from '../hooks/useCounter'
export default {
setup() {
// 使用
const {
counter, increment, decrement } = useCounter()
return {
counter,
increment,
decrement
}
}
}
</script>
11_script setup语法糖
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,当同时使用 SFC 与组合式 API 时则推荐该语法。
- 更少的样板内容,更简洁的代码;
- 能够使用纯 Typescript 声明 prop 和抛出事件;
- 更好的运行时性能 ;
- 更好的 IDE 类型推断性能 ;
里面的代码会被编译成组件 setup() 函数的内容:
- 这意味着与普通的
<script>
只在组件被首次引入的时候执行一次不同; <script setup>
中的代码会在每次组件实例被创建的时候执行。
11.1_顶层的绑定会被暴露给模板
当使用 <script setup>
的时候,任何声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用。
注意,响应式数据需要通过ref、reactive来创建。
<script setup>
范围里的值也能被直接作为自定义组件的标签名使用
11.2_defineProps() 和 defineEmits() 和defineExpose()
为了在声明 props 和 emits 选项时获得完整的类型推断支持,可以使用 defineProps 和 defineEmits API,它们将自动地在 <script setup>
中可用
使用 <script setup>
的组件是默认关闭的。 通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定;
通过 defineExpose 编译器宏来显式指定在 <script setup>
组件中要暴露出去的 property:
<template>
<div>ShowInfo: {
{
name }}+{
{
age }}</div>
<button @click="showInfoBtnClick">showInfoButton</button>
</template>
<script setup>
// 定义props
const props = defineProps({
name: {
type: String,
default: "默认值"
},
age: {
type: Number,
default: 0
}
})
// 绑定函数, 并且发出事件
const emits = defineEmits(["infoBtnClick"])
function showInfoBtnClick() {
emits("infoBtnClick", "showInfo内部发生了点击")
}
// 定义foo的函数,暴露出去
function foo() {
console.log("foo function")
}
defineExpose({
foo
})
</script>