写在前面
本文的知识主要来源为本人观看vue conf上antfu演讲后做的总结和自己的经验。
组合式api介绍
如果你已经了解vue3组合式api,并已经清楚知道它比vue2强大在何处,那么可以跳过这一节。
本节图片来源juejin.cn/post/706075…
先介绍一下vue3的组合式api,以一段切换暗色模式的组件代码为例子;
vue2的写法:
<script>
export default{
data(){
return{
dark: false
}
},
computed:{
light(){
return !this.dark
}
},
methods:{
toggleDark(){
this.dark = !this.dark
}
}
}
</script>
接下来是vue3组合式API的写法:
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const dark = ref(false)
const light = computed(() => !dark.value)
const toggleDark = ()=>{
dark.value = !dark.value
}
return {
dark,
light,
toggleDark
}
}
}
</script>
如果想修改这个切换暗色模式的逻辑,你会怎么做呢?
在vue2中,你需要先后在data、computed、methods中寻找这段逻辑有关的代码,然后再一个个修改,修改时要在代码上下跳转:
而在vue3中,你只需要找到这块逻辑集中的地方,统一改动就可以了。
这就引出了vue3的第一个优点:逻辑关注点分离。
vue2组织代码的形式是按照API类型来组织的,它把一个组件分成了很多不同的API块,如data、computed、methods、生命周期函数等等:
而vue3的代码,你可以自由按照功能逻辑来组织:
当然,当一个组件代码量过大的时候,vue3仍会有可读性的问题,这时候就需要拆分代码了。
在vue2中,如果我们想要拆分出一段涉及到响应式变量(视图)逻辑,通常在定义一个组件,然后通过mixins来组合它们。
但是mixins的有很多缺陷,最大的缺点就是丢失上下文,且拥有潜在的命名冲突,很多时候根本享受不到拆分带来的便利。
那么,vue3应该怎么做呢? 还是以这段逻辑为基础,我们先写出抽离后,我们想要的效果:
App.vue:
<template>
<button @click="toggleDark"></button>
</template>
<script>
import { ref, computed } from 'vue'
import { useDark } from './composible'
export default{
setup(){
const { dark, light, toggleDark } = useDark()
return {
dark,light,toggleDark
}
}
}
</script>
这样,如果我们需要改动暗色模式的逻辑,只用去修改useDark函数就可以了。
如果只接触过vue2的同学可能不会理解,为什么这种涉及到模板变量的逻辑也可以被单独抽到一个函数里?
这就引出了vue3第二个强大之处:vue3的响应式系统可以独立在组件外使用!
composible.ts:
import { ref, computed } from 'vue'
export function useDark(){
const dark = ref(false)
const light = computed(() => !dark.value)
const toggleDark = ()=>{
dark.value = !dark.value
}
return{
dark, light, toggleDark
}
}
这样把逻辑拆成一个函数,而函数中可以单独使用vue响应式系统提供的api,甚至可以单独使用生命周期钩子,这种函数我们一般称为vue3的hook函数。多个hook函数可以灵活组合,每个hook函数里也可以使用其他hook。
最终,一个vue3页面的架构应该如下图所示:
至于什么时候拆分,如何拆分,这就看具体的场景和个人习惯了,大家可以参考一下vueuse项目:vueuse.org/,或者github上的…
总结:
| 声明式api | 组合式api |
|---|---|
| 不利于复用 | 极易复用(原生JS函数) |
| 潜在命名冲突 | 可灵活组合(生命周期钩子可多次使用) |
| 上下文丢失 | 更好的上下文支持 |
| 有限的类型支持 | 完善的Typescript支持 |
| 按API类型组织 | 按功能逻辑组织 |
| 只能用于vue组件中 | 可在vue组件外独立使用 |
hook函数编写模式与技巧
ref和reactive怎么选择?
这里有个偷懒的选择方法,但也经过了很多库的实践:能使用ref的情况下 就使用ref。
原因如下:
ref可以显式调用.value,触发类型检查
例如 let foo = ref(1)如果不小心给foo赋值了一个普通变量foo = 2,TS编译器会报错,然后加上.value,你就能区分这是个响应式的变量,而reactive可能会和普通变量混淆。
ref比起reactive局限更少
reactive可以自动解包,但是有一些坏处:
-
在类型上和一般对象没有区别
-
使用ES6解构会导致响应式丢失(toRef)
-
需要使用箭头函数包装才可以进行watch
-
const foo = { prop: 0 } const bar = reactve({ prop: 0 }) foo.prop = 1 bar.prop = 2 //这种代码看上去,这两个变量没有区别,需要去检查初始化的代码才能知道
有很多同学刚开始用vue3,可能不太喜欢.value的使用,但其实ref在很多情况下会被vue自动解包:
-
watch监听时
-
模板中(同时在模板中赋值也不用.value)
-
//使用reactive包裹嵌套的ref也会自动解包 const foo = ref('bar') const data = reactive({ foo, id }) data.foo = bar //ok
unref - Ref的反操作
原理:如果传入ref,则返回其值,否则就原样返回。
实现:
import { isRef } from 'vue'
function unref<T>( r: Ref<T> | T ): T {
return isRef(r) ? r.value : r
}
该函数已于正式版中被vue3官方收编,直接使用:
import { unref } from 'vue'
有了这个函数,我们就可以进行一些比较无脑的写法:
import { ref, unref } from 'vue'
const foo = ref('foo')
unref(foo) // foo
const bar = 'bar'
unref(bar) // bar
这在hooks多重嵌套时将会很有帮助。
模式:接受ref作为函数参数
先来一个纯函数:
function add(a: number, b: number) {
return a + b
}
这个函数的结果不会依赖a和b之后变化。
接下来是接受ref作为参数的函数:
import { Ref, computed } from 'vue'
function add( a: Ref<number>, b: Ref<number> ){
return computed( ()=> a.value + b.value )
}
这样,函数的结果也是一个ref,它也会永远依赖与a和b的值( computed )。
更加灵活的写法,同时接受ref和字面量:
import { Ref, computed, unref } from 'vue'
function add( a: Ref<number> | number, b: Ref<number> | number ){
return computed( ()=>unref(a) + unref(b) )
}
使用起来很无脑:
const a = ref(1)
const c = add( a,5 )
console.log( c.value ) //6
a.value = 2
console.log( c.value ) //7
MaybeRef类型工具
如果不太了解TS的类型工具编写,可以先学习后再进行实践。
MaybeRef类型工具实现:
type MaybeRef<T> = Ref<T> | T
很简单但是很实用,在vueuse库中就大量使用了这个类型工具。
假如不使用Mayberef:
export function useTimeAgo(
time: Date | number | string | Ref<Date | number | string>
){
return computed( ()=> someFormating( unref(time) ) )
}
可以看到参数类型非常繁琐,可以使用MaybeRef进行简化:
export function useTimeAgo( MaybeRef<Date | number | string> ){
return computed( ()=> someFormating( unref(time) ) )
}
编写更加灵活的hook
尽量让函数可以灵活的使用,以vueuse中的useTitle函数举例,该函数控制页面的title标签。
使用:
// 通常用法
const title = useTitle()
title.value = 'Hello World'
// 更加灵活的用法
const name = ref('Hello')
const title = computed( ()=> `${name.value} World` )
useTitle(title) //此时title和页面的title建立了连结
name.value = 'Hi' //页面标题变成 Hi World
实现
import { ref, watch } from 'vue'
import { MaybeRef } from '@vueuse/core'
function useTitle( newTitle: MaybeRef<string | null | undefined>){
const title = ref(newTitle ?? document.title) //核心
watch(title, (t)=> {
if(t){
document.title = t
}
}, { immediate: true })
}
可以看到,这个hook使用watch api让自己变得更加灵活,我们在编写hook时也应该时刻考虑如何让它更灵活。
重复使用已有的ref
新手可能会编写这种代码:
const foo = ref(1)
// ...
const bar = isRef(foo) ? foo : ref(foo)
const bar = ref(foo)
在不确定类型时会进行这种判断,但其实并不需要。
因为如果一个ref被传递给ref构造函数,它将原样返回
const foo = ref(1)
const bar = ref(foo)
foo === bar // true
模式:由ref组成的对象
在hook中返回由ref组成的对象,会让hook的使用更加灵活:
function useMouse() {
// ...
return {
x: ref(0)
y: ref(0)
}
}
//可以直接使用
const { x } = useMouse()
x.value // 0
//也可以自动解包(不需要.value)
const mouse = reactve(useMouse())
mouse.x // 0
记住这个模式的编写和使用,很省事。
技巧:将异步操作转换为同步写法
以vueuse中的useFetchhook为例,使用:
// 原生fetch
const data = await fetch('url').then( r=> r.json )
// useFetch
const data = useFetch('url').json
const user = computed( () => data.value ? data.value.user )
大概的实现如下:
function useFetch( url: MaybeRef<string> ){
const data = shallowRef<T | undefined>()
const error = shallowRef<Error | undefined>()
fetch(unref(url))
.then(c => c.json)
.then(r => data.value = r)
.catch(e => error.value = e)
return {
data,
error
}
}
关于shallowRef,可以参考下vue官方文档哦。
重点:清除副作用
首先,编写hook时记得清除自己创造的副作用:
import { onUnmounted } from 'vue'
export function useEventListener(target: EventTarget, name: string, fn: any){
target.addEventListener(name, fn)
onUnmounted(()=>{
target.removeEventListener(name, fn) // <--
})
}
但是有另外一种特殊的副作用也需要清除,
在vue的setup()中,watch、computed等副作用会被收集并且绑定到当前组件实例上,当实例unmounted时,这些副作用会被vue释放。
但我们在编写独立hook时,没有vue帮我们来处理这种脏活了,我们需要自己处理这种情况,
比如,先收集computed和watch:
const disposables = []
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
disposables.push(() => stop(doubled.effect))
const stopWatch1 = watchEffect(() => {
console.log(`counter: ${counter.value}`)
})
disposables.push(stopWatch1)
const stopWatch2 = watch(doubled, () => {
console.log(doubled.value)
})
disposables.push(stopWatch2)
然后手动释放:
disposables.forEach((f) => f())
disposables = []
我们不想每次编写hook都要干这种脏活。
还好,vue3.2提供了一个专门处理这种情况的api:effectScope。
effectScope会收集在它内部的effect、computed、watch、watchEffect,然后提供了函数来释放。
function useXXX() {
// ...
const scope = effectScope()
//副作用写在run的回调函数中
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
//释放所有副作用
scope.stop()
}
记得要用哦。
类型安全的Provice & Inject
如果不知道这个玩意,我们在写Provide和Inject时会丢失类型(也就是变成any)。
这个玩意就是injectionKey:
//context.ts
import { injectionKey } from 'vue'
export interface UserInfo{
id: number
name: string
}
//父组件
export const injectKeyUser: injectionKey<UserInfo> = Symbol()
import { provice } from 'vue'
import { injectKeyUser } from './context'
export default {
setup(){
provice( injectKeyUser, {
id: 1,
name: 'xxx'
})
}
}
import { inject } from 'vue'
import { injectKeyUser } from './context'
export default {
setup(){
const user = inject(injectKeyUser)
if(user){
console.log( user.name ) //ok
}
}
}
这样就可以给他们类型了。
模式:状态共享
也许你已经看了一些关于vue3并不需要状态共享的文章了。
为什么说在vue3中可以不需要vuex这种状态管理工具?
因为组合式API可以独立于组件被使用,所以天然可以被组件共享:
//store.ts
import { reactive } from 'vue'
export const store = reactive({
state: {
foo: 'bar'
}
})
//A.vue
import { store } from 'store'
store.state.foo = 'yeah'
//B.vue
import { store } from 'store'
console.log(store.state.foo) // 'yeah'
vue3官方推荐状态管理库pinia就是类似这种模式。
让vue2也能用上组合式api
插件@vue/composition-api是可以为vue2提供组合式API的插件,可以使用以上所有的技巧!
此外,vue2.7版本也会官方支持以下特性:
- 将vue/composition-api整合进vue2核心
- 支持setup script
- 将vue2代码迁移到typescript
- vue2将继续支持ie11
- LTS
如果不想从vue2直接升级到vue3,可以一起期待vue2.7的到来哦。
另外,对于vue组件库作者来说,可以引入vue-demi,这样你的包既可以使用组合式api,又可以同时兼容vue2和vue3!