Эта статья синхронизирована в личном блогеshymean.comвверх, добро пожаловать, чтобы следовать
В этой статье рассказывается, как использовать Vue3 для инкапсуляции некоторых полезных API-интерфейсов композиции, включая предысторию, идеи реализации и некоторые мысли.
Что касается моих собственных чувств,Hookа такжеComposition APIКонцепция очень похожа, на самом деле большинство хуков, доступных в React, можно снова реализовать с помощью Vue3.
Ссылаться на
Для удобства правописания следующее содержимое заменено на HookComposition API. Соответствующие коды размещены вgithubнад.
useRequest
задний план
Перехватчики легко использовать для инкапсуляции набора операций с данными, таких как следующие: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
}
}
Он инкапсулирует логику получения ресурсов и обработки состояния загрузки, что, кажется, соответствует нашим потребностям.
Недостаток в том, что для другого ресурса вроде бы нужно писать аналогичный код шаблона, поэтому этот кусок кода можно абстрагировать и инкапсулировать вuseApiметод
выполнить
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返回的结果直接复制给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
}
}
Обратите внимание, что это очень общий метод, если предположить, что сейчас нужно инкапсулировать другие запросы, это также очень удобно для обработки, и нет необходимости снова и снова обрабатывать флаги, такие как загрузка и ошибка.
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) => {
// 封装请求返回值
return fetchUserList(params).then(res => {
console.log(res)
if (res.code === 200) {
return res.data
}
return []
})
})
// ...
}
думать
Обработка сетевых запросов — очень распространенная проблема во фронтенде.Обработка загрузки, обработки ошибок и т. д., перечисленных выше, также может включать debounce, throttling, polling и т. д., а также отмену незавершенных запросов при выходе со страницы, и т. д., доступны вuseRequestдополнительно инкапсулированный в
useEventBus
EventBus более полезен в сценарии уведомления о событиях между несколькими компонентами.Прослушивая события и инициируя события, подписчик и издатель могут быть разделены, и относительно просто реализовать обычную шину событий.
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)
})
}
}
Легко понять, что мы слушаем события, когда компонент инициализируется, и инициируем события, когда он взаимодействует с ним; но легко забыть, что нам также нужно отменить регистрацию события, когда компонент выгружается, освобождая связанные ресурсы.
Таким образом, можно инкапсулироватьuseEventBusинтерфейс, который обрабатывает эту логику единообразно
выполнить
Если он хочет отменить регистрацию, когда компонент разгружает связанные события, реализация идеи проста: до тех пор, пока на момент регистрации (onа такжеonce) для сбора связанных событий и обработчиков, а затемonUnmountedотменить, когда (off) для сбора этих событий
Таким образом, мы можем перехватить метод регистрации события и создать дополнительныйeventMapИспользуется для сбора событий, зарегистрированных в текущем интерфейсе.
// 事件总线,全局单例
const bus = new EventBus()
export default function useEventBus() {
let instance = {
eventMap: new Map(),
// 复用eventBus事件收集相关逻辑
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()
// 劫持两个监听方法,收集当前组件对应的事件
const on = (key, cb) => {
instance.on(key, cb)
bus.on(key, cb)
}
const once = (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
Ссылаться на:
задний план
После освоения Hook (или Composition API) я чувствую, что все можно зацепить, и мне всегда хочется инкапсулировать данные и методы для манипулирования этой кучей данных, например следующий счетчик
function useCounter() {
const count = ref(0)
const decrement = () => {
count.value--
}
const increment = () => {
count.value++
}
return {
count,
decrement,
increment
}
}
этоuseCounterПредоставляет данные и методы, такие как получение счетчика текущего значения, увеличение приращения значения и уменьшение приращения значения, а затем вы можете с радостью реализовать счетчик в каждом компоненте.
В некоторых сценариях мы хотим, чтобы несколько компонентов совместно использовали один и тот же счетчик, а не собственный независимый счетчик каждого компонента.
Одним из случаев является использование глобального инструмента управления состоянием, такого как vuex, а затем изменениеuseCounterреализация
import {createStore} from 'vuex'
const store = createStore({
state: {
count: 0
},
mutations: {
setCount(state, payload) {
state.count = payload
}
}
})
затем повторно реализовать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
}
}
Очевидно, сейчасuseCounter2просто хранитьstateа такжеmutationsТого же эффекта можно добиться, используя store непосредственно в компоненте, и инкапсуляция становится бессмысленной, к тому же, если vuex-зависимость добавляется в проект только для этой функции, это очень громоздко.
Основываясь на этих вопросах, мы можем использоватьuseModelДля достижения необходимости повторного использования состояния ловушки
выполнить
Вся идея относительно проста: использование карты для сохранения состояния хука.
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)
}
// 在多个组件调用
const {count, decrement, increment} = useCounter3()
// ...
const {count, decrement, increment} = useCounter3()
Таким образом, каждый раз, когда вы звонитеuseCounter3Когда одно и то же состояние возвращается, реализуется совместное использование состояния ловушки между несколькими компонентами.
думать
userModelобеспечиваетvuexа такжеprovide()/inject()В дополнение к идее совместного использования состояния данных, он может гибко управлять данными и манипулировать данными, не объединяя все состояния вместе или по модулям.
Недостатком является то, что при неиспользованииuseModelПри упаковке,useCounterЭто обычный хук.С точки зрения последующего обслуживания нам трудно судить, является ли состояние глобально общими данными или локальными данными.
Итак, используяuseModelИмея дело с общим состоянием хуков, также необходимо тщательно продумать, подходит оно или нет.
useReducer
Идею редукции можно просто резюмировать как
- Хранилище поддерживает состояние данных глобального состояния,
- Каждый компонент может использовать данные в состоянии по мере необходимости и прослушивать изменения в состоянии.
-
reducerПолучить действие и вернуть новое состояние, компонент может передатьdispatchПередать действие, чтобы вызвать редуктор - После обновления состояния уведомите соответствующие зависимости для обновления данных.
Мы даже можем использовать redux, что-то вроде
function reducer(state, action){
// 根据action进行处理
// 返回新的state
}
const initialState = {}
const {state, dispatch} = useReducer(reducer, initialState);
выполнить
С системой ответа данных Vue нам даже не нужно реализовывать какую-либо логику публикации и подписки.
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
}
}
затем реализоватьuseReduxОтветственный за прохождение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);
}
Мы хотим поддерживать глобальное хранилище, поэтому мы можем использовать вышеуказанноеuseModel
export function useRedux() {
return useModel(useStore);
}
Затем вы можете использовать его в компоненте
<template>
<div>
<button @click="dispatch({type:'decrement'})">-</button>
<span>{{ state.count }}</span>
<button @click="dispatch({type:'increment'})">+</button>
</div>
</template>
<script>
export default {
name: "useReducer",
setup() {
const {state, dispatch} = useStore()
return {
state,
dispatch
}
}
}
</script>
посмотри над намиuseModelПример не имеет значения, в основном раскрывая общийdispatchметод, поддерживать логику изменения состояния в редюсере, вместо того, чтобы поддерживать логику модификации данных в каждом useCounter
думать
Конечно, это очень простой редукс, включая промежуточное ПО,combineReducers,connectи другие методы реализованы, но он также показывает нам базовый процесс потока данных с редукцией.
useDebounce и useThrottle
задний план
Многие клиентские бизнес-сценарии должны иметь дело со сценариями регулирования или устранения дребезга.Функция регулирования и функция устранения дребезга сами по себе не сокращают количество триггеров событий, но контролируют выполнение функции обработки событий, чтобы сократить фактический процесс логической обработки, тем самым улучшение производительности браузера.
Сценарий устранения дребезга: поиск связанного контента в поле поиска на основе текста, введенного пользователем, и потяните вниз, чтобы отобразить его.Поскольку ввод — это событие с высокой частотой срабатывания, обычно необходимо дождаться, пока пользователь остановится вывод текста за период времени до запроса интерфейса данных.
Сначала реализуем самую примитивную бизнес-логику
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 请求接口获取数据
}
// watch(keyword, search) // 原始逻辑,每次变化都请求
watch(keyword, debounce(search, 1000)) // 去抖,停止操作1秒后再请求
return {
keyword
}
}
а затем импортировать его в представление
<template>
<div>
<input type="text" v-model="keyword">
</div>
</template>
<script>
import {useAssociateSearch} from "../useDebounce";
export default {
name: "useDebounce",
setup() {
const {keyword} = useAssociateSearch()
return {
keyword
}
}
}
</script>
а такжеuseApiТочно так же мы можем абстрагироваться от логики этого устранения дребезга и инкапсулировать ее в общийuseDebounce
Реализовать использованиеDebounce
Кажется, что нам не нужно писать никакого дополнительного кода, непосредственноdebounceметод переименован вuseDebounceВсе, чтобы восполнить количество слов, мы еще модифицируем его и заодно добавим метод отмены.
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
}
}
Реализовать использование дроссельной заслонки
Насколько вы знаете, дросселирование и устранение дребезга в основном упакованы одинаково.throttleможно реализовать.
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
}
}
}
думать
По форме debounce/throttle видно, что некоторые хуки не имеют очень четких границ с нашими предыдущими служебными функциями. Унифицировать ли весь код для перехвата или сохранить оригинальный стиль введения инструментальных функций — это вопрос, над которым нужно подумать и попрактиковаться.
резюме
В этой статье в основном показаны идеи упаковки и простая реализация нескольких хуков.
-
useRequestОн используется для управления соответствующим статусом сетевых запросов унифицированным образом, без повторения логики загрузки, ошибок и т. д. в каждом сетевом запросе. -
useEventBusСобытие, которое слушает текущий компонент, автоматически отменяется при выгрузке компонента, без повторения записиonUnmountedcode, эту идею также можно использовать для регистрации и отмены DOM-событий, таймеров, сетевых запросов и т. д. -
useModelРеализовано совместное использование одного и того же состояния хука в нескольких компонентах, показывая способ удаления vuex,provide/injectСценарии обмена данными между компонентами вне функций -
useReducerРеализована простая версия с использованием хуковreduxи использоватьuseModelРеализован глобальный магазин -
useDebounceа такжеuseThrottle, реализовал debounce и throttling, а также подумал о стиле перехваченного кода в сравнении со стилем кода обычного util, а также о том, нужно ли перехватывать все подряд.
Весь код в этой статье размещен вgithubКак упоминалось выше, поскольку это только для демонстрации идей и понимания гибкого использования комбинированного API, код очень прост. Если вы обнаружите ошибки или у вас есть другие идеи, пожалуйста, укажите и обсудите вместе.