vue3 实践之 Composition API(ref、reactive、toRef、toRefs、自定义 hooks 等)
Vue3 与 Vue2 最大的不同在于新增了 Composition API,Composition API 官方文档:https://cn.vuejs.org/guide/extras/composition-api-faq.html
setup
setup 函数是处于生命周期函数 beforeCreate 之前的函数,新的 option、所有的组合式 API 函数都在此使用,并且只在初始化时执行一次。返回的属性、方法可在 template 直接使用。
- beforeCreate 之前执行一次,此时组件对象还没有创建
- setup中,this 是 undefined,不能通过 this 访问 data/computed/methods/props 属性、方法
- 所有的 Composition API 相关函数中都不可以访问
- setup 返回值
- 一般返回一个对象,为模板提供数据,template 可直接使用对象中的属性、方法
- 返回的属性与 data 函数返回对象的属性合并
- 返回的方法与 methods 中的方法合并
- 如果有重名,setup 优先
注意:
一般 setup、methods、data 不要混合使用,因为 methods 中可以访问的 setup 提供的属性和方法,但在 setup 方法中不能访问 data 和 methods
- setup 返回参数
- setup(props, context) / setup(props, {attrs, slots, emit})
- props: 包含 props 配置声明且传入的所有属性的对象
- attrs: 包含没有在 props 配置中声明的属性的对象,相当于 this.$attrs
- slots: 包含所有传入的插槽内容对象,相当于 this.$slots
- emit: 用来颁发自定义事件的函数,相当于 this.$emit
props 与 attrs 区别
- props 要声明才能使用,attrs 不用先声明
- porps 声明过的属性,attrs 里不会再出现
- props 不包含事件,attrs 包含事件
- props 支持 string 以外的数据类型,attrs 只有 string 类型
ref
作用:定义一个数据的响应式
语法:const name = ref(initValue)
注意:模板语法中可以使用 name,js 中操作数据需要 name.value 语法
setup() {
const count = ref(0);
function addCount() {
count.value++
}
return { count, addCount }
}
reactive
作用:定义多个数据的响应式(对象)
语法:const user = reactive(obj) 接收一个普通对象,返回一个该对象响应式代理对象
响应式转换是 “深层的”,它会影响对象内部的所有嵌套属性,内部是基于 ES6 的 Proxy 实现,通过代理对象操作对象内部数据。
const user = {
name: '张三',
age: 18
}
const proxyUser = new Proxy(user, {
get(target, prop) {
console.log('劫持 get', prop);
return Reflect.get(target, prop);
},
set(target, prop, val) {
console.log('劫持 set', prop);
return Reflect.set(target, prop, val);
},
deleteProperty(target, prop) {
console.log('劫持 delete', prop);
return Reflect.deleteProperty(target, prop);
}
});
console.log(proxyUser === user, user);
console.log(proxyUser.name, proxyUser.age);
proxyUser.name = '李四';
proxyUser.age = 30;
console.log(user, proxyUser)
Vue2 与 Vue3 响应式比较
- Vue2 响应式
对象:通过 defineProperty 对对象的已有属性值的读取和修改进行劫持
数组:通过重写数组、更新数组等一系列更新元素的方法来实现元素修改的支持
问题:
1)对象直接新添加属性和删除已有属性。界面不会自动更新;2)数组直接通过下标替换元素或更新元素 length,界面不会自动更新。
- Vue3 响应式
通过 Proxy(代理):拦截 data 任意属性的任意(13种)操作,包括属性值的读写、属性的添加、属性的删除等
通过 Reflect(反射):动态地对被代理对象的相应属性进行特定的操作
Computed 与 watch、watchEffect
computed 函数:可以只有 getter,也可以同时有 getter 和 setter
watch 函数:监视指定的一个或多个响应式数据,一旦变化自动执行回调方法。immediate: true 初始执行,deep: true 深度监视。
watchEffect 函数:不能直接指定要监视的数据,回调函数中使用了哪些响应式数据就监听哪些响应式数据。默认初始时就会执行一次,收集需要监听的数据:
watch 与 watchEffect 区别:
- watchEffect 不需要手动传入依赖
- watchEffect 每次初始化时会执行一次回调函数来自动收集依赖
- watchEffect 无法获取到原始值,只能得到变化后的值
自定义 hooks
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition () {
const x = ref(-1)
const y = ref(-2)
const updatePosition = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
document.addEventListener('click', updatePosition)
})
onUnmounted(() => {
document.addEventListener('click', updatePosition)
})
return { x, y }
}
toRef、toRefs
把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref
解决问题:reactive 对象取出的所有属性值都是非响应式的
<template>
<div>
<p>名字:{{name}}</p>
<p>年龄:{{age}}</p>
<p>{{age2}}</p>
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, toRef, watch } from 'vue'
export default defineComponent({
setup () {
const user = reactive({
name: '张三',
age: 100
})
setTimeout(() => {
user.age++
}, 1000)
watch(user, () => {
console.log(user, 'change')
}, {
immediate: true,
deep: true
})
function update () {
const name = toRef(user, 'name')
name.value += 'update'
}
const age2 = toRef(user, 'age')
console.log(toRefs(user), user)
return {
...toRefs(user),
age2,
update
}
}
})
</script>
这里的 age2 ,还有展开后的 user 属性,如果不用 toRef、toRefs 转换一下,就不会保持响应式。
ref
利用 ref 函数可以获取组件中的标签元素
<template>
<input type="text" ref="inputRef" />
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
setup () {
const inputRef = ref<HTMLElement | null>(null)
onMounted(() => {
inputRef.value && inputRef.value.focus()
})
return { inputRef }
}
})
</script>
shallowReactive 与 shallowRef
shallowReactive:只处理对象最外层属性的响应式
shallowRef:只处理 value 的响应式,不进行对象的 reactive 处理
使用场景:
- 多数场景使用 ref、reactive
- 如果一个对象数据,结构比较深,但变化时只是外层属性变化,则可以使用 shallowReactive
- 如果一个对象数据后面会被新产生的对象替换,则可以使用 shallowRef
readonly 与 shallowReadonly
readonly
- 尝试只读数据
- 获取一个对象或 ref 并返回原始代理的只读代理
- 只读代理是深层的,访问的任何嵌套 property 也是只读的
shallowReadonly
- 浅只读数据
- 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
toRaw 与 markRaw
toRaw
- 返回由 reactive 或 readonly 方法转换成响应式代理的普通对象
- 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新
markRaw 标记一个对象,使其永远不会转换为代理,而是返回对象本身
- 有些值不应被设置为响应式的,例如复杂的第三方类实例或 vue 组件对象
- 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能
<template>
<h3>用户:{{user}}</h3>
<button @click="testToRaw">测试toRaw</button>
<button @click="testMarkRaw">测试markRaw</button>
</template>
<script lang="ts">
import { defineComponent, markRaw, reactive, toRaw } from 'vue'
interface UserInfo {
name: string
age: number
like?: string[]
}
export default defineComponent({
setup () {
const user = reactive<UserInfo>({
name: '张三',
age: 18
})
const testToRaw = () => {
// 把代理对象变成了普通对象,数据变化,界面不变化
const newUser = toRaw(user)
newUser.name = '李四'
console.log(newUser, user)
}
const testMarkRaw = () => {
user.like = ['看书', '看电视']
console.log(user)
const like = ['看书', '看电视']
// markRaw 标记的对象数据,以后都不能成为代理对象
user.like = markRaw(like)
setTimeout(() => {
if (user.like) {
user.name = '李四'
user.like[0] = '跑步'
console.log('延时执行')
}
}, 1000)
}
return { user, testToRaw, testMarkRaw }
}
})
</script>
unRef
如果参数是 ref 属性,则返回它的值,否则返回本身
const refValue = ref('ref value')
const reactiveValue = reactive({
name: '张三',
age: 18
})
console.log(unref(refValue))
console.log(unref(reactiveValue))
customRef
customRef 用于创建一个自定义的 ref 对象,可以显式地控制其依赖跟踪和触发响应
customRef 方法接收两个参数,分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个带有 get 和 set 属性的对象
customRef 实现防抖节流
import { customRef } from 'vue'
// 防抖
export function useDebouncedRef<T> (value: T, delay = 300) {
let timeoutId: number
return customRef((track, trigger) => {
return {
get () {
track()
return value
},
set (newValue: T) {
// 清除定时器
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
value = newValue
// 更新
trigger()
}, delay)
}
}
})
}
// 节流
export function useThrottleRef<T> (value: T, delay = 300) {
let flag = true
return customRef((track, trigger) => {
return {
get () {
track()
return value
},
set (newValue: T) {
if (flag) {
flag = false
const timer = setTimeout(() => {
value = newValue
trigger()
flag = true
}, delay)
}
}
}
})
}
使用案例
<template>
<p>{{keyword}}</p>
<input type="text" v-model="keyword" />
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
import { useDebouncedRef, useThrottleRef } from '../hooks/useHooks'
export default defineComponent({
setup () {
const keyword = useDebouncedRef('vue.js', 1000)
watch(
keyword,
(value) => {
console.log(value)
}
)
return {
keyword
}
}
})
</script>
provide 与 inject
provide 和 inject 是成对出现的,在主组件通过 provide 提供数据和方法,在子组件或者孙辈等下级组件中通过 inject 调用主组件提供的数据和方法
// provideDemo.vue
<template>
<div>
{{message}}
<button @click="clickFn">set</button>
<hr />
<SonComponent />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, provide } from 'vue'
import SonComponent from './SonComponent.vue'
export default defineComponent({
components: {
SonComponent
},
setup () {
const message = ref<string>('')
const name = ref<string>('')
const grandSonName = ref<string>('')
// 提供数据
provide('name', name)
provide('grandSonName', grandSonName)
// 提供方法
provide('callBack', () => {
message.value = '更新消息'
name.value = '更新姓名'
})
const clickFn = () => {
message.value = 'provide 更新消息'
name.value = 'provide 更新姓名'
grandSonName.value = 'provide 更新孙子组件姓名'
}
return { clickFn, message }
}
})
</script>
// SonComponent.vue
<template>
<div>
儿子:{{name}}
<hr />
<GrandSon />
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
import GrandSon from './GrandSon.vue'
export default defineComponent({
components: {
GrandSon
},
setup () {
// 注入操作
const name = inject('name')
return { name }
}
})
</script>
// GrandSon.vue
<template>
<div>
孙子:{{grandSonName}} <button @click="setName">设置Name</button>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
export default defineComponent({
setup () {
const setName = inject('callBack')
const grandSonName = inject('grandSonName')
return { setName, grandSonName }
}
})
</script>
响应式数据的判断
- isRef:检查一个值是否为一个 ref 对象
- isReactive:检查一个对象是否是由 reactive 方法创建的响应式代理
- isReadonly:检查一个对象是否是由 readonly 方法创建的只读代理
- isProxy:检查一个对象是 否是由 reactive 或者 readonly 方法创建的代理
console.log(isRef(ref({}))) // true
console.log(isReactive(reactive({}))) // true
console.log(isReadonly(readonly({}))) // true
console.log(isProxy(readonly({}))) // true
console.log(isProxy(reactive({}))) // true