# Введение
# Почему появился Composition API?
Примечание
Прежде чем приступать к изучению этого раздела документации необходимо понимать как базовые основы Vue, так и принципы создания компонентов.
Создание компонентов Vue позволяет извлекать повторяющиеся части интерфейса, вместе со связанной функциональностью, в переиспользуемые части кода. Этот подход добавляет приложению достаточно много с точки зрения удобства обслуживания и гибкости. Однако, коллективный опыт показал, что этого всё ещё может быть недостаточно, если приложение становится действительно большим — когда счёт идёт на несколько сотен компонентов. Когда приходится работать с такими большими приложениями — возможность разделения и переиспользования кода становится крайне важна.
Представим, что в приложении есть компонент, который отображает список репозиториев конкретного пользователя. Поверх него, нужно реализовать функциональность поиска и фильтрации. Компонент, управляющий подобным отображением, может выглядеть так:
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// использование `this.user` для загрузки пользовательских репозиториев
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Этот компонент имеет несколько обязанностей:
- Загрузка списка репозиториев из какого-либо внешнего API для этого имени пользователя и его обновление при изменении пользователя
- Поиск репозиториев с помощью строки
searchQuery
- Фильтрация репозиториев с помощью объекта
filters
Организация логики в опциях компонента (data
, computed
, methods
, watch
) отлично работает в большинстве случаев. Однако, чем больше становятся компоненты, тем больше разрастётся список логических блоков. Что может привести к появлению компонентов, которые сложно изучать и понимать, особенно для тех, кто не занимался их разработкой.
Пример большого компонента, в котором сгруппированы по цвету его логические блоки.
Подобная фрагментация усложняет понимание и поддержание таких сложных компонентов. Разделение по опциям делает менее заметными логические блоки используемые в них. Кроме того, при работе над одной логической задачей приходится постоянно «прыгать» между блоками в поисках соответствующего кода.
Было бы удобнее, если соответствующий логическому блоку код можно разместить рядом. И это именно то, чего позволяет добиться Composition API.
# Основы Composition API
Разобравшись с вопросом почему появился, можно перейти к вопросу как использовать. Прежде чем начинать работу с Composition API должно быть место, где его можно использовать. В компоненте Vue это место называется setup
.
# Опция компонента setup
Новая опция компонента setup
выполняется перед созданием компонента, сразу после разрешения входных параметров props
, и служит точкой старта для Composition API.
ВНИМАНИЕ
Внутри setup
не получится использовать this
, потому что он не будет ссылаться на экземпляр компонента. Потому что вызов setup
будет происходить до разрешения свойств data
, computed
или methods
, а значит они будут недоступны.
Опция setup
должна быть функцией, которая принимает аргументами props
и context
(о которых подробнее поговорим дальше). Кроме того, всё что возвращается из функции setup
будет доступно для остальных частей компонента (вычисляемых свойств, методов, хуков жизненного цикла и т.д.), а также в шаблоне компонента.
Добавим setup
в компонент:
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }
return {} // всё что возвращается, станет доступно в остальной части компонента
}
// ... остальная часть компонента
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Извлечём логику первого логического блока (отмеченной как «1» в исходном примере).
- Загрузка списка репозиториев из какого-либо внешнего API для этого имени пользователя и его обновление при изменении пользователя
Начнём с самых очевидных частей:
- Списка репозиториев
- Функции для обновления списка репозиториев
- Возвращение как списка репозиториев, так и функции обновления, чтобы использовать их в других опциях компонента
// src/components/UserRepositories.vue; функция `setup`
import { fetchUserRepositories } from '@/api/repositories'
// в компоненте
setup(props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories // возвращаемые функции ведут себя также, как и методы
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Начало положено! Пока что ещё не всё работает, потому что переменная repositories
не реактивна. С точки зрения пользователя, список репозиториев будет оставаться пустым. Давайте это исправим!
# Реактивные переменные с помощью ref
Во Vue 3 теперь можно сделать реактивную переменную где угодно с помощью новой функции ref
, например так:
import { ref } from 'vue'
const counter = ref(0)
2
3
ref
принимает аргумент и возвращает его обёрнутым в объект со свойством value
, которое затем можно использовать для чтения или изменения значения реактивной переменной:
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
2
3
4
5
6
7
8
9
Оборачивание значения в объект может показаться лишним, но это нужно для одинакового поведения разных типов данных в JavaScript. Это связано с тем, что примитивные типы в JavaScript, такие как Number
или String
, передаются по значению, а не по ссылке:
Наличие объекта-обёртки вокруг любого значения позволяет безопасно использовать его в любой части приложения, не беспокоясь о потере реактивности где-то по пути.
Примечание
Другими словами, ref
создаёт реактивную ссылку к значению. Концепция работы со ссылками используется повсеместно в Composition API.
Возвращаясь к примеру, создадим реактивную переменную repositories
:
// src/components/UserRepositories.vue; функция `setup`
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
// в компоненте
setup(props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Готово! Теперь каждый вызов getUserRepositories
будет изменять repositories
и обновлять вид блока, чтобы отобразить изменения. Компонент станет выглядеть так:
// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
},
data () {
return {
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Несколько частей первого логического блока теперь переместились в метод setup
и удобно располагаются рядом друг с другом. Остался вызов getUserRepositories
в хуке mounted
и метод-наблюдатель, отслеживающий изменения входного параметра user
.
Начнём с хука жизненного цикла.
# Использование хуков жизненного цикла внутри setup
Для паритета возможностей между Composition API и Options API, также требовался способ использовать хуки жизненного цикла внутри setup
. Это стало возможно благодаря новым функциям, экспортируемым Vue. Хуки жизненного цикла в Composition API именуются как и в Options API, но с префиксом on
: т.е. mounted
станет onMounted
.
Эти функции принимают аргументом коллбэк, который выполнится при вызове компонентом хука жизненного цикла.
Добавим его в функцию setup
:
// src/components/UserRepositories.vue; функция `setup`
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// в компоненте
setup(props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // хук `mounted` вызовет `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Осталось реализовать отслеживание изменений входного параметра user
и реагирование на них. Для этого воспользуемся автономной функцией watch
.
# Отслеживание изменений с помощью watch
Аналогично тому, как реализуем отслеживание изменений свойства user
через опцию watch
внутри компонента — тоже самое можно сделать и через функцию watch
, импортированную из Vue. Она принимает 3 аргумента:
- Реактивная ссылка или геттер-функция, которую требуется отслеживать
- Коллбэк
- Опциональные настройки
Простой пример, чтобы понять как это работает:
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('Новое значение counter: ' + counter.value)
})
2
3
4
5
6
При каждом изменении counter
, например после counter.value = 5
, будет срабатывать наблюдатель и вызовется коллбэк (второй аргумент), который в данном случае выведет в консоль 'Новое значение counter: 5'
.
Эквивалент при использовании Options API:
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('Новое значение counter: ' + this.counter)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
Более подробную информацию о watch
можно найти в продвинутом руководстве.
Возвращаемся к примеру:
// src/components/UserRepositories.vue; функция `setup`
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'
// в компоненте
setup(props) {
// `toRefs` создаёт реактивную ссылку для входного параметра `user` из `props`
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// меняем `props.user` на `user.value` для получения значения по реактивной ссылке
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// устанавливаем наблюдатель на реактивную ссылку входного параметра `user`
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Использование toRefs
в самом начале setup
необходимо для того, чтобы убедиться, что метод-наблюдатель будет реагировать на изменения входного параметра user
.
Благодаря всем этим изменениям получилось вынести весь первый логический блок в одно место. Теперь можно сделать тоже самое и со вторым логическим блоком — фильтрацией по свойству searchQuery
, но на этот раз воспользовавшись вычисляемым свойством.
# Автономные computed
-свойства
Вне компонента Vue, как ref
или watch
, также можно создавать вычисляемые свойства с помощью функции computed
, импортированной из Vue. Вернёмся к примеру со счётчиком:
import { ref, computed } from 'vue'
const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)
counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
2
3
4
5
6
7
8
Функция computed
вернёт реактивную ссылку только для чтения с результатом коллбэка, который был передан первым аргументом в computed
. Для доступа к значению такого вычисляемого свойства, нужно обращаться к свойству .value
, как и в случае с ref
.
Перенесём функциональность поиска в setup
:
// src/components/UserRepositories.vue; функция `setup`
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'
// в компоненте
setup(props) {
// `toRefs` создаёт реактивную ссылку для входного параметра `user`
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// меняем `props.user` на `user.value` для получения значения по реактивной ссылке
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// устанавливаем наблюдатель на реактивную ссылку входного параметра `user`
watch(user, getUserRepositories)
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(
repository => repository.name.includes(searchQuery.value)
)
})
return {
repositories,
getUserRepositories,
searchQuery,
repositoriesMatchingSearchQuery
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Можно продолжать делать аналогичное и для других логических блоков, но возможно уже раздаётся вопрос — Разве это не перемещение кода в опцию setup
, что теперь сделает её гигантской? Что ж, это не так. Поэтому, прежде чем продолжить, вынесем приведённый код выше в автономную функцию композиции. Начнём с создания useUserRepositories.js
:
// src/composables/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function useUserRepositories(user) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Функциональность поиска вынесем в собственную функцию композиции:
// src/composables/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Теперь, когда эти две функциональности разнесены по отдельным файлам, можно начать использовать их в компоненте вот таким образом:
// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
return {
// Поскольку репозитории без фильтрации не используются, можно
// объявить отфильтрованные результаты под именем `repositories`
repositories: repositoriesMatchingSearchQuery,
getUserRepositories,
searchQuery,
}
},
data () {
return {
filters: { ... }, // 3
}
},
computed: {
filteredRepositories () { ... }, // 3
},
methods: {
updateFilters () { ... }, // 3
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Уже должно быть понятно, что и как делается, поэтому перейдём к концу и переместим оставшуюся функциональность фильтрации. Вдаваться в детали её реализации не будем, это не важно для данного руководства.
// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery)
return {
// Поскольку репозитории без фильтрации не используются, можно
// объявить отфильтрованные результаты под именем `repositories`
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Вот и всё!
И это лишь видимая часть айсберга Composition API и того, что он позволяет сделать. Более подробную информацию можно узнать в продвинутом руководстве.