Appearance
6.12 组合式Api
6.12.1 核心API
setup
reactive
返回一个对象的响应式代理
- 响应式代理与原始对象的关系
- 只有代理对象是响应式的,更改原始对象不会触发更新
reactive返回的是一个原始对象的代理,它和原始对象是不相等的(嵌套对象也适用)
js
const raw = {count: 0 }
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // truereactive()的局限性
- 仅对对象类型有效(
Object、Array、Map、Set),而对String、Number、Boolean这样的原始类型无效 - 因为Vue的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失
js
let state = reactive({ count: 0 })
// { count: 0 }失去响应,被{ count: 1 }覆盖
state = reactive({ count: 1 })
// n没有响应,不影响state
let n = state.count
n++
// count没有响应,state也没有响应
let { count } = state
// 不会影响原始的 state
count++ref
可以使用任何值类型的响应式,没有reactive()的上述限制和局限性
vue
<script>
import { ref } from 'vue'
export default {
// `setup` 是一个专门用于组合式 API 的特殊钩子函数
setup() {
const state = ref(0) // ref在顶层 { value: 0 }
function increment() {
state.value++
}
// 暴露 state 到模板和increment方法
return {
state, increment
}
}
}
</script>
<template>
<div @click="increment()">{{ state }}</div> <!-- ref在顶层的时候自动解包无需 .value -->
</template>- 解构或覆盖不会失去响应
js
const objectRef = ref({ count: 0 })
// 这是响应式的替换,仍然具有响应
objectRef.value = { count: 1 }
const obj = {
foo: ref(1),
bar: ref(2)
}
// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)
// 仍然是响应式的
const { foo, bar } = objref解包
- 当ref在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用
.value - 使用
<script setup>语法时,也会自动解包
vue
<script>
import { ref } from 'vue'
export default {
// `setup` 是一个专门用于组合式 API 的特殊钩子函数
setup() {
const object = { foo: ref(1) } // ref不在顶层
const { foo } = object // 通过将 foo 改成顶层属性> 来解决这个问题
// 暴露 state 到模板和increment方法
return {
object,foo
}
}
}
</script>
<template>
<div >{{ object.foo +1 }}</div> <!-- ref不在顶在层 foo是一个ref对象 返回[object Object]1 -->
<!-- 正确写法 -->
<div >{{ foo +1 }}</div>
<div >{{object.foo.value +1}}</div>
</template>reactive 与 ref 的比较
ref支持原始类型和对象,reactive只支持对象。(ref处理对象类型的时候,也是通过reactive实现的)reactive是proxy代理的对象数据,不需要.value访问- 对象层级较深,需要深度跟踪,那么推荐使用
reactive(性能更高) watch监听ref类型的数据时,需要设置深度监听,reactive不需要reactive不能替换整个对象,不能直接解构,需要使用toRef或toRefs进行解构
readonly
返回一个只读的响应式代理
js
const copy = readonly(original)
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!computed
computed()接收一个getter函数,返回值为一个计算属性 ref,其它特性同选项式Api中的计算属性
vue
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<span>{{ publishedBooksMessage }}</span> <!-- 自动解包无需 .value -->
</template>- 可写计算属性
vue
<script setup>
import { reactive, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.>split(' ')
}
})
</script>
<template>
<span>{{ fullName }}</span> <!-- 自动解包无需 .value -->
</template>watch
js
const state = ref(0)
function increment() {
state.value++
}
watch(state, (newValue, oldValue) => {
console.log(newValue, oldValue)
})- 侦听数据源类型 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
js
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})不能直接侦听响应式对象的属性值
js
const obj = reactive({ count: 0 })
// 错误:因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 正确写法:提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)- 深层侦听器
watch()会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++只有在返回不同的对象时,才会触发回调
js
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)deep选项,强制转成深层侦听器
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)停止侦听
js
const unwatch = watchEffect(() => {})
unwatch()回调的触发时机flushwatch的侦听器回调,都会在Vue组件更新之前被调用。这意味着你在侦听器回调中访问的DOM将是被Vue更新之前的状态,如果想在侦听器回调中能访问被Vue更新之后的DOM。就要使用flush属性
可选参数:
pre: 在组件更新之前异步调用,(onBeforeUpdate之前调用)post:在组件更新之后异步调用,(onBeforeUpdate之后调用,onUpdated之前)async:在组件更新之前异同步调用,(onBeforeUpdate之前调用)
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})执行顺序
watchSyncEffect → watchEffect → onBeforeUpdate → watchPostEffect → onUpdated
watchEffect
watch() 是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。
js
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})watch 与 watchEffect
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确watchEffect相当于watch设置了immediate:true
watchPostEffect
watchEffect() 使用 flush: 'post' 选项时的别名。
watchSyncEffect
watchEffect() 使用 flush: 'async' 选项时的别名
6.12.2 工具API
isRef
判断是否是 ref 对象
js
isRef(foo)unref
如果参数是 ref ,则返回内部值,否则返回参数本身,相当于val = isRef(val) ? val.value : val 是其语法糖
toRef与toRefs
toRef:基于响应式对象上的一个属性,创建一个对应的reftoRefs:基于响应式对象,创建一个对应的ref,对象的所有属性都会创建一个ref
vue
<script setup lang="ts">
import { reactive, toRefs,toRef } from "vue";
let data = reactive({
a: 1,
b: 2,
c: 3
});
let { a, b, c } = toRefs(data)
let s = toRef(data,'a') // 基于data对象的a属性创建一个响应式对象
console.log(a);
function increment() {
data.a++;
}
</script>
<template>
<div @click="increment">
{{ a }}--{{ s }}
</div>
</template>toValue
将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。 这可以在组合式函数中使用,用来规范化一个可以是值、ref 或 getter的参数。
主要用于在组合式函数中规范化参数
js
import type { MaybeRefOrGetter } from 'vue'
function useFeature(id: MaybeRefOrGetter<number>) {
watch(() => toValue(id), id => {
// 处理 id 变更
})
}
// 这个组合式函数支持以下的任意形式:
useFeature(1)
useFeature(ref(1))
useFeature(() => 1)isProxy
检查一个对象是否是由 reactive、readonly、shallowReactive、shallowReadonly 创建的代理
isReactive
检查一个对象是否是由 reactive 或 shallowReactive 创建的代理
isReadonly
检查传入的值是否为只读对象,通过 readonly 和 shallowReadonly 创建的代理都是只读的,(不能是ref对象)
6.12.3 进阶API
shallowRef、shallowReactive
对应ref与reactive,开启浅层监听
js
const shallowArray = shallowRef([
/* 巨大的列表,里面包含深层的对象 */
])
// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]
// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [
{
...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]triggerRef
强制触发ref与reactive,的浅层监听
js
const shallow = shallowRef({
greet: 'Hello, world'
})
// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 因为是浅层监听,不会触发watchEffect事件
shallow.value.greet = 'Hello, universe'
// 强制触发浅层监听
triggerRef(shallow)customRef
创建一个自定义的ref,显式声明对其依赖追踪和更新触发的控制方式
- 示例:创建一个防抖
ref,即只在最近一次set调用后的一段固定间隔后再调用
js
import { customRef } from 'vue'
export function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}vue
<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>
<template>
<input v-model="text" />
</template>shallowReadonly
readonly的浅层监听
toRaw
返回由 reactive、readonly、shallowReactive、shallowReadonly创建的代理对应的原始对象
js
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // truemarkRaw
将一个对象标记为不可被转为代理。返回该对象本身。
js
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false isReactive:检查对象是否是由 `reactive` 或 `shallowReactive` 创建的代理
// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // falseeffectScope
创建一个 effect 作用域,方便统一管理响应式副作用(即计算属性和侦听器)
js
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()getCurrentScope
如果有的话,返回当前活跃的 effect 作用域。
onScopeDispose
在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
getCurrentInstance
在组件内部访问当前组件实例的属性和方法
vue
<template>
<div ref="ref">
<div ref="childRef">
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
const that = getCurrentInstance();// 获取当前实例对象this
onMounted(()=>{
console.log(that.refs);// 实例对象的内容需要在onMounted生命周期之后获取
})
</script>useCssModule
setup()和<script setup>中访问注入的class 详见:vue中的特殊选择器、方法
6.12.4 <script setup>API
defineProps
为 props 定义类型
vue
<script setup>
const props = defineProps({
foo: String
})
</script>defineEmits
为 emits 定义类型
vue
<script setup>
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// Vue3.3+:简洁的语法
const emit = defineEmits<{
change: [id: number] // 具名元组语法
update: [value: string]
}>()
</script>defineExpose
<script setup> 的组件属性和方法都是默认私有的,需要使用defineExpose显式暴露
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const add = ()=>{count++}
// 向外暴露变量 count 和 add 方法
defineExpose({
count,
add
})
</script>defineOptions
使<script setup>支持声明组件选项API
vue
<script setup>
import { onMounted } from 'vue'
defineOptions({
name: 'HelloWorld',
mounted() {
console.log('mounted')
},
})
onMounted(()=>{
console.log('onMounted')
})
</script>defineSlots
为作用域插槽定义类型
vue
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): any
}>()
</script>与setup()混合使用
vue
<script>
// 普通 <script>, 在模块作用域下执行 (仅一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>useSlots
useAttrs
await
<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup()
vue
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>泛型
vue
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
selected: T
}>()
</script>vue
<script
setup
lang="ts"
generic="T extends string | number, U extends Item"
>
import type { Item } from './types'
defineProps<{
id: T
list: U[]
}>()
</script>