首页>国内 > 正文

手把手教你封装几个Vue3中很有用的组合式API

2022-03-24 18:40:42来源:Vue中文社区

就我自己的感觉而言,Hook与Composition API概念是很类似的,事实上在React大部分可用的Hook都可以使用Vue3再实现一遍。

为了拼写方便,下文内容均使用Hook代替Composition API。相关代码均放在github[1]上面。

useRequest背景

使用hook来封装一组数据的操作是很容易的,例如下面的useBook

import{ref,onMounted}from"vue"functionfetchBookList(){returnnewPromise((resolve)=>{setTimeout(()=>{resolve([1,2,3])},1000)})}exportfunctionuseBook(){constlist=ref([])constloading=ref(false)constgetList=async()=>{loading.value=trueconstdata=awaitfetchBookList({page:1})loading.value=falselist.value=data}onMounted(()=>{getList()})return{list,loading,getList}}

其中封装了获取资源、处理加载状态等逻辑,看起来貌似能满足我们的需求了

缺点在于对应另外一个资源而言,我们貌似还需要写类似的模板代码,因此可以将这一堆代码进行抽象,封装成useApi方法

实现
functionuseApi(api){constloading=ref(false)constresult=ref(null)consterror=ref(null)constfetchResource=(params)=>{loading.value=truereturnapi(params).then(data=>{//按照约定,api返回的结果直接复制给resultresult.value=data}).catch(e=>{error.value=e}).finally(()=>{loading.value=false})}return{loading,error,result,fetchResource}}

然后修改上面的useBook方法

functionuseBook2(){const{loading,error,result,fetchResource,}=useApi(fetchBookList)onMounted(()=>{fetchResource({page:1})})return{loading,error,list:result}}

注意这是一个非常通用的方法,假设现在需求封装其他的请求,处理起来也是非常方便的,不需要再一遍遍地处理loading和error等标志量

functionfetchUserList(){returnnewPromise((resolve)=>{setTimeout(()=>{constpayload={code:200,data:[11,22,33],msg:"success"}resolve(payload)},1000)})}functionuseUser(){const{loading,error,result,fetchResource,}=useApi((params)=>{//封装请求返回值returnfetchUserList(params).then(res=>{console.log(res)if(res.code===200){returnres.data}return[]})})//...}
思考

处理网络请求是前端工作中十分常见的问题,处理上面列举到的加载、错误处理等,还可以包含去抖、节流、轮询等各种情况,还有离开页面时取消未完成的请求等,都是可以在useRequest中进一步封装的

useEventBus

EventBus在多个组件之间进行事件通知的场景下还是比较有用的,通过监听事件和触发事件,可以在订阅者和发布者之间解耦,实现一个常规的eventBus也比较简单

classEventBus{constructor(){this.eventMap=newMap()}on(key,cb){lethandlers=this.eventMap.get(key)if(!handlers){handlers=[]}handlers.push(cb)this.eventMap.set(key,handlers)}off(key,cb){consthandlers=this.eventMap.get(key)if(!handlers)returnif(cb){constidx=handlers.indexOf(cb)idx>-1&&handlers.splice(idx,1)this.eventMap.set(key,handlers)}else{this.eventMap.delete(key)}}once(key,cb){consthandlers=[(payload)=>{cb(payload)this.off(key)}]this.eventMap.set(key,handlers)}emit(key,payload){consthandlers=this.eventMap.get(key)if(!Array.isArray(handlers))returnhandlers.forEach(handler=>{handler(payload)})}}

我们在组件初始化时监听事件,在交互时触发事件,这些是很容易理解的;但很容易被遗忘的是,我们还需要在组件卸载时取消事件注册,释放相关的资源。

因此可以封装一个useEventBus接口,统一处理这些逻辑

实现

既然要在组件卸载时取消注册的相关事件,简单的实现思路是:只要在注册时(on和once​)收集相关的事件和处理函数,然后在onUnmounted​的时候取消(off)收集到的这些事件即可

因此我们可以劫持事件注册的方法,同时额外创建一个eventMap用于收集使用当前接口注册的事件

//事件总线,全局单例constbus=newEventBus()exportdefaultfunctionuseEventBus(){letinstance={eventMap:newMap(),//复用eventBus事件收集相关逻辑on:bus.on,once:bus.once,//清空eventMapclear(){this.eventMap.forEach((list,key)=>{list.forEach(cb=>{bus.off(key,cb)})})eventMap.clear()}}leteventMap=newMap()//劫持两个监听方法,收集当前组件对应的事件conston=(key,cb)=>{instance.on(key,cb)bus.on(key,cb)}constonce=(key,cb)=>{instance.once(key,cb)bus.once(key,cb)}//组件卸载时取消相关的事件onUnmounted(()=>{instance.clear()})return{on,once,off:bus.off.bind(bus),emit:bus.emit.bind(bus)}}

这样,当组价卸载时也会通过instance.clear​移除该组件注册的相关事件,比起手动在每个组件onUnmounted时手动取消要方便很多。

思考

这个思路可以运用在很多需要在组件卸载时执行清理操作的逻辑,比如:

DOM事件注册addEventListener和removeEventListener计时器setTimeout和clearTimeout网络请求request和abort

从这个封装也可以看见组合API一个非常明显的优势:尽可能地抽象公共逻辑,而无需关注每个组件具体的细节

useModel

参考:

hox源码[2]背景

当掌握了Hook(或者Composition API)之后,感觉万物皆可hook,总是想把数据和操作这堆数据的方法封装在一起,比如下面的计数器

functionuseCounter(){constcount=ref(0)constdecrement=()=>{count.value--}constincrement=()=>{count.value++}return{count,decrement,increment}}

这个useCounter暴露了获取当前数值count、增加数值decrement和减少数值increment等数据和方法,然后就可以在各个组件中愉快地实现计数器了

在某些场景下我们希望多个组件可以共享同一个计数器,而不是每个组件自己独立的计数器。

一种情况是使用诸如vuex等全局状态管理工具,然后修改useCounter的实现

import{createStore}from"vuex"conststore=createStore({state:{count:0},mutations:{setCount(state,payload){state.count=payload}}})

然后重新实现useCounter

exportfunctionuseCounter2(){constcount=computed(()=>{returnstore.state.count})constdecrement=()=>{store.commit("setCount",count.value+1)}constincrement=()=>{store.commit("setCount",count.value+1)}return{count,decrement,increment}}

很显然,现在的useCounter2​仅仅只是store的state与mutations的封装,直接在组件中使用store也可以达到相同的效果,封装就变得意义不大;此外,如果单单只是为了这个功能就为项目增加了vuex依赖,显得十分笨重。

基于这些问题,我们可以使用一个useModel来实现复用某个钩子状态的需求

实现

整个思路也比较简单,使用一个Map来保存某个hook的状态

constmap=newWeakMap()exportdefaultfunctionuseModel(hook){if(!map.get(hook)){letans=hook()map.set(hook,ans)}returnmap.get(hook)}

然后包装一下useCounter

exportfunctionuseCounter3(){returnuseModel(useCounter)}//在多个组件调用const{count,decrement,increment}=useCounter3()//...const{count,decrement,increment}=useCounter3()

这样,在每次调用useCounter3时,都返回的是同一个状态,也就实现了多个组件之间的hook状态共享。

思考

userModel​提供了一种除vuex和provide()/inject()之外共享数据状态的思路,并且可以很灵活的管理数据与操作数据的方案,而无需将所有state放在一起或者模块下面。

缺点在于,当不使用useModel​包装时,useCounter就是一个普通的hook,后期维护而言,我们很难判断某个状态到底是全局共享的数据还是局部的数据。

因此在使用useModel处理hook的共享状态时,还要要慎重考虑一下到底合不合适。

useReducer

redux的思想可以简单概括为

store维护全局的state数据状态,各个组件可以按需使用state中的数据,并监听state的变化reducer​接收action并返回新的state,组件可以通过dispatch传递action触发reducerstate更新后,通知相关依赖更新数据

我们甚至可以将redux的使用hook化,类似于

functionreducer(state,action){//根据action进行处理//返回新的state}constinitialState={}const{state,dispatch}=useReducer(reducer,initialState);
实现

借助于Vue的数据响应系统,我们甚至不需要实现任何发布和订阅逻辑

import{ref}from"vue"exportdefaultfunctionuseReducer(reducer,initialState={}){conststate=ref(initialState)//约定action格式为{type:string,payload:any}constdispatch=(action)=>{state.value=reducer(state.value,action)}return{state,dispatch}}

然后实现一个useRedux​负责传递reducer和action

importuseReducerfrom"./index"functionreducer(state,action){switch(action.type){case"reset":returninitialState;case"increment":return{count:state.count+1};case"decrement":return{count:state.count-1};}}functionuseStore(){returnuseReducer(reducer,initialState);}

我们希望是维护一个全局的store,因此可以使用上面的useModel

exportfunctionuseRedux(){returnuseModel(useStore);}

然后就可以在组件中使用了

<script>exportdefault{name:"useReducer",setup(){const{state,dispatch}=useStore()return{state,dispatch}}}</script>

看起来跟我们上面useModel​的例子并没有什么区别,主要是暴露了通用的dispatch方法,在reducer处维护状态变化的逻辑,而不是在每个useCounter中自己维护修改数据的逻辑

思考

当然这个redux是非常简陋的,包括中间件、combineReducers、connect等方法均为实现,但也为我们展示了一个最基本的redux数据流转过程。

useDebounce与useThrottle背景

前端很多业务场景下都需要处理节流或去抖的场景,节流函数和去抖函数本身没有减少事件的触发次数,而是控制事件处理函数的执行来减少实际逻辑处理过程,从而提高浏览器性能。

一个去抖的场景是:在搜索框中根据用户输入的文本搜索关联的内容并下拉展示,由于input是一个触发频率很高的事件,一般需要等到用户停止输出文本一段时间后才开始请求接口查询数据。

先来实现最原始的业务逻辑

import{ref,watch}from"vue"functiondebounce(cb,delay=100){lettimerreturnfunction(){clearTimeout(timer)letargs=arguments,context=thistimer=setTimeout(()=>{cb.apply(context,args)},delay)}}exportfunctionuseAssociateSearch(){constkeyword=ref("")constsearch=()=>{console.log("search...",keyword.value)//mock请求接口获取数据}//watch(keyword,search)//原始逻辑,每次变化都请求watch(keyword,debounce(search,1000))//去抖,停止操作1秒后再请求return{keyword}}

然后在视图中引入

<script>import{useAssociateSearch}from"../useDebounce";exportdefault{name:"useDebounce",setup(){const{keyword}=useAssociateSearch()return{keyword}}}</script>

与useApi​同理,我们可以将这个debounce的逻辑抽象出来,,封装成一个通用的useDebounce

实现useDebounce

貌似不需要我们再额外编写任何代码,直接将debounce​方法重命名为useDebounce即可,为了凑字数,我们还是改装一下,同时增加cancel方法

exportfunctionuseDebounce(cb,delay=100){consttimer=ref(null)lethandler=function(){clearTimeout(timer.value)letargs=arguments,context=thistimer.value=setTimeout(()=>{cb.apply(context,args)},delay)}constcancel=()=>{clearTimeout(timer)timer.value=null}return{handler,cancel}}
实现useThrottle

节流与去抖的封装方式基本相同,只要知道throttle的实现就可以了。

exportfunctionuseThrottle(cb,duration=100){letstart=+newDate()returnfunction(){letargs=argumentsletcontext=thisletnow=+newDate()if(now-start>=duration){cb.apply(context,args)start=now}}}
思考

从去抖/节流的形式可以看出,某些hook与我们之前的工具函数并没有十分明显的边界。是将所有代码统一hook化,还是保留原来引入工具函数的风格,这是一个需要思考和实践的问题

小结

本文主要展示了几种Hook的封装思路和简单实现

useRequest用于统一管理网络请求相关状态,而无需在每次网络请求中重复处理loading、error等逻辑useEventBus​实现了在组件卸载时自动取消当前组件监听的事件,无需重复编写onUnmounted代码,这个思路也可以用于DOM事件、定时器、网络请求等注册和取消useModel​实现了在多个组件共享同一个hook状态,展示了一种除vuex、provide/inject函数之外跨组件共享数据的方案useReducer​利用hook实现了一个简易版的redux​,并且利用useModel实现了全局的storeuseDebounce与useThrottle,实现了去抖和节流,并思考了hook化的代码风格与常规的util代码风格,以及是否有必要将所有的东西都hook化

本文全部代码均放在github[3]上面了,由于只是展示思路,了解组合式API的灵活用法,因此代码写的十分简陋,如果发现错误或有其他想法,欢迎指定并一起讨论。

参考

值得推荐的十大React Hook 库[4]awesome-react-hooks[5]hooks-guide[6]ahooks[7]crooks[8]​

关键词: 这是一个 因此可以 可以使用 比较简单 触发事件

相关新闻

Copyright 2015-2020   三好网  版权所有