新聞中心
本文將介紹如何使用Vue3來(lái)封裝一些比較有用的組合API,主要包括背景、實(shí)現(xiàn)思路以及一些思考。

成都創(chuàng)新互聯(lián)公司主營(yíng)無(wú)極網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,APP應(yīng)用開發(fā),無(wú)極h5小程序開發(fā)搭建,無(wú)極網(wǎng)站營(yíng)銷推廣歡迎無(wú)極等地區(qū)企業(yè)咨詢
就我自己的感覺而言,Hook與Composition API概念是很類似的,事實(shí)上在React大部分可用的Hook都可以使用Vue3再實(shí)現(xiàn)一遍。
為了拼寫方便,下文內(nèi)容均使用Hook代替Composition API。相關(guān)代碼均放在 ??github??上面。
useRequest
背景
使用hook來(lái)封裝一組數(shù)據(jù)的操作是很容易的,例如下面的useBook:
import {ref, onMounted} from 'vue'
function fetchBookList() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([1, 2, 3])
}, 1000)
})
}
export function useBook() {
const list = ref([])
const loading = ref(false)
const getList = async () => {
loading.value = true
const data = await fetchBookList({page: 1})
loading.value = false
list.value = data
}
onMounted(() => {
getList()
})
return {
list,
loading,
getList
}
}其中封裝了獲取資源、處理加載狀態(tài)等邏輯,看起來(lái)貌似能滿足我們的需求了。
缺點(diǎn)在于對(duì)應(yīng)另外一個(gè)資源而言,我們貌似還需要寫類似的模板代碼,因此可以將這一堆代碼進(jìn)行抽象,封裝成??useApi??方法。
實(shí)現(xiàn)
function useApi(api) {
const loading = ref(false)
const result = ref(null)
const error = ref(null)
const fetchResource = (params) => {
loading.value = true
return api(params).then(data => {
// 按照約定,api返回的結(jié)果直接復(fù)制給result
result.value = data
}).catch(e => {
error.value = e
}).finally(() => {
loading.value = false
})
}
return {
loading,
error,
result,
fetchResource
}
}然后修改上面的useBook方法:
function useBook2() {
const {loading, error, result, fetchResource,} = useApi(fetchBookList)
onMounted(() => {
fetchResource({page: 1})
})
return {
loading,
error,
list: result
}
}注意這是一個(gè)非常通用的方法,假設(shè)現(xiàn)在需求封裝其他的請(qǐng)求,處理起來(lái)也是非常方便的,不需要再一遍遍地處理loading和error等標(biāo)志量:
function fetchUserList() {
return new Promise((resolve) => {
setTimeout(() => {
const payload = {
code: 200,
data: [11, 22, 33],
msg: 'success'
}
resolve(payload)
}, 1000)
})
}
function useUser() {
const {loading, error, result, fetchResource,} = useApi((params) => {
// 封裝請(qǐng)求返回值
return fetchUserList(params).then(res => {
console.log(res)
if (res.code === 200) {
return res.data
}
return []
})
})
// ...
}
思考
處理網(wǎng)絡(luò)請(qǐng)求是前端工作中十分常見的問(wèn)題,處理上面列舉到的加載、錯(cuò)誤處理等,還可以包含去抖、節(jié)流、輪詢等各種情況,還有離開頁(yè)面時(shí)取消未完成的請(qǐng)求等,都是可以在useRequest中進(jìn)一步封裝的。
2. useEventBus
EventBus在多個(gè)組件之間進(jìn)行事件通知的場(chǎng)景下還是比較有用的,通過(guò)監(jiān)聽事件和觸發(fā)事件,可以在訂閱者和發(fā)布者之間解耦,實(shí)現(xiàn)一個(gè)常規(guī)的eventBus也比較簡(jiǎn)單:
class EventBus {
constructor() {
this.eventMap = new Map()
}
on(key, cb) {
let handlers = this.eventMap.get(key)
if (!handlers) {
handlers = []
}
handlers.push(cb)
this.eventMap.set(key, handlers)
}
off(key, cb) {
const handlers = this.eventMap.get(key)
if (!handlers) return
if (cb) {
const idx = handlers.indexOf(cb)
idx > -1 && handlers.splice(idx, 1)
this.eventMap.set(key, handlers)
} else {
this.eventMap.delete(key)
}
}
once(key, cb) {
const handlers = [(payload) => {
cb(payload)
this.off(key)
}]
this.eventMap.set(key, handlers)
}
emit(key, payload) {
const handlers = this.eventMap.get(key)
if (!Array.isArray(handlers)) return
handlers.forEach(handler => {
handler(payload)
})
}
}我們?cè)诮M件初始化時(shí)監(jiān)聽事件,在交互時(shí)觸發(fā)事件,這些是很容易理解的;但很容易被遺忘的是,我們還需要在組件卸載時(shí)取消事件注冊(cè),釋放相關(guān)的資源。
因此可以封裝一個(gè)useEventBus接口,統(tǒng)一處理這些邏輯。
實(shí)現(xiàn)
既然要在組件卸載時(shí)取消注冊(cè)的相關(guān)事件,簡(jiǎn)單的實(shí)現(xiàn)思路是:只要在注冊(cè)時(shí)(on和once)收集相關(guān)的事件和處理函數(shù),然后在onUnmounted的時(shí)候取消(off)收集到的這些事件即可。
因此我們可以劫持事件注冊(cè)的方法,同時(shí)額外創(chuàng)建一個(gè)eventMap用于收集使用當(dāng)前接口注冊(cè)的事件:
// 事件總線,全局單例
const bus = new EventBus()
export default function useEventBus() {
let instance = {
eventMap: new Map(),
// 復(fù)用eventBus事件收集相關(guān)邏輯
on: bus.on,
once: bus.once,
// 清空eventMap
clear() {
this.eventMap.forEach((list, key) => {
list.forEach(cb => {
bus.off(key, cb)
})
})
eventMap.clear()
}
}
let eventMap = new Map()
// 劫持兩個(gè)監(jiān)聽方法,收集當(dāng)前組件對(duì)應(yīng)的事件
const on = (key, cb) => {
instance.on(key, cb)
bus.on(key, cb)
}
const once = (key, cb) => {
instance.once(key, cb)
bus.once(key, cb)
}
// 組件卸載時(shí)取消相關(guān)的事件
onUnmounted(() => {
instance.clear()
})
return {
on,
once,
off: bus.off.bind(bus),
emit: bus.emit.bind(bus)
}
}
這樣,當(dāng)組價(jià)卸載時(shí)也會(huì)通過(guò)instance.clear移除該組件注冊(cè)的相關(guān)事件,比起手動(dòng)在每個(gè)組件onUnmounted時(shí)手動(dòng)取消要方便很多。
思考
這個(gè)思路可以運(yùn)用在很多需要在組件卸載時(shí)執(zhí)行清理操作的邏輯,比如:
- DOM事件注冊(cè)addEventListener和removeEventListener
- 計(jì)時(shí)器setTimeout和clearTimeout
- 網(wǎng)絡(luò)請(qǐng)求request和abort
從這個(gè)封裝也可以看見組合API一個(gè)非常明顯的優(yōu)勢(shì):盡可能地抽象公共邏輯,而無(wú)需關(guān)注每個(gè)組件具體的細(xì)節(jié)。
3. useModel
參考:??hox源碼??
背景
當(dāng)掌握了Hook(或者Composition API)之后,感覺萬(wàn)物皆可hook,總是想把數(shù)據(jù)和操作這堆數(shù)據(jù)的方法封裝在一起,比如下面的計(jì)數(shù)器:
function useCounter() {
const count = ref(0)
const decrement = () => {
count.value--
}
const increment = () => {
count.value++
}
return {
count,
decrement,
increment
}
}這個(gè)useCounter暴露了獲取當(dāng)前數(shù)值count、增加數(shù)值decrement和減少數(shù)值increment等數(shù)據(jù)和方法,然后就可以在各個(gè)組件中愉快地實(shí)現(xiàn)計(jì)數(shù)器了。
在某些場(chǎng)景下我們希望多個(gè)組件可以共享同一個(gè)計(jì)數(shù)器,而不是每個(gè)組件自己獨(dú)立的計(jì)數(shù)器。
一種情況是使用諸如vuex等全局狀態(tài)管理工具,然后修改useCounter的實(shí)現(xiàn):
import {createStore} from 'vuex'
const store = createStore({
state: {
count: 0
},
mutations: {
setCount(state, payload) {
state.count = payload
}
}
})然后重新實(shí)現(xiàn)useCounter:
export function useCounter2() {
const count = computed(() => {
return store.state.count
})
const decrement = () => {
store.commit('setCount', count.value + 1)
}
const increment = () => {
store.commit('setCount', count.value + 1)
}
return {
count,
decrement,
increment
}
}很顯然,現(xiàn)在的useCounter2?僅僅只是store的state與mutations的封裝,直接在組件中使用store也可以達(dá)到相同的效果,封裝就變得意義不大;此外,如果單單只是為了這個(gè)功能就為項(xiàng)目增加了vuex依賴,顯得十分笨重。
基于這些問(wèn)題,我們可以使用一個(gè)useModel來(lái)實(shí)現(xiàn)復(fù)用某個(gè)鉤子狀態(tài)的需求。
實(shí)現(xiàn)
整個(gè)思路也比較簡(jiǎn)單,使用一個(gè)Map來(lái)保存某個(gè)hook的狀態(tài):
const map = new WeakMap()
export default function useModel(hook) {
if (!map.get(hook)) {
let ans = hook()
map.set(hook, ans)
}
return map.get(hook)
}
然后包裝一下useCounter:
export function useCounter3() {
return useModel(useCounter)
}
// 在多個(gè)組件調(diào)用
const {count, decrement, increment} = useCounter3()
// ...
const {count, decrement, increment} = useCounter3()這樣,在每次調(diào)用??useCounter3??時(shí),都返回的是同一個(gè)狀態(tài),也就實(shí)現(xiàn)了多個(gè)組件之間的hook狀態(tài)共享。
思考
userModel?提供了一種除vuex和provide()/inject()之外共享數(shù)據(jù)狀態(tài)的思路,并且可以很靈活的管理數(shù)據(jù)與操作數(shù)據(jù)的方案,而無(wú)需將所有state放在一起或者模塊下面。
缺點(diǎn)在于,當(dāng)不使用useModel?包裝時(shí),useCounter就是一個(gè)普通的hook,后期維護(hù)而言,我們很難判斷某個(gè)狀態(tài)到底是全局共享的數(shù)據(jù)還是局部的數(shù)據(jù)。
因此在使用useModel處理hook的共享狀態(tài)時(shí),還要要慎重考慮一下到底合不合適。
4. useReducer
redux的思想可以簡(jiǎn)單概括為:
- store維護(hù)全局的state數(shù)據(jù)狀態(tài),
- 各個(gè)組件可以按需使用state中的數(shù)據(jù),并監(jiān)聽state的變化
- reducer?接收action并返回新的state,組件可以通過(guò)dispatch傳遞action觸發(fā)reducer
- state更新后,通知相關(guān)依賴更新數(shù)據(jù)
我們甚至可以將redux的使用hook化,類似于:
function reducer(state, action){
// 根據(jù)action進(jìn)行處理
// 返回新的state
}
const initialState = {}
const {state, dispatch} = useReducer(reducer, initialState);
實(shí)現(xiàn)
借助于Vue的數(shù)據(jù)響應(yīng)系統(tǒng),我們甚至不需要實(shí)現(xiàn)任何發(fā)布和訂閱邏輯:
import {ref} from 'vue'
export default function useReducer(reducer, initialState = {}) {
const state = ref(initialState)
// 約定action格式為 {type:string, payload: any}
const dispatch = (action) => {
state.value = reducer(state.value, action)
}
return {
state,
dispatch
}
}然后實(shí)現(xiàn)一個(gè)useRedux?負(fù)責(zé)傳遞reducer和action:
import useReducer from './index'
function reducer(state, action) {
switch (action.type) {
case "reset":
return initialState;
case "increment":
return {count: state.count + 1};
case "decrement":
return {count: state.count - 1};
}
}
function useStore() {
return useReducer(reducer, initialState);
}
我們希望是維護(hù)一個(gè)全局的store,因此可以使用上面的useModel:
export function useRedux() {
return useModel(useStore);
}然后就可以在組件中使用了:
{{ state.count }}
看起來(lái)跟我們上面??useModel???的例子并沒有什么區(qū)別,主要是暴露了通用的??dispatch??方法,在reducer處維護(hù)狀態(tài)變化的邏輯,而不是在每個(gè)useCounter中自己維護(hù)修改數(shù)據(jù)的邏輯。
思考
當(dāng)然這個(gè)redux是非常簡(jiǎn)陋的,包括中間件、??combineReducers???、??connect??等方法均為實(shí)現(xiàn),但也為我們展示了一個(gè)最基本的redux數(shù)據(jù)流轉(zhuǎn)過(guò)程。
5. useDebounce與useThrottle
背景
前端很多業(yè)務(wù)場(chǎng)景下都需要處理節(jié)流或去抖的場(chǎng)景,節(jié)流函數(shù)和去抖函數(shù)本身沒有減少事件的觸發(fā)次數(shù),而是控制事件處理函數(shù)的執(zhí)行來(lái)減少實(shí)際邏輯處理過(guò)程,從而提高瀏覽器性能。
一個(gè)去抖的場(chǎng)景是:在搜索框中根據(jù)用戶輸入的文本搜索關(guān)聯(lián)的內(nèi)容并下拉展示,由于input是一個(gè)觸發(fā)頻率很高的事件,一般需要等到用戶停止輸出文本一段時(shí)間后才開始請(qǐng)求接口查詢數(shù)據(jù)。
先來(lái)實(shí)現(xiàn)最原始的業(yè)務(wù)邏輯:
import {ref, watch} from 'vue'
function debounce(cb, delay = 100) {
let timer
return function () {
clearTimeout(timer)
let args = arguments,
context = this
timer = setTimeout(() => {
cb.apply(context, args)
}, delay)
}
}
export function useAssociateSearch() {
const keyword = ref('')
const search = () => {
console.log('search...', keyword.value)
// mock 請(qǐng)求接口獲取數(shù)據(jù)
}
// watch(keyword, search) // 原始邏輯,每次變化都請(qǐng)求
watch(keyword, debounce(search, 1000)) // 去抖,停止操作1秒后再請(qǐng)求
return {
keyword
}
}然后在視圖中引入:
與useApi?同理,我們可以將這個(gè)debounce的邏輯抽象出來(lái),,封裝成一個(gè)通用的useDebounce。
實(shí)現(xiàn)useDebounce
貌似不需要我們?cè)兕~外編寫任何代碼,直接將debounce?方法重命名為useDebounce即可,為了湊字?jǐn)?shù),我們還是改裝一下,同時(shí)增加cancel方法:
export function useDebounce(cb, delay = 100) {
const timer = ref(null)
let handler = function () {
clearTimeout(timer.value)
let args = arguments,
context = this
timer.value = setTimeout(() => {
cb.apply(context, args)
}, delay)
}
const cancel = () => {
clearTimeout(timer)
timer.value = null
}
return {
handler,
cancel
}
}
實(shí)現(xiàn)useThrottle
節(jié)流與去抖的封裝方式基本相同,只要知道??throttle??的實(shí)現(xiàn)就可以了。
export function useThrottle(cb, duration = 100) {
let start = +new Date()
return function () {
let args = arguments
let context = this
let now = +new Date()
if (now - start >= duration) {
cb.apply(context, args)
start = now
}
}
}
思考
從去抖/節(jié)流的形式可以看出,某些hook與我們之前的工具函數(shù)并沒有十分明顯的邊界。是將所有代碼統(tǒng)一hook化,還是保留原來(lái)引入工具函數(shù)的風(fēng)格,這是一個(gè)需要思考和實(shí)踐的問(wèn)題。
小結(jié)
本文主要展示了幾種Hook的封裝思路和簡(jiǎn)單實(shí)現(xiàn):
- useRequest用于統(tǒng)一管理網(wǎng)絡(luò)請(qǐng)求相關(guān)狀態(tài),而無(wú)需在每次網(wǎng)絡(luò)請(qǐng)求中重復(fù)處理loading、error等邏輯
- useEventBus?實(shí)現(xiàn)了在組件卸載時(shí)自動(dòng)取消當(dāng)前組件監(jiān)聽的事件,無(wú)需重復(fù)編寫onUnmounted代碼,這個(gè)思路也可以用于DOM事件、定時(shí)器、網(wǎng)絡(luò)請(qǐng)求等注冊(cè)和取消
- useModel?實(shí)現(xiàn)了在多個(gè)組件共享同一個(gè)hook狀態(tài),展示了一種除vuex、provide/inject函數(shù)之外跨組件共享數(shù)據(jù)的方案
- useReducer?利用hook實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的redux?,并且利用useModel實(shí)現(xiàn)了全局的store
- useDebounce與useThrottle,實(shí)現(xiàn)了去抖和節(jié)流,并思考了hook化的代碼風(fēng)格與常規(guī)的util代碼風(fēng)格,以及是否有必要將所有的東西都hook化
本文全部代碼均放在??github??上面了,由于只是展示思路,了解組合式API的靈活用法,因此代碼寫的十分簡(jiǎn)陋,如果發(fā)現(xiàn)錯(cuò)誤或有其他想法,歡迎指定并一起討論。
本文題目:封裝幾個(gè)有用的Vue3組合式API
文章起源:http://www.dlmjj.cn/article/dhhphso.html


咨詢
建站咨詢
