Composable — это функция с именем use*, которая инкапсулирует **реактивную логику** и позволяет переиспользовать её в нескольких компонентах. Composables — главный способ переиспользования кода в Vue 3.
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// В компоненте:
const { x, y } = useMousePosition()import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timer = null
watch(value, (newVal) => {
clearTimeout(timer)
timer = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}
// Применение — поиск с задержкой
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
watch(debouncedQuery, (query) => {
if (query) fetchResults(query) // запрос только когда пользователь перестал печатать
})import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
fetch(url)
.then(r => r.json())
.then(json => { data.value = json })
.catch(err => { error.value = err.message })
.finally(() => { loading.value = false })
return { data, error, loading }
}
// В компоненте:
const { data: users, loading, error } = useFetch('/api/users')Mixins — старый способ переиспользования в Vue 2. Они имеют серьёзные недостатки:
| Проблема | Mixins | Composables |
|-------------------------|---------------------------------|--------------------------|
| Источник свойств | Непонятно — откуда пришло? | Явно из const { x } = useX() |
| Конфликты имён | Перезапись без предупреждения | Переименование через деструктуризацию |
| Переиспользование | Нельзя передавать параметры | Полноценные аргументы |
| Отладка | Сложно трассировать | Обычная функция |
// MIXINS — проблема: откуда взялось searchQuery?
export default {
mixins: [SearchMixin, UserMixin, PaginationMixin],
mounted() {
// searchQuery из SearchMixin или UserMixin? Непонятно!
this.searchQuery = ''
}
}
// COMPOSABLES — явно и прозрачно
export default {
setup() {
const { query: searchQuery, results } = useSearch()
const { currentUser } = useUser()
const { page, totalPages } = usePagination(results)
// всё читаемо и явно
return { searchQuery, results, currentUser, page, totalPages }
}
}// Composable с опциями
function useInterval(callback, { delay = 1000, immediate = false } = {}) {
let id = null
onMounted(() => {
if (immediate) callback()
id = setInterval(callback, delay)
})
onUnmounted(() => clearInterval(id))
return {
pause: () => clearInterval(id),
resume: () => { id = setInterval(callback, delay) }
}
}
// Composable возвращающий composable
function useAsyncState(asyncFn, defaultValue) {
const state = ref(defaultValue)
const loading = ref(false)
const error = ref(null)
async function execute(...args) {
loading.value = true
error.value = null
try {
state.value = await asyncFn(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return { state, loading, error, execute }
}useDebounce и useFetch как чистые JS функции без Vue
// useDebounce — задержка выполнения функции
function useDebounce(fn, delay = 300) {
let timer = null
const debounced = function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
// Дополнительные методы
debounced.cancel = () => clearTimeout(timer)
debounced.flush = function(...args) {
clearTimeout(timer)
fn.apply(this, args)
}
return debounced
}
// Использование useDebounce
const handleSearch = useDebounce((query) => {
console.log(`Поиск: "${query}"`)
}, 300)
// Имитируем быстрый ввод пользователя
handleSearch('J')
handleSearch('Ja')
handleSearch('Jav')
handleSearch('Java')
handleSearch('JavaS')
// Только последний вызов выполнится через 300мс (в реальном браузере)
// В Node.js нет таймаута между вызовами — демонстрируем концепцию
console.log('(запросы задебаунсированы, выполнится только последний)')
// useFetch — загрузка данных с состоянием
function useFetch(fetchFn) {
const state = {
data: null,
loading: false,
error: null,
}
let abortController = null
async function execute(...args) {
// Отменяем предыдущий запрос если он ещё идёт
if (abortController) abortController.abort()
abortController = { aborted: false, abort() { this.aborted = true } }
const currentController = abortController
state.loading = true
state.error = null
try {
const result = await fetchFn(...args)
// Если запрос был отменён — игнорируем результат
if (!currentController.aborted) {
state.data = result
}
} catch (err) {
if (!currentController.aborted) {
state.error = err.message
}
} finally {
if (!currentController.aborted) {
state.loading = false
}
}
return state
}
return { state, execute }
}
// Имитируем async fetchFn
async function fakeApiCall(userId) {
// В реальности: return fetch(`/api/users/${userId}`).then(r => r.json())
return new Promise((resolve) => {
setTimeout(() => resolve({ id: userId, name: `User ${userId}` }), 10)
})
}
const { state, execute } = useFetch(fakeApiCall)
async function demo() {
console.log('loading:', state.loading) // false
const result = await execute(1)
console.log('data:', JSON.stringify(state.data)) // { id: 1, name: "User 1" }
console.log('loading:', state.loading) // false
await execute(42)
console.log('data:', JSON.stringify(state.data)) // { id: 42, name: "User 42" }
}
demo()Composable — это функция с именем use*, которая инкапсулирует **реактивную логику** и позволяет переиспользовать её в нескольких компонентах. Composables — главный способ переиспользования кода в Vue 3.
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// В компоненте:
const { x, y } = useMousePosition()import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timer = null
watch(value, (newVal) => {
clearTimeout(timer)
timer = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}
// Применение — поиск с задержкой
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
watch(debouncedQuery, (query) => {
if (query) fetchResults(query) // запрос только когда пользователь перестал печатать
})import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
fetch(url)
.then(r => r.json())
.then(json => { data.value = json })
.catch(err => { error.value = err.message })
.finally(() => { loading.value = false })
return { data, error, loading }
}
// В компоненте:
const { data: users, loading, error } = useFetch('/api/users')Mixins — старый способ переиспользования в Vue 2. Они имеют серьёзные недостатки:
| Проблема | Mixins | Composables |
|-------------------------|---------------------------------|--------------------------|
| Источник свойств | Непонятно — откуда пришло? | Явно из const { x } = useX() |
| Конфликты имён | Перезапись без предупреждения | Переименование через деструктуризацию |
| Переиспользование | Нельзя передавать параметры | Полноценные аргументы |
| Отладка | Сложно трассировать | Обычная функция |
// MIXINS — проблема: откуда взялось searchQuery?
export default {
mixins: [SearchMixin, UserMixin, PaginationMixin],
mounted() {
// searchQuery из SearchMixin или UserMixin? Непонятно!
this.searchQuery = ''
}
}
// COMPOSABLES — явно и прозрачно
export default {
setup() {
const { query: searchQuery, results } = useSearch()
const { currentUser } = useUser()
const { page, totalPages } = usePagination(results)
// всё читаемо и явно
return { searchQuery, results, currentUser, page, totalPages }
}
}// Composable с опциями
function useInterval(callback, { delay = 1000, immediate = false } = {}) {
let id = null
onMounted(() => {
if (immediate) callback()
id = setInterval(callback, delay)
})
onUnmounted(() => clearInterval(id))
return {
pause: () => clearInterval(id),
resume: () => { id = setInterval(callback, delay) }
}
}
// Composable возвращающий composable
function useAsyncState(asyncFn, defaultValue) {
const state = ref(defaultValue)
const loading = ref(false)
const error = ref(null)
async function execute(...args) {
loading.value = true
error.value = null
try {
state.value = await asyncFn(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return { state, loading, error, execute }
}useDebounce и useFetch как чистые JS функции без Vue
// useDebounce — задержка выполнения функции
function useDebounce(fn, delay = 300) {
let timer = null
const debounced = function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
// Дополнительные методы
debounced.cancel = () => clearTimeout(timer)
debounced.flush = function(...args) {
clearTimeout(timer)
fn.apply(this, args)
}
return debounced
}
// Использование useDebounce
const handleSearch = useDebounce((query) => {
console.log(`Поиск: "${query}"`)
}, 300)
// Имитируем быстрый ввод пользователя
handleSearch('J')
handleSearch('Ja')
handleSearch('Jav')
handleSearch('Java')
handleSearch('JavaS')
// Только последний вызов выполнится через 300мс (в реальном браузере)
// В Node.js нет таймаута между вызовами — демонстрируем концепцию
console.log('(запросы задебаунсированы, выполнится только последний)')
// useFetch — загрузка данных с состоянием
function useFetch(fetchFn) {
const state = {
data: null,
loading: false,
error: null,
}
let abortController = null
async function execute(...args) {
// Отменяем предыдущий запрос если он ещё идёт
if (abortController) abortController.abort()
abortController = { aborted: false, abort() { this.aborted = true } }
const currentController = abortController
state.loading = true
state.error = null
try {
const result = await fetchFn(...args)
// Если запрос был отменён — игнорируем результат
if (!currentController.aborted) {
state.data = result
}
} catch (err) {
if (!currentController.aborted) {
state.error = err.message
}
} finally {
if (!currentController.aborted) {
state.loading = false
}
}
return state
}
return { state, execute }
}
// Имитируем async fetchFn
async function fakeApiCall(userId) {
// В реальности: return fetch(`/api/users/${userId}`).then(r => r.json())
return new Promise((resolve) => {
setTimeout(() => resolve({ id: userId, name: `User ${userId}` }), 10)
})
}
const { state, execute } = useFetch(fakeApiCall)
async function demo() {
console.log('loading:', state.loading) // false
const result = await execute(1)
console.log('data:', JSON.stringify(state.data)) // { id: 1, name: "User 1" }
console.log('loading:', state.loading) // false
await execute(42)
console.log('data:', JSON.stringify(state.data)) // { id: 42, name: "User 42" }
}
demo()Реализуй composable `usePagination(items, pageSize)`, который управляет пагинацией массива. Возвращаемый объект должен содержать: - `currentPage` — текущая страница (начиная с 1) - `totalPages` — общее количество страниц (Math.ceil(items.length / pageSize)) - `currentItems` — элементы текущей страницы (правильный slice массива) - `nextPage()` — перейти на следующую страницу (нельзя выйти за totalPages) - `prevPage()` — перейти на предыдущую страницу (нельзя выйти ниже 1) - `goToPage(n)` — перейти на страницу n (зажать в диапазоне 1..totalPages) ``` const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] const pager = usePagination(items, 3) console.log(pager.totalPages) // 4 console.log(pager.currentItems) // [1, 2, 3] pager.nextPage() console.log(pager.currentItems) // [4, 5, 6] pager.goToPage(4) console.log(pager.currentItems) // [10] pager.nextPage() console.log(pager.currentPage) // 4 (не вышли за границу) ```
getCurrentItems: const start = (currentPage - 1) * pageSize; return items.slice(start, start + pageSize). nextPage: if (currentPage < totalPages) currentPage++. prevPage: if (currentPage > 1) currentPage--. goToPage: currentPage = Math.min(totalPages, Math.max(1, n)).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке