Vue3

您所在的位置:网站首页 怎么hook一个函数 Vue3

Vue3

2023-06-16 16:33| 来源: 网络整理| 查看: 265

写在前面

本文的知识主要来源为本人观看vue conf上antfu演讲后做的总结和自己的经验。

组合式api介绍

如果你已经了解vue3组合式api,并已经清楚知道它比vue2强大在何处,那么可以跳过这一节。

本节图片来源juejin.cn/post/706075…

先介绍一下vue3的组合式api,以一段切换暗色模式的组件代码为例子;

vue2的写法:

export default{ data(){ return{ dark: false } }, computed:{ light(){ return !this.dark } }, methods:{ toggleDark(){ this.dark = !this.dark } } }

接下来是vue3组合式API的写法:

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 } } }

如果想修改这个切换暗色模式的逻辑,你会怎么做呢?

在vue2中,你需要先后在data、computed、methods中寻找这段逻辑有关的代码,然后再一个个修改,修改时要在代码上下跳转:

而在vue3中,你只需要找到这块逻辑集中的地方,统一改动就可以了。

这就引出了vue3的第一个优点:逻辑关注点分离。

vue2组织代码的形式是按照API类型来组织的,它把一个组件分成了很多不同的API块,如data、computed、methods、生命周期函数等等:

img

而vue3的代码,你可以自由按照功能逻辑来组织:

img

当然,当一个组件代码量过大的时候,vue3仍会有可读性的问题,这时候就需要拆分代码了。

在vue2中,如果我们想要拆分出一段涉及到响应式变量(视图)逻辑,通常在定义一个组件,然后通过mixins来组合它们。

但是mixins的有很多缺陷,最大的缺点就是丢失上下文,且拥有潜在的命名冲突,很多时候根本享受不到拆分带来的便利。

那么,vue3应该怎么做呢? 还是以这段逻辑为基础,我们先写出抽离后,我们想要的效果:

App.vue:

import { ref, computed } from 'vue' import { useDark } from './composible' export default{ setup(){ const { dark, light, toggleDark } = useDark() return { dark,light,toggleDark } } }

这样,如果我们需要改动暗色模式的逻辑,只用去修改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页面的架构应该如下图所示:

img

至于什么时候拆分,如何拆分,这就看具体的场景和个人习惯了,大家可以参考一下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( r: Ref | 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, b: Ref ){ return computed( ()=> a.value + b.value ) }

这样,函数的结果也是一个ref,它也会永远依赖与a和b的值( computed )。

更加灵活的写法,同时接受ref和字面量:

import { Ref, computed, unref } from 'vue' function add( a: Ref | number, b: Ref | 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 = Ref | T

很简单但是很实用,在vueuse库中就大量使用了这个类型工具。

假如不使用Mayberef:

export function useTimeAgo( time: Date | number | string | Ref ){ return computed( ()=> someFormating( unref(time) ) ) }

可以看到参数类型非常繁琐,可以使用MaybeRef进行简化:

export function useTimeAgo( MaybeRef ){ 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){ 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 ){ const data = shallowRef() const error = shallowRef() 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) // 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 = 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!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3