一、setup()
1.1、选项式api中简单使用
setup() 钩子是在组件中使用组合式 API 的入口
使用场景:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
当我们使用响应式api的时候,需要在setup()中返回出去暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
count
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
- 在模板中访问从
setup
返回的ref时,它会自动浅层解包,因此你无须再在模板中为它写.value
。当通过this
访问时也会同样如此解包。 setup()
自身并不含对组件实例的访问权,即在setup()
中访问this
会是undefined
。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。setup()
应该同步地返回一个对象。唯一可以使用async setup()
的情况是,该组件是Suspense组件的后裔- 对于结合单文件组件使用的组合式 API,推荐通过 <script setup>以获得更加简洁及符合人体工程学的语法。
1.2、<script setup>语法糖
在script 中加入setup 属性,是给script添加语法糖。
Q:什么是语法糖?
A:也称糖衣语法。指的是在计算机语言中添加的某种语法,这种语法对语言的编译结果和功能并没有实际影响, 但是却能更方便程序员使用该语言。
在<script setup>的语法糖中,虽然看上去写法简洁,没有setup()的export default,不需要return,但是实际编译出来还是会有这些代码。
因此,<script setup>也会遵循setup()的某些规则,如模板中ref自动解包,<script setup></script>标签内,this为undefine。
二、ref全家桶
2.1、ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
如果将一个对象赋值给 ref,那么这个对象将通过 reavtive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。
类型:
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
使用ts为其标注类型
<script setup lang="ts">
import { ref, reactive } from 'vue';
import type { Ref } from 'vue';
const message: Ref<string | number> = ref('你好');//使用接口约束
const value = ref<string | number>('你好吗??');//使用泛型约束
</script>
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包。
你也可以直接在事件监听器中改变一个 ref
<button @click="count++">
{{ count }}
</button>
ref深层响应式,即便是map,或者对象,他的属性的改变依旧能够被监听到。
<template>
姓名:<input type="text" v-model="person.name" />
<br />
年龄:<input type="text" v-model="person.age" />
<br />
性别:<input type="text" v-model="person.sex" />
<br>
c: <input type="text" name="" id="" v-model="person.a.b.c">
<br />
{{ person }}
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
const person: Ref<any> = ref({
name: "张三",
age: 18,
sex: "男",
a:{
b:{
c:'aaa'
}
}
});
</script>
2.2、shallowRef()
ref()的浅层作用形式,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对
.value
的访问是响应式的。
类型:
function shallowRef<T>(value: T): ShallowRef<T>
interface ShallowRef<T> {
value: T
}
实例:
<template>
{{ person }}
<br />
<button @click="chlck1">修改.value</button>
<button @click="chlck2">替换.value</button>
</template>
<script setup lang="ts">
import { shallowRef } from "vue";
import type { ShallowRef } from "vue";
const person: ShallowRef<Map<string, any>> = shallowRef(new Map());
//不会触发更新
const chlck1 = () => {
person.value.set("name", "张三");
console.log(person);
};
//会触发更新
const chlck2 = () => {
const map = new Map();
map.set("name", "李四");
person.value = map;
console.log(person);
};
</script>
2.3、triggerRef()
当你修改shallowRef的属性的时候,是不会触发深层响应的。
这个时候使用triggerRef可以强制触发响应式。
类型:
function triggerRef(ref: ShallowRef): void
实例:当使用普通修改的时候,因为修改是深层的,所有不会触发响应式。
但是修改之后使用triggerRef()可以将浅层响应式强制触发为深层。
<template>
<div>
{{ number }}
<button @click="but1">普通修改</button
><button @click="but2">修改后使用triggerRef</button>
</div>
</template>
<script setup lang="ts">
import { shallowRef, triggerRef } from "vue";
const number = shallowRef({
number: 1,
});
const but1 = () => {
number.value.number++;
console.log(number.value);
};
const but2 = () => {
number.value.number++;
triggerRef(number);
console.log(number.value);
};
</script>
2.4、customRef()
创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
类型:
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
type CustomRefFactory<T> = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
自定义一个ref,返回一个customRef()出去,customRef()接收两个参数:
- track() 依赖收集
- trigger() 依赖更新
customRef()返回一个对象,带有以下两个方法:
- get(): 拦截对象的访问,通常在这里进行track()依赖收集。
- set(): 拦截对象的赋值,通常在这里进行trigger()依赖更新。
看到get() 和 set(),你就应该对此有印象。因为vue3的响应式原理就是基于proxy,进行拦截。
实例:自建一个防抖ref
<template>
<button style="width: 100px;height: 50px;" @click="number++">{{ number }}</button>
</template>
<script setup lang="ts">
import { customRef } from 'vue';
const myRef = (value: any) => {
return customRef((track, trigger) => {
let timmer: any = null;
return {
get() {
track();
return value;
},
set(newValue) {
value = newValue;
clearTimeout(timmer);
timmer = setTimeout(() => {
clearTimeout(timmer);
trigger();
}, 500);
},
}
})
}
const number = myRef(0);
</script>
三、reactive
3.1、reavtive()
返回一个对象的响应式代理。
类型:
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
详细信息:
- 响应式转换是“深层”的
- 当一个 ref 作为一个响应式对象的 property 被访问或更改时,它会自动解包。
- 当访问到某个响应式数组或
Map
这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。 - 返回的对象以及其中嵌套的对象都会通过 Proxy 包裹,
自动解包示例:
<template>
<div>
{{ person }}
<br>
<button @click="">点击</button>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
const name = ref('李四');
const person = reactive({
name: name,//响应式对象作为属性
age: 19
})
console.log(name);//这里不会自动解包
console.log(person.name);//这里的name可以不.value 即ref对象作为响应式对象的属性会自动解包。
</script>
3.2、shallowReactive()
reactive()的浅层作用形式。
类型:
function shallowReactive<T extends object>(target: T): T
详细信息:
和 reactive()
不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
示例:
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性是响应式的
state.foo++
// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false
// 不是响应式的
state.nested.bar++
四、计算属性computed()
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过
.value
暴露 getter 函数的返回值。它也可以接受一个带有get
和set
函数的对象来创建一个可写的 ref 对象
类型:
// 只读
function computed<T>(
getter: () => T,
// 查看下方的 "计算属性调试" 链接
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
示例:
创建一个只读的计算属性:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
实际场景:简单的购物车,使用计算属性来响应式最后的总价格。
<template>
<table border="">
<tr>
<th>商品</th>
<th>价格</th>
<th>数量</th>
<th>小计</th>
</tr>
<tr v-for="item, index in itemArr" :key="index">
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<td>{{ item.number }} <button @click="item.number++">+</button><button
@click="item.number < 1 ? item : item.number--">-</button></td>
<td>{{ item.number * item.price }}¥</td>
</tr>
<tr>
<td>合计:</td>
<td></td>
<td></td>
<td>{{ title }}¥</td>
</tr>
</table>
</template>
<script setup lang="ts">
import { reactive, computed } from 'vue';
const itemArr = reactive([
{ name: '面包', price: 3, number: 3 },
{ name: '火腿', price: 1.5, number: 2 },
{ name: '泡面', price: 4, number: 1 },
])
//计算属性
const title = computed(() => {
let sum = 0;
itemArr.forEach(item => {
sum += item.number * item.price;
})
return sum;
})
</script>
<style>
th,
td {
width: 100px;
height: 50px;
text-align: center;
}
button {
width: 20px;
}
</style>
创建一个可修改的计算属性:
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
五、readonly
5.1、readonly()
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
类型:
function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>>
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive()
相同,但解包得到的值是只读的。
示例:
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})
// 更改源属性会触发其依赖的侦听器
original.count++
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!
5.2、shallowReadonly()
readonly()的浅层作用形式
类型:
function shallowReadonly<T extends object>(target: T): Readonly<T>
和 readonly()
不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性会失败
state.foo++
// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false
// 这是可以通过的
state.nested.bar++
六、侦听器
6.1、watch()
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
类型:
// 侦听单个来源
function watch<T>(
source: WatchSource<T>,
callback: WatchCallback<T>,
options?: WatchOptions
): StopHandle
// 侦听多个来源
function watch<T>(
sources: WatchSource<T>[],
callback: WatchCallback<T[]>,
options?: WatchOptions
): StopHandle
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
type WatchSource<T> =
| Ref<T> // ref
| (() => T) // getter
| T extends object
? T
: never // 响应式对象
interface WatchOptions extends WatchEffectOptions {
immediate?: boolean // 默认:false
deep?: boolean // 默认:false
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
参数详解:
第一个参数是侦听器的源,在源发生改变的时候执行回调函数。它可以接受以下类型的源:
- 一个函数,带有返回值。即getter()
- 一个ref
- 一个响应式对象
- 以上类型组成的数组。
第二个参数是在发生变化时要调用的回调函数。
这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,用来配置侦听器,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。onTrack / onTrigger
:调试侦听器的依赖。
第一个参数是我们需要监听的对象,第二个参数是监听对象发生变化所执行的回调函数,第三个参数是配置。
示例:
监听一个getter():
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
监听一个ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
深层次监听:
监听一个响应式对象时,会自动开启深层监听。但是监听一个getter,不会自动开启深层监听,需要手动开启配置。
在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。
<template>
<input type="text" v-model="person.age">
</template>
<script lang="ts" setup>
import { watch, reactive } from 'vue';
const person = reactive({
name: '李四',
age: 18
})
watch(
() => person,
(newVla, oldVal) => {
//这里的新值和旧值会是同一个值。
// 因为深层监听 两者指向的是同一个引用。
console.log('这是新值:' + newVla.age);
console.log('这是旧值:' + oldVal.age);
},
// 第三个参数为对象,用来配置侦听器
{
deep: true,//开启深层监听
}
)
</script>
停止侦听器:
const stop = watch(source, callback)
// 当已不再需要该侦听器时:
stop()
响应式工具
2.5.1、isRef()
检查某个值是否为 ref。
类型:
function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
实例:在使用响应式之前进行检测,可方式类型错误。
<template>
<div>
{{ name + '::' + age }}
</div>
<button @click="clickT">改变值</button>
</template>
<script setup lang="ts">
import { ref, isRef } from 'vue'
let name = ref('李四');
let age: any = 18;
const clickT = () => {
if (isRef(name)) {
name.value = '张三';
} else {
name = ref('张三');
}
if (isRef(age)) {
age.value = 22;
} else {
age = ref(22);
}
}
</script>
2.5.2、unref()
如果参数是 ref,则返回内部值,否则返回参数本身。这是
val = isRef(val) ? val.value : val
计算的一个语法糖。
类型:
function unref<T>(ref: T | Ref<T>): T
示例:
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x)
// unwrapped 现在保证为 number 类型
}
2.5.3、toRef()
toRef()有两种用法,一种是规划化签名。
另一种是对象属性签名: 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
两者返回的都是一个ref对象。
2.5.3.1、规范化签名
根据传入的参数不同,返回不同类型的ref对象。
- ref对象。
// 按原样返回现有的 ref
toRef(existingRef)
- getter函数
// 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
toRef(() => props.foo)
- 普通值
// 从非函数的值中创建普通的 ref
// 等同于 ref(1)
toRef(1)
2.5.3.2、对象属性签名
类型:
// 对象属性签名
function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]>
type ToRef<T> = T extends Ref ? T : Ref<T>
示例:
<template>
{{ person }}
<br>
{{ car }}
<br>
<button @click="name = '张三'">改变name</button>
<button @click="brand = '比亚迪'">改变brand</button>
</template>
<script setup lang="ts">
import { ref, toRef, reactive } from 'vue';
const car = reactive({
brand: '华为',
})
const person = ref({
name: '李四',
age: 22,
sex: '男'
})
//将person的属性单独拿出来响应
const name = toRef(person.value, 'name');
//如果是reactive的响应式对象
const brand = toRef(car, 'brand');
</script>