🧊 Vue 3 的组合 API 编程
# 常用 API
# setup
setup
是 Vue 3 中的新的一个配置项,值为一个函数。是所有 Composition 组合 API 「表演的舞台」。
组件中所有到的:数据、方法,都要配置在
setup
中。setup
函数中的两种返回值:- ⭐️ 若最后返回一个对象,则对象中的属性、方法,在模版中可以直接使用。
- 若返回一个渲染函数,则可以返回自定义的渲染内容(了解)。
🌰 例子:在 setup
中返回一个对象:
setup() {
// 数据
let name = 'Simon'
let age = '3'
// 方法
function sayHello() {
alert(`hello, this is ${name}, age:${age}`)
}
return {
name,
age,
sayHello
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- ⚠️ 注意:
- 尽量不要与 Vue 2.x 中的配置混用:
- Vue 2.x 的配置(
data
、methods
、computed
… )中可以访问到setup
中的属性、方法。 - 但是在
setup
配置中不能访问 Vue 2.x 的配置(data
、methods
、computed
… )。 - 当
setup
中的属性、方法与 Vue 2.x 的配置有重名时,以setup
优先。
- Vue 2.x 的配置(
setup
不能是一个async
函数,因为返回值不再是return
对象,而是一个被async
包裹的对象,而是promise
,此时模版无法使用return
中的属性(后期可以返回一个Promise
的实例,但是需要Suspense
和异步组件的配合 🔗 Suspense 组件)
- 尽量不要与 Vue 2.x 中的配置混用:
# ref
函数
- 在之前使用的
ref
,作用是给原生的 DOM 标签添加标识,替代id
的作用。- 在上述
setup
的例子中定义的数据不是一个响应式的数据,发生变化 Vue 无法监测。
ref
函数作用:定义一个响应式的数据。语法:
const xxx = ref(initValue)
- 创建一个
ReflImpl
:reference
+implement
包含响应式数据的引用ref
对象(引用实现对象)。 - 在 JS 中操作数据:
xxx.value
- 在模版中读取数据:直接
(自动解析
ReflImpl
的value
值)
- 创建一个
注意:
- 参数可以接收的数据有:基本数据类型、对象类型。
- 基本类型的数据:响应式依然是直接靠
Object.defineProperty()
的get
与set
(数据劫持)完成的。 - 对象类型的数据:借助 Vue 3 中的
reactive
函数。
# reactive
函数
reactive
函数的作用:定义一个对象类型的响应式数据。(对于基本数据类型只能使用ref
,基本数据类型放在对象中再使用reactive
函数即可 )语法:
const 代理对象 = reactive(源对象)
,参数接收一个对象(或数组),返回一个代理对象(Proxy
的实例对象,代理 Proxy 对象)使用
reactive
函数定义的响应式数据是「深层次的」。reactive
函数内部基于 ES6 的Proxy
函数实现,通过代理对象操作源对象的内部数据。
# 对比 ref
与 reactive
函数
从定义数据类型角度对比:
ref
用来定义基本数据类型。reactive
用来定义对象(包括数组)类型。
reactive
可以用来定义基本数据类型,封装在一个data
对象中即可。从原理角度对比:
ref
通过Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。reactive
通过使用Proxy
(数据劫持)与Reflect
(操作源对象内部的数据)结合实现响应式。
从实用角度对比:
ref
定义的数据,操作数据需要使用.value
读取,在模版中可以直接读取。reactive
定义的数据,操作和读取数据可以直接读取。
# setup
的注意点
setup
执行的时机:- 在
beforeCreate
之前执行一次,this
是undefined
。
- 在
setup
的参数:props
:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。context
:上下文对象,包含以下内容:attrs
: 值为对象,包含:组件外部传递过来,但没有在props
配置中声明的属性,相当于this.$attrs
。slots
:收到的插槽内容,相当于this.$slots
。emit
:分发自定义事件的函数,相当于this.$emit
。注意,自定义事件在要触发的组件中,与props
相同,使用emits
先声明,否则会警告。
# 响应式原理
# Vue 2 中的响应式
实现原理(对于对象类型和数组类型的数据):
对象类型:通过
Object.defineProperty()
对属性的读取、修改进行拦截(数据劫持)。数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
Object.defineProperty(data, 'count', { get () {}, set () {} })
1
2
3
4
存在问题:
(可以通过Vue.set
(this.$set
) 解决)- 新增属性、删除属性,界面不会更新。
- 直接通过下标修改数组,界面不会自动更新。
# Vue3 的响应式
- 实现原理:
- 通过 Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等。
- 通过 Reflect(反射):映射对源对象的属性进行操作。
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
🔗 相关链接 MDN:
Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
# 计算属性 computed
函数
Vue 3 中的计算属性 computed
函数的使用方法与 Vue 2 中的 computed
一致,在 Vue 3 中计算属性放入了 setup
中。可以将定义在 setup
中的数据设置为计算属性。
🌰 计算属性的简写形式:
setup() {
// 数据属性
let person = reactive({
firstName: 'Simon',
lastName: 'Luo',
})
// 计算属性
person.fullName = computed(() => {
return person.firstName + '-' + person.lastName
})
return {
person,
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 上述简写的形式若将计算后的属性修改会提示警告,因为简写的计算属性是只读的。
🌰 计算属性的完整形式(包括 get()
和 set()
):
person.fullName = computed({
get() {
return person.firstName + '-' + person.lastName
},
set(value) {
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
2
3
4
5
6
7
8
9
10
# 数据监视 watch
函数
与 computed
函数类似, watch
函数放在 setup
中使用,并且与 Vue 2 时的数据监视 watch
配置用法相同。
但是 Vue 3 中的数据监视,对于不同的使用情况有不同的结果:
- 监视普通的
ref
定义的一个响应式数据:
watch(sum, (newValue, oldValue)=>{
console.log('newValue', newValue)
console.log('oldValue', oldValue)
}, {immediate: true})
2
3
4
- 监视普通的
ref
定义的多个响应式数据:
watch([sum, msg], (newValue, oldValue)=>{
console.log('newValue', newValue)
console.log('oldValue', oldValue)
})
2
3
4
监视使用
ref
定义的数据可以完整获取newValue
、oldValue
。
注意
对于 ref
定义的响应式数据的一个注意点: ref
定义的普通数据的监视不能使用 .value
,不用使用 .value
是因为 ref
定义的数据生成引用对象, .value
直接获取的是数据。
而对于使用 ref
定义的响应式数据为一个对象时,此时 .value
的值为借用 reative
生成的 Proxy
对象,这时并不能监视到该层次的变化。可以使用加上 .value
或者开启深度监视解决。
- 监视
reactive
定义的响应式数据(以对象为例):
watch(person, (newValue, oldValue) => {
console.log('oldValue', oldValue) // 依旧是newValue
console.log('newValue', newValue)
}, {deep: false})
2
3
4
注意
监视使用
reactive
定义的响应式数据不能获取oldValue
;监视使用
reactive
定义的响应式数据,会强制开启深度监视(对于此事监视完整的一个对象而言)。
- 监视
reactive
定义的响应式数据的某一个属性或者某些属性,第一个参数需要为一个函数的返回值或者对象,监测多个属性需要使用数组包裹。
watch(() => person.age, (newValue, oldValue) => {
console.log('newValue', newValue)
}, {immediate: true, deep: false})
2
3
watch([() => person.age, () => person.name], (newValue, oldValue) => {
console.log('newValue', newValue)
}, {deep: false})
2
3
注意
- 当监视的为
reactive
定义的响应式数据的某一个属性为一个对象时,并且其中有需要深度监视的属性,此时需要开启深度监视。
watch(() => person.job, (newValue, oldValue) => {
console.log('oldValue', oldValue)
console.log('newValue', newValue)
}, {deep: true})
2
3
4
由于监视的是 reactive 定义的对象中的某个属性,所以 deep 配置有效
# watchEffect
函数
- 在
watch
函数中:既要指明监视的属性,也要指明监视的回调。 - 在
watchEffect
函数中:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。 - 与
computed
函数相似,在computed
函数中注重计算出来的值(回调函数的返回值),所以必须要返回一个值;但是watchEffect
注重的是过程(回调函数的函数体),所以不用写返回值。
🌰 watchEffect
函数的使用例子:
watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
console.log('watchEffect回调执行')
})
2
3
4
5
只要
watchEffect
指定的回调函数中用到的数据发生变化,则直接重新执行回调。
# 生命周期
- Vue 3 中可以继续使用 Vue 2 中的生命周期钩子,但有有两个被更名:
beforeDestroy
改名为beforeUnmount
destroyed
改名为unmounted
- Vue 3 也提供了 Composition API 形式的生命周期钩子,与 Vue 2 中钩子对应关系如下:
beforeCreate
===>setup()
created
=======>setup()
beforeMount
===>onBeforeMount
mounted
=======>onMounted
beforeUpdate
===>onBeforeUpdate
updated
=======>onUpdated
beforeUnmount
==>onBeforeUnmount
unmounted
=====>onUnmounted
# 自定义 hook 函数
自定义 hook 函数本质是一个函数,把 setup
函数中使用的 Composition API 进行了封装。
类似于 Vue2 中的 mixin。
自定义 hook 的优势:复用代码,让
setup
中的逻辑更清楚易懂。
🌰 例子:
- 在
src
目录下创建新的目录hook
存储hook
函数,创建文件usePoint
为例(一般命名开头为use
):
// 实现获取鼠标坐标
import {onBeforeUnmount, onMounted, reactive} from "vue";
export default function () {
// 实现的数据
let point = reactive({
x: 0,
y: 0
})
// 实现的方法
function savePoint(event) {
point.x = event.pageX
point.y = event.pageY
console.log(event.pageX, event.pageY)
}
// 实现的相关钩子
onMounted(() => {
window.addEventListener('click', savePoint)
})
onBeforeUnmount(() => {
window.removeEventListener('click', savePoint)
})
return point
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- 在要使用这个
hook
函数的组件,引入当作函数并使用:
import usePoint from "../hooks/usePoint";
setup() {
let point = usePoint()
return {
point
}
}
2
3
4
5
6
7
# toRef
函数
toRef
函数创建一个ref
对象,其value
值指向另一个对象中的某个属性。- 语法:
const name = toRef(person,'name')
/toRefs(person)
- 应用:要将响应式对象中的某个属性单独提供给外部使用时。
与
ref
函数不同,ref
函数会重新创建新的引用RefImpl
对象;toRef
创建的是引用原来的RefImpl
对象。
🌰:使用 toRefs()
批量处理 setup
中的某个对象:
setup(){
let person = reactive({
name: 'Simon',
age: 3
})
return {
...toRefs(person)
}
}
2
3
4
5
6
7
8
9
10
# 其他 API
# shallowReactive
原始 shallowRef
函数
浅层响应式数据的定义:
shallowReactive
:只处理对象最外层属性的响应式(浅响应式)。shallowRef
:只处理基本数据类型的响应式,不进行对象的响应式处理(对于使用这个函数定义的对象,不会借用reactive
处理对象为Proxy
对象)。
应用场景:
- 如果只有一个对象数据,由多层对象结构(结构较深层),但只需要实现外层属性的变化,使用
shallowReactive
函数定义。 - 如果只有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换,使用
shallowRef
函数定义。
- 如果只有一个对象数据,由多层对象结构(结构较深层),但只需要实现外层属性的变化,使用
# readonly
与 shallowReadonly
函数
- 只读属性相关的 API :(* 对于定义的响应式数据有效)
readonly
:让一个响应式数据变为只读的(深只读);shallowReadonly
:让一个响应式数据变为只读(浅只读);
- 应用场景:不希望响应式数据被修改时。(当收到来自另一个组件定义的响应式数据,在现组件不希望引入的数据被修改时)
# toRaw
与 markRaw
函数
生成与标记为原始普通数据:
toRaw
:- 作用:将一个由
reactive
(只能是)定义的响应式对象转为普通对象(原始)。 - 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作不会引起页面的变化。
- 作用:将一个由
markRaw
:- 用于某些数据 ** 不应该被赋值时自动设置为响应式数据的,** 例如复杂的第三方库数据。
- 当渲染具有不可变数据源的大列表时,跳过响应式转换会提高渲染性能。
# customRef
- 作用:创建一个自定义
ref
,并且对其依赖项跟踪和更新触发进行显示控制。
🌰 实现防抖效果(延迟显示):
点击查看
// 自定义一个ref
function myRef(value, delay) {
let timer
// console.log('myRef', value)
return customRef((track, trigger) => {
return {
// 类似计算属性get/set
get() {
console.log(`read from myRef, value: ${value}`)
track() // 通知Vue追踪value的变化(告诉Vue)
return value
},
set(newValue) {
console.log(`modify in myRef, after Modified Value: ${newValue}`)
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
trigger() // 通知Vue重新解析模版
}, delay)
},
}
})
}
let keyword = myRef('hello', 500) // 使用自定义ref
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# provide
与 inject
- 作用: 实现 **(祖与后代)跨级组件之间 ** 的通信。
- 套路:父组件有一个
provide
选项来提供数据,后代组件有一个inject
选项来开始使用这些数据。
🌰 例子:
- 在祖组件中的
setup
中provide
:
setup(){
......
let car = reactive({name:'奔驰',price:'40万'})
provide('car',car)
......
}
2
3
4
5
6
- 在后代组件中使用
inject
获取:
setup(props,context){
......
const car = inject('car')
return {car}
......
}
2
3
4
5
6
# 响应式数据判断
当数据逐渐增多或者获取从它处来的数据,未知该数据的响应式类型(是否为响应式),需要进行该数据的判断,使用下列函数判断:
isRef
:检查目标数据是否为ref
创建的引用对象;isReactive
:检查目标数据是否由reactive
创建的响应式代理;isReadonly
:检查目标数据是否为由readonly
创建的只读代理;isProxy
:检查一个对象是否由reactive
或者readonly
方法创建的代理。
# Composition API 的优势
# Options API 存在的问题
提示
使用传统 Options API (配置式 API)中,新增或者修改一个需求,就需要分别在 data
、 methods
、 computed
里修改 。
# Composition API 的优势
提示
⭐️ 在 Composition API (基于函数组合 API)中,可以整合在一块组织代码、函数(配合自定义钩子函数使用),让相关功能的代码更加有序的组织在一起。