函数式计算属性
通常写计算属性的一般写法如下
typescript
const numberA = ref(0)
const numberB = ref(0)
const result = computed(() => numberA.value + numberB.value)
函数式可以这样写。这样写的理由是,add
可以封装一次多处复用,而不是像上边那种写法,每次都需要书写一遍
typescript
// 函数传入a、b两个Ref类型的参数,返回计算属性
function add(a: Ref<number>, b: Ref<number>) {
return computed(() => a.value + b.value)
}
// 无论修改numberA还是numberB,result都会自动改变。也可以将numberA、numberB设置一个常量一个Ref。
const numberA = ref(0)
const numberB = ref(0)
const result = add(numberA, numberB)
ref 自动解包
将ref
整合到reactive
,而不用在.value
去调用
typescript
const numberA = ref(0)
const numberB = ref(0)
const numberC = 0
const result = reactive({
numberA,
numberB,
numberC,
})
// result.numberA
// result.numberB
// result.numberC
上边这种类比比较基础,通畅在封装一个use
函数,往往有许多ref
返回的类型,可以直接整合起来
typescript
// 自定义的hook
function useAdd(a: Ref<number>, b: Ref<number>) {
return {
a,
b,
result: computed(() => a.value + b.value)
}
}
// 可以整合到reactive
const result = reactive(useAdd(ref(0), ref(0)))
result.a
result.b
result.result
重用ref
如果不太想关心传入的参数是不是ref
,可以直接用ref
去接收一个ref
,像这个例子:这里 isRef(a) ? a.value
写法 和ref(a).value
完全等价
typescript
function add(a: Ref<number> | number, b: Ref<number>) {
// 这里不清楚传入的a 是不是ref
// 一般是这样做
// return computed(() => isRef(a) ? a.value : a + b.value)
// 这里 isRef(a) ? a.value 写法 和 ref(a).value 完全等价
return computed(() => ref(a).value + b.value)
}
利用组合式函数和计算属性异步转同步
项目中最频繁的使用是发送后端的请求,以常见的axios
请求为例,通常会这样写。
typescript
const data = ref([]) // 需要和页面交互的数据
function loadList() {
axios.get("xxxx").then(res => {
data.value = res.data
})
}
这样的使用很不符合组合api的思维,这里简单封装一下
typescript
function useAxios(config: AxiosRequestConfig) {
const data = ref({})
function load() {
axios(config).then(res => {
data.value = res.data
})
}
return {
load,
data
}
}
在使用的时候就很方便了
typescript
const { load: load1, data: data1 } = useAxios({ url: "xxxxx1", method: "get" })
const { load: load2, data: data2 } = useAxios({ url: "xxxxx2", method: "post" })
这样一来,只需要调用load
函数,data
数据就会自动响应到页面了。当然一般而言,我们响应的数据还要再处理下。
typescript
// 后端返回的可能是这个样子: {msg:"成功",data:[],code: 0},这样在取值的时候可以根据情况处理,然后再在页面上渲染。这样一来,就将一个异步代码
const result = computed(()=> data1.code === 0 ? data.value?.data : [])
这种情况挺多的例如setInterval
和setTimeout
。这类封装可参考https://vueuse.org/shared/usetimeout/#usetimeout
,axios封装的比较完善的代码可参考:https://github.com/biancangming/howuse/blob/main/src/axios/index.ts
。
清除副作用
组合api最大的好处就是封装更高阶的函数,这样就自动请求一些副作用,就像上边的useAxios
,组件卸载的时候可以直接取消正在进行的请求。就像axios
正在请求时,或者路由离开这个页面,当页面onUnmounted
时,需要清理内存中残留的状态。比如中断这个连接,清理data
中的数据,以此来提高页面的性能,而且封装一次,以后也不必关心这个重复的写法。当然也可以不这么做,老板也不懂
typescript
function useAxios(config: AxiosRequestConfig) {
const cancelToken = axios.CancelToken.source()
const data = ref({})
function load() {
axios({ ...config, cancelToken: cancelToken.token }).then(res => {
data.value = res.data
})
}
onUnmounted(() => {
cancelToken.cancel() // 离开页面时,取消请求
data.value = [] // 离开页面,清除不必要的数据
})
return {
load,
data
}
}
或者是像常见的addEventListener
,setInterval
,都可以这样封装,路由跳转的时候removeEventListener
和clearInterval
,消除副作用。
vue
组件在销毁的时候会自动清除一些副作用,但是有时候,想要一些动作,达到一定目的之后就不再执行。vue3
有个优秀的清除副作用的辅助函数,可以参考这里:https://cn.vuejs.org/api/reactivity-advanced.html#effectscope
,刚好这个函数就可以达到这种目的
typescript
const scope = effectScope() // 定义一个scope
const counter = ref(1) // counter 初始值 1
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
})
// 这个定时任务用于counter自增,当counter自增到10的时候,让 doubled 和 watch 失去作用
const countPlus = setInterval(() => {
counter.value++
if (counter.value > 10) {
scope.stop() // 让 doubled 和 watch 失去作用,这样的好处就是及时回收没有必要的副作用。即便定时任务停止,再修改counter,对应的计算属性、监听都不再产生作用
clearTimeout(countPlus)// 清除定时任务
}
}, 1000)
依赖注入可靠类型注解
依赖注入经常在用,尤其针对项目的全局配置很有用,但是类型提示很头疼,InjectionKey
这个辅助函数很有用,可以将注入的对象单独写在一个公用文件。
import { InjectionKey, Ref } from 'vue';
export const injectKey: InjectionKey<Ref<string>> = Symbol()
提供注入的函数
import { injectKey } from "xx.ts"
const username = ref()
provide(injectKey, username)
使用注入
import { injectKey } from "xx.ts"
const username = inject(injectKey)
username.value // 得到正确的代码提示
状态共享
在进行项目开发的时候,很多时候需要多个组件之间数据交互,父子组件可以props
和emit
,兄弟组件多使用eventBus
或者mitt
,也有好多人直接使用vuex
或者pinia
。在大多需求中,这些方式要么不方便、要么消耗过多、或者是代码不好组织。vue3
这种灵活的api,可以随意使用。就像官方说的那样,vue3的组合api,可以直接被拎出来使用。
// example/store.ts
import { ref } from "@vue/reactivity" // 这个包是响应式核心依赖包,vue当中也是依赖这个包
//或者
// import { ref } from "vue"
export const count = ref(0)
在组件当中使用的时候可以这样, A组件和B组件得到共享,同样的,需要注意在页面销毁时清除count
,否则残留在内存当中,因为他脱离了vue
的周期
// 组件A
import { count } from "example/store"
count.value ++
// 组件B
import { count } from "example/store"
console.log(count.value) // 1
useSlots()
vue3 废除了 $slots
,可以用useSlots
代替
const slots = useSlots() // slots获得的值,等价于原来的`$slots`
继承父级slots,封装高阶组件时,经常要继承别的组件。
html
<template v-for="(_,name) in slots" #[name]>
<slot :name="name"></slot>
</template>
typescript
const slots = useSlots()
判断是否传入插槽,有时候需要在没有传入插槽内容的时候,隐藏对应插槽样式
html
<div :class="`${prefixCls}__footer`" v-if="isShowSlot('footer')">
<slot name="footer"></slot>
</div>
typescript
const slots = useSlots()
const isShowSlot = (name: string) => !!slots[name];
useAttrs()
vue3 废除了 $attrs
以及$listeners
,这两个api都合并到useAttrs
,对应vue3来说
,@
符号相当于:on
加上事件名首字母大写,
这就是为什么可以通过useAttrs
这么一个api或者全部的事件以及属性。
例如:
html
<template>
<button @click="handle1">按钮1</button>
<button :onClick="handle2">按钮2</button>
</template>
<script lang="ts" setup>
const handle1 = () => { alert(1) }
const handle2 = () => { alert(2) }
</script>
这样,在封装高阶组件的时候很有用,许多crud的组件都是这么干的。例如:这块有个增删改查的组件。https://biancangming.github.io/howuse/#/crudAntd
源码可以在这里看看:https://github.com/biancangming/howuse/tree/main/src/crud/antd
父子组件快速双向绑定
传统模式需要定义props
和emit
, 通常子组件的代码如下,比较繁琐。
ts
const content = ref("")
const props = defineProps({
content: ""
})
const emit = defineEmits<{
(e: "update:content", content: string): void
}>()
emit("update:content", unref(content))
vue 3.3 + 增加了一个宏函数,defineModel
, 实现上述任务只需要一行代码
ts
const content = defineModel("content", { type: String, default: () => "" })
父组件
html
<MingHtmlEditor v-model:content="content"/>
由于这个目前还是实验性特性,需要开启vue
相关配置,vite.config.ts
中配置如下
ts
plugins: [
vue({
script: {
defineModel: true,
}
}),
]