Vue3 |
您所在的位置:网站首页 › 怎么hook一个函数 › Vue3 |
写在前面
本文的知识主要来源为本人观看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、生命周期函数等等: 而vue3的代码,你可以自由按照功能逻辑来组织: 当然,当一个组件代码量过大的时候,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页面的架构应该如下图所示: 至于什么时候拆分,如何拆分,这就看具体的场景和个人习惯了,大家可以参考一下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 |