Options API 的弊端

在 Vue2 中,编写组件的方式是 Options Api

  • Options API 的一大特点就是在对应的属性中编写对应的功能模块
  • 比如 data 定义数据、 methods 中定义方法、 computed 中定义计算属性、 watch 中监听属性改变,也包括生命周期钩子

这种代码有一个很大的弊端:

  • 实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中
  • 组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
  • 组件的代码是难以阅读和理解

Composition API 的思想就是: 将同一个逻辑关注点相关的代码收集在一起

认识 Composition API

Vue2 是面向对象编程,

Vue3 是函数式编程

所有的代码都写在 setup 函数

代码演示:

App.vue

<template>
<div class="app">
<!-- template中ref对象自动解包 -->
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>

<script>
import { ref } from 'vue'
import useCounter from './hooks/useCounter'

export default {
setup() {
// 第1种写法:定义counter的内容
// 默认定义的数据都不是响应式数据
// 使用 ref() 包裹就变成响应数据
// let counter = ref(100)
// const increment = () => {
// counter.value++
// console.log(counter.value)
// }
// const decrement = () => {
// counter.value--
// }

// 第2种写法:将代码逻辑抽取函数到文件,方便复用 --> 函数式思想
// const { counter, increment, decrement } = useCounter()

return {
// 第3种写法:更加简便的写法
...useCounter()
}
}
}
</script>

<style>
</style>

useCounter.vue

import { ref } from 'vue'

export default function useCounter() {
// 默认定义的数据都不是响应式数据
// 使用 ref() 包裹就变成响应数据
let counter = ref(100)
const increment = () => {
counter.value++
console.log(counter.value)
}
const decrement = () => {
counter.value--
}

return { counter, increment, decrement }
}

setup 函数

参数

它主要有两个参数

  • 第一个参数: props
  • 第二个参数: context

props 就是父组件传递过来的属性会被放到 props 对象中,在 setup 中如果需要使用,那么就可以直接通过 props 参数获取

  • 对于定义 props 的类型,还是和之前的规则是一样的,在 props 选项中定义
  • 并且在 template 中依然是可以正常去使用 props 中的属性,比如 message
  • 在 setup 函数中想要使用 props,那么不可以通过 this 去获取
  • 因为 props 有直接作为参数传递到 setup 函数中,所以可以直接通过参数来使用

context,也可以称之为是 SetupContext,它里面包含三个属性

  • attrs:所有的非 prop 的 attribute
  • slots:父组件传递过来的插槽
  • emit:当组件内部需要发出事件时会用到 emit

返回值

setup 既然是一个函数,那么它也可以有返回值

  • 返回值可以在模板 template 中被使用
  • 可以通过 setup 的返回值来替代 data 选项
  • 可以返回一个执行函数来代替在 methods 中定义的方法

注意:
上面定义的变量 counter 并不是响应式的
默认情况下, Vue 并不会跟踪它的变化,来引起界面的响应式操作

响应式 API

reactive

想为在 setup 中定义的数据提供响应式的特性,那么可以使用 reactive 的函数

  • 因为当使用 reactive 函数处理我们的数据之后,数据再次被使用时就会进行依赖收集
  • 数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)
  • 事实上,编写的 data 选项,也是在内部交给了 reactive 函数将其编程响应式对象的
const state =reactive({
name: "fredo",
counter: 100
})

Ref

reactive API 对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型

  • 如果我们传入一个基本数据类型(String、 Number、 Boolean)会报一个警告:
value cannote be made reactive: Hello World

对于基础数据类型可以使用 ref

  • ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref 名称的来源
  • 它内部的值是在 ref 的 value 属性中被维护的
const message = ref("Hello World");

注意:

  • 模板中引入 ref 的值时, Vue 会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref. value 的方式来使用
  • 但是在 setup 函数内部,它依然是一个 ref 引用,所以对其进行操作时,我们依然需要使用 ref. value 的方式, 手动解包

computed

某些属性是依赖其他状态时,可以使用计算属性来处理

  • Vue2 是在 Options API 中,使用 computed 选项来完成的
  • Vue3 是在 Composition API 中,在 setup 函数中使用 computed 方法来编写一个计算属性

使用方式

  • 方式一:接收一个 getter 函数,并为 getter 函数返回的值,返回一个不变的 ref 对象
  • 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写) ref 对象
// 方式一:
const fullName = computed(() => {
return firstName.value + " " + lastName.value;
})
// 方式二:
const fullName = computed(() => {
get: ()=>{
return firstName.value + " " + lastName.value;
},
set: newValue => {
const names = newValue.split(" ");
firstName.value = names[0];
lastName.value = names[1];
}
})

在 setup 中如何使用 ref 获取元素或者组件

  • 只需要定义一个 ref 对象,绑定到元素或者组件的 ref 属性上即可

生命周期函数

setup 可以用来替代 data 、 methods 、 computed 等等这些选项,也可以替代生命周期钩子

  • 可以使用直接导入的 onX 函数注册生命周期钩子
onMounted(()=>{
console.log("onMounted")
})

onUpdated(()=>{
console.log("onUpdated")
})

onUnmounted(()=>{
console.log("onUnmounted")
})

Provide 和 Inject

Composition API 也可以替代之前的 Provide 和 Inject 的选项

通过 provide 来提供数据

  • provide 方法来定义每个 Property
  • provide 可以传入两个参数
  • name:提供的属性名称
  • value:提供的属性值
let counter = 100
let info ={
name: "fredo",
age: 20
}

provide("counter", counter)
provide("info", info)

通过 inject 来注入需要的属性和对应的值

  • 可以传入两个参数
  • inject 的 property 的 name
  • 默认值
const counter = inject("counter")
const info = inject("info")

为了增加 provide 值和 inject 值之间的响应性,还可以在 provide 值时使用 ref 和 reactive

let counter = ref(100)
let info ={
name: "fredo",
age: 20
}

provide("counter", counter)
provide("info", info)

watch

在 Options API 中,可以通过 watch 选项来侦听 data 或者 props 的数据变化,当数据变化时执行某一些操作

在 Composition API 中,我们可以使用 watchEffect 和 watch 来完成响应式数据的侦听

  • watchEffect: 用于自动收集响应式数据的依赖
  • watch: 需要手动指定侦听的数据源

使用 watch

watch 的 API 完全等同于组件 watch 选项的 Property

  • watch 需要侦听特定的数据源,并且执行其回调函数
  • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调
const name = ref("fredo")

watch(name, (newValue, oldValue) => {
console.log(newValue, oldValue);
})

const changeName = () => {
name.value = "fu-jw";
}
  • 使用数组同时侦听多个源
const name = ref("fredo");
const age = ref(20);

const changeName = ()=> {
name.value = "fjw";
}

watch([name, age], (newValue, oldValue) => {
console.log(newValue, oldValue);
})

深层的侦听

依然需要设置 deep 为 true

  • 也可以传入 immediate 立即执行
const info reactive({
name: "fredo",
age: 20,
friend: {
name: "fjw"
}
})

watch(info, (newValue, oldValue) => {
console.log(newValue, oldValue);
}, {
immediate: true,
deep: true
})

watchEffect

当侦听到某些响应式数据变化时,希望执行某些操作,这个时候可以使用 watchEffect

如下案例:

  • 首先, watchEffect 传入的函数会被立即执行一次,并且在执行的过程中会收集依赖
  • 其次,只有收集的依赖发生变化时, watchEffect 传入的函数才会再次执行
const name = ref("fredo");
const age = ref(20);

watchEffect(() => {
console.log("watchEffect~~", name.value, age.value);
})

停止侦听

  • 如果在发生某些情况下,希望停止侦听,这个时候可以获取 watchEffect 的返回值函数,调用该函数即可
  • 比如在上面的案例中,age 达到 20 的时候就停止侦听
const stopWatch = watchEffect(() => {
console.log("watchEffect~~", name.value, age.value);
});

const changeAge = () => {
age.value++;
if(age.value > 20){
stopWatch();
}
};

script setup

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,当同时使用 SFC 与组合式 API 时则推荐该语法

  • 更少的样板内容,更简洁的代码
  • 能够使用纯 Typescript 声明 prop 和抛出事件
  • 更好的运行时性能
  • 更好的 IDE 类型推断性能
<script setup>
console.log("Hello World")
</script>

里面的代码会被编译成组件 setup () 函数的内容

  • 这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同
  • <script setup> 中的代码会在每次组件实例被创建的时候执行

当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用

<script setup>
const message = "Hello World"
function btnClick() {
console.log("btnClick")
}
</script>

<template>
<h2>message: {{ message }}</h2>
<button @click="btnClick">按钮</button>
</template>

导入的组件直接使用

  • <script setup> 范围里的值也能被直接作为自定义组件的标签名使用
<script setup>
import ShowInfo from './ShowInfo.vue'
</script>

<template>
<show-info></show-info>
</template>

使用 <script setup> 的组件是默认关闭

  • 通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定
  • 通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的 property

props 和 emits

为了在声明 props 和 emits 选项时获得完整的类型推断支持,我们可以使用 defineProps 和 defineEmits API,它们将自动地在 <script setup> 中可用

<script setup>
const props = defineProps({
name: {
type: String,
default: ""
},
age: {
type: Number,
default: 0
}
})

const emit = defineEmits(["changeAge"])
function changeAge() {
emit("changeAge", 200)
}
</script>
<template>
<h2>ShowInfo: {{ name }}-{{ age }}</h2>
<button @click="changeAge">修改age</button>
</template>