Все composables называются с префиксом use. Это конвенция (не требование), которая:
// ✅ Правильно
export function useCounter() { ... }
export function useUserProfile() { ... }
export function useWindowSize() { ... }
// ❌ Неправильно (функция, не composable)
export function getCounter() { ... }
export function createUser() { ... }Рекомендуется возвращать объект с ref-свойствами (не reactive-объект):
// ✅ Рекомендуется: деструктурирование работает корректно
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
// Деструктурирование сохраняет реактивность:
const { count, increment } = useCounter()
// ❌ Проблема с reactive: деструктурирование ломает реактивность
export function useCounter(initial = 0) {
return reactive({ count: initial }) // не делайте так
}
const { count } = useCounter() // count — примитив, не реактивный!Composables можно использовать внутри других composables:
// useUserSettings.js — использует useStorage внутри
export function useUserSettings() {
const { data, save } = useStorage('user-settings')
const { locale, setLocale } = useI18n()
const settings = computed(() => ({
...data.value,
locale: locale.value,
}))
async function updateSettings(patch) {
await save({ ...data.value, ...patch })
}
return { settings, updateSettings, setLocale }
}export function useAsyncData(fetcher) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function execute(...args) {
loading.value = true
error.value = null
try {
data.value = await fetcher(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// Немедленный вызов при создании
execute()
return { data, loading, error, execute }
}Важнейший паттерн — всегда очищать подписки, таймеры, слушатели:
export function useEventListener(target, event, handler) {
onMounted(() => {
target.addEventListener(event, handler)
})
// Автоматическая очистка при уничтожении компонента
onUnmounted(() => {
target.removeEventListener(event, handler)
})
}
export function useInterval(callback, ms) {
let timer = null
onMounted(() => {
timer = setInterval(callback, ms)
})
onUnmounted(() => {
if (timer) clearInterval(timer) // утечка памяти предотвращена
})
}import { toRef, isRef } from 'vue'
// Composable принимает и ref, и обычное значение
export function useDouble(value) {
const valueRef = isRef(value) ? value : ref(value)
return computed(() => valueRef.value * 2)
}
// Оба варианта работают:
const double1 = useDouble(5)
const count = ref(10)
const double2 = useDouble(count) // реактивно к countsrc/
composables/
useAuth.js — авторизация
useForm.js — формы
useFetch.js — HTTP-запросы
useDebounce.js — дебаунс
index.js — реэкспорт всегоНабор composable-паттернов: async data, cleanup, composition — всё на чистом JS
// Реализуем ключевые паттерны composables без Vue-рантайма,
// используя замыкания как аналог реактивного состояния.
// --- Паттерн: useAsyncData ---
function useAsyncData(fetcher) {
let data = null
let loading = false
let error = null
const listeners = new Set()
function notify() {
listeners.forEach(fn => fn({ data, loading, error }))
}
const composable = {
subscribe(fn) {
listeners.add(fn)
fn({ data, loading, error }) // немедленный вызов с текущим состоянием
return () => listeners.delete(fn) // отписка
},
async execute(...args) {
loading = true
error = null
notify()
try {
data = await fetcher(...args)
} catch (e) {
error = e.message
} finally {
loading = false
notify()
}
},
getState() { return { data, loading, error } }
}
return composable
}
// --- Паттерн: useCompose (composable из composables) ---
function useFilteredData(allItems) {
let filter = ''
let sortKey = null
const listeners = new Set()
function getResult() {
let result = allItems.filter(item =>
!filter || JSON.stringify(item).toLowerCase().includes(filter.toLowerCase())
)
if (sortKey) {
result = [...result].sort((a, b) => a[sortKey] < b[sortKey] ? -1 : 1)
}
return result
}
return {
setFilter(f) { filter = f; listeners.forEach(fn => fn(getResult())) },
setSortKey(k) { sortKey = k; listeners.forEach(fn => fn(getResult())) },
subscribe(fn) {
listeners.add(fn)
fn(getResult())
return () => listeners.delete(fn)
},
}
}
// --- Паттерн: cleanup (имитация onUnmounted) ---
function createComponent(setup) {
const cleanups = []
const onUnmounted = (fn) => cleanups.push(fn)
const instance = setup({ onUnmounted })
return {
...instance,
destroy() {
console.log('[Component] destroying — вызываем cleanup...')
cleanups.forEach(fn => fn())
cleanups.length = 0
}
}
}
function useInterval(callback, ms, { onUnmounted }) {
const id = setInterval(callback, ms)
console.log(`[useInterval] запущен (id=${id})`)
onUnmounted(() => {
clearInterval(id)
console.log(`[useInterval] очищен (id=${id})`)
})
return { stop: () => clearInterval(id) }
}
// === Тест: useAsyncData ===
async function runAsyncTest() {
console.log('=== useAsyncData ===')
const userData = useAsyncData(async (id) => {
await new Promise(r => setTimeout(r, 30))
if (id === 0) throw new Error('Пользователь не найден')
return { id, name: `User #${id}`, email: `user${id}@example.com` }
})
const unsub = userData.subscribe(state => {
if (state.loading) console.log(' loading...')
if (state.data) console.log(' data:', state.data.name)
if (state.error) console.log(' error:', state.error)
})
await userData.execute(42)
await userData.execute(0) // ошибка
unsub()
await userData.execute(7) // не отобразится (отписались)
console.log('После отписки:', userData.getState().data?.name)
}
// === Тест: useFilteredData ===
function runFilterTest() {
console.log('\n=== useFilteredData ===')
const items = [
{ name: 'Apple', price: 100 },
{ name: 'Banana', price: 50 },
{ name: 'Cherry', price: 200 },
{ name: 'Date', price: 150 },
]
const filteredData = useFilteredData(items)
const unsub = filteredData.subscribe(result => {
console.log('Результат:', result.map(i => i.name))
})
filteredData.setFilter('a')
filteredData.setSortKey('price')
filteredData.setFilter('')
unsub()
}
// === Тест: cleanup ===
function runCleanupTest() {
console.log('\n=== Cleanup (onUnmounted) ===')
let ticks = 0
const comp = createComponent(({ onUnmounted }) => {
const { stop } = useInterval(() => ticks++, 10, { onUnmounted })
return { getTicks: () => ticks }
})
setTimeout(() => {
console.log('Тиков до destroy:', comp.getTicks())
comp.destroy()
// После destroy интервал остановлен
setTimeout(() => {
console.log('Тиков после destroy (не изменилось):', comp.getTicks())
}, 50)
}, 50)
}
runAsyncTest().then(runFilterTest).then(runCleanupTest)
Все composables называются с префиксом use. Это конвенция (не требование), которая:
// ✅ Правильно
export function useCounter() { ... }
export function useUserProfile() { ... }
export function useWindowSize() { ... }
// ❌ Неправильно (функция, не composable)
export function getCounter() { ... }
export function createUser() { ... }Рекомендуется возвращать объект с ref-свойствами (не reactive-объект):
// ✅ Рекомендуется: деструктурирование работает корректно
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
// Деструктурирование сохраняет реактивность:
const { count, increment } = useCounter()
// ❌ Проблема с reactive: деструктурирование ломает реактивность
export function useCounter(initial = 0) {
return reactive({ count: initial }) // не делайте так
}
const { count } = useCounter() // count — примитив, не реактивный!Composables можно использовать внутри других composables:
// useUserSettings.js — использует useStorage внутри
export function useUserSettings() {
const { data, save } = useStorage('user-settings')
const { locale, setLocale } = useI18n()
const settings = computed(() => ({
...data.value,
locale: locale.value,
}))
async function updateSettings(patch) {
await save({ ...data.value, ...patch })
}
return { settings, updateSettings, setLocale }
}export function useAsyncData(fetcher) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function execute(...args) {
loading.value = true
error.value = null
try {
data.value = await fetcher(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// Немедленный вызов при создании
execute()
return { data, loading, error, execute }
}Важнейший паттерн — всегда очищать подписки, таймеры, слушатели:
export function useEventListener(target, event, handler) {
onMounted(() => {
target.addEventListener(event, handler)
})
// Автоматическая очистка при уничтожении компонента
onUnmounted(() => {
target.removeEventListener(event, handler)
})
}
export function useInterval(callback, ms) {
let timer = null
onMounted(() => {
timer = setInterval(callback, ms)
})
onUnmounted(() => {
if (timer) clearInterval(timer) // утечка памяти предотвращена
})
}import { toRef, isRef } from 'vue'
// Composable принимает и ref, и обычное значение
export function useDouble(value) {
const valueRef = isRef(value) ? value : ref(value)
return computed(() => valueRef.value * 2)
}
// Оба варианта работают:
const double1 = useDouble(5)
const count = ref(10)
const double2 = useDouble(count) // реактивно к countsrc/
composables/
useAuth.js — авторизация
useForm.js — формы
useFetch.js — HTTP-запросы
useDebounce.js — дебаунс
index.js — реэкспорт всегоНабор composable-паттернов: async data, cleanup, composition — всё на чистом JS
// Реализуем ключевые паттерны composables без Vue-рантайма,
// используя замыкания как аналог реактивного состояния.
// --- Паттерн: useAsyncData ---
function useAsyncData(fetcher) {
let data = null
let loading = false
let error = null
const listeners = new Set()
function notify() {
listeners.forEach(fn => fn({ data, loading, error }))
}
const composable = {
subscribe(fn) {
listeners.add(fn)
fn({ data, loading, error }) // немедленный вызов с текущим состоянием
return () => listeners.delete(fn) // отписка
},
async execute(...args) {
loading = true
error = null
notify()
try {
data = await fetcher(...args)
} catch (e) {
error = e.message
} finally {
loading = false
notify()
}
},
getState() { return { data, loading, error } }
}
return composable
}
// --- Паттерн: useCompose (composable из composables) ---
function useFilteredData(allItems) {
let filter = ''
let sortKey = null
const listeners = new Set()
function getResult() {
let result = allItems.filter(item =>
!filter || JSON.stringify(item).toLowerCase().includes(filter.toLowerCase())
)
if (sortKey) {
result = [...result].sort((a, b) => a[sortKey] < b[sortKey] ? -1 : 1)
}
return result
}
return {
setFilter(f) { filter = f; listeners.forEach(fn => fn(getResult())) },
setSortKey(k) { sortKey = k; listeners.forEach(fn => fn(getResult())) },
subscribe(fn) {
listeners.add(fn)
fn(getResult())
return () => listeners.delete(fn)
},
}
}
// --- Паттерн: cleanup (имитация onUnmounted) ---
function createComponent(setup) {
const cleanups = []
const onUnmounted = (fn) => cleanups.push(fn)
const instance = setup({ onUnmounted })
return {
...instance,
destroy() {
console.log('[Component] destroying — вызываем cleanup...')
cleanups.forEach(fn => fn())
cleanups.length = 0
}
}
}
function useInterval(callback, ms, { onUnmounted }) {
const id = setInterval(callback, ms)
console.log(`[useInterval] запущен (id=${id})`)
onUnmounted(() => {
clearInterval(id)
console.log(`[useInterval] очищен (id=${id})`)
})
return { stop: () => clearInterval(id) }
}
// === Тест: useAsyncData ===
async function runAsyncTest() {
console.log('=== useAsyncData ===')
const userData = useAsyncData(async (id) => {
await new Promise(r => setTimeout(r, 30))
if (id === 0) throw new Error('Пользователь не найден')
return { id, name: `User #${id}`, email: `user${id}@example.com` }
})
const unsub = userData.subscribe(state => {
if (state.loading) console.log(' loading...')
if (state.data) console.log(' data:', state.data.name)
if (state.error) console.log(' error:', state.error)
})
await userData.execute(42)
await userData.execute(0) // ошибка
unsub()
await userData.execute(7) // не отобразится (отписались)
console.log('После отписки:', userData.getState().data?.name)
}
// === Тест: useFilteredData ===
function runFilterTest() {
console.log('\n=== useFilteredData ===')
const items = [
{ name: 'Apple', price: 100 },
{ name: 'Banana', price: 50 },
{ name: 'Cherry', price: 200 },
{ name: 'Date', price: 150 },
]
const filteredData = useFilteredData(items)
const unsub = filteredData.subscribe(result => {
console.log('Результат:', result.map(i => i.name))
})
filteredData.setFilter('a')
filteredData.setSortKey('price')
filteredData.setFilter('')
unsub()
}
// === Тест: cleanup ===
function runCleanupTest() {
console.log('\n=== Cleanup (onUnmounted) ===')
let ticks = 0
const comp = createComponent(({ onUnmounted }) => {
const { stop } = useInterval(() => ticks++, 10, { onUnmounted })
return { getTicks: () => ticks }
})
setTimeout(() => {
console.log('Тиков до destroy:', comp.getTicks())
comp.destroy()
// После destroy интервал остановлен
setTimeout(() => {
console.log('Тиков после destroy (не изменилось):', comp.getTicks())
}, 50)
}, 50)
}
runAsyncTest().then(runFilterTest).then(runCleanupTest)
Реализуй функцию `useForm(initialValues, validators)`. Аргументы: initialValues — объект начальных значений полей; validators — объект с функциями-валидаторами `{ fieldName: (value) => errorString | null }`. Возвращает: `values` (объект-getter/setter через метод get/set), `errors` (объект с ошибками), `validate()` — запускает все валидаторы и обновляет errors, возвращает true если всех ошибок нет, `reset()` — сбрасывает values к initialValues и очищает errors, `isDirty()` — возвращает true если хоть одно поле изменилось от начального значения.
В constructor-замыкании объяви: let _values = { ...initialValues }, let _errors = {}. В validate() используй: let isValid = true; for (const field in validators) { const err = validators[field](_values[field]); _errors[field] = err || null; if (err) isValid = false; } return isValid. В isDirty(): return Object.keys(initialValues).some(k => _values[k] !== initialValues[k]).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке