defineExpose() в Vue 3

Привет, Хабр!
Сколько раз вы сталкивались с ситуацией, когда сделали аккуратный Vue-компонент на , вроде всё красиво, а потом… вам внезапно нужно из родительского компонента сфокусировать инпут, сбросить фильтр, открыть модалку, валидировать форму. Казалось бы, задача тривиальная, но
script setup
не даёт просто так вынуть методы наружу.
Сегодня рассмотрим одну из самых неочевидных, но крайне полезных возможностей Vue 3 — функцией defineExpose()
.
Зачем вообще нужен defineExpose ()
Когда Vue 3 представил синтаксис script setup
, он упростил декларативную структуру компонентов. Нет необходимости возвращать объект из setup
, не нужно явно импортировать defineComponent
, всё стало более компактным.
Однако, с этой лаконичностью пришло одно жёсткое ограничение: всё, что вы определяете в script setup
, остаётся приватным. Ни один метод, переменная или реактивное состояние не будет доступен извне компонента, даже если родитель создал ref
на этот компонент.
Это сознательное архитектурное решение. Vue предполагает, что компонент должен быть инкапсулирован. Но в жизни часто сталкиваемся с кейсами, когда нужно дать родителю инструмент для управления дочерним компонентом напрямую.
Тут и поможет defineExpose()
.
Проблема: как добраться до метода внутри компонента?
Посмотрим на типичную ситуацию:
Теперь допустим, хочется вызвать
logMessage
из родителя:childRef.value.logMessage(); // Ошибка: undefined
Это не сработает потому что
script setup
инкапсулирует всё внутри себя. Вся логика и данные компонента просто недоступны черезref
.В отличие от классического
setup
, где можно вернуть объект или использоватьexpose
,script setup
изначально ничего наружу не отдаёт. Vue рассматривает его как изолированную зону.Что делает defineExpose () и как он работает
defineExpose()
позволяет явно указать, какие переменные или методы компонента вы хотите сделать доступными родителю черезref
.Пример:
Теперь, если вы получите
ref
на этот компонент в родителе, вы сможете вызвать:childRef.value.sayHi(); // всё работает!
Только то, что вы передадите в
defineExpose
, будет доступно. Всё остальное останется приватным, как и положено хорошему модульному коду.Как это связано с ref на компонент
Для доступа к публичному API компонента необходимо использовать
ref
на сам компонент, а не на внутренние элементы:
В
setup
родителя:const childRef = ref(null); onMounted(() => { childRef.value.sayHi(); // работает, если sayHi был передан через defineExpose });
Если вы попытаетесь вызвать метод без
defineExpose
,childRef.value
будет пустым объектом или вообщеundefined
.Примеры применения
.focus () у поля ввода
Компонент:
Родитель:
Без
defineExpose
это бы не работало. Vue не стал бы передаватьfocus
наружу..validate () в форме
Представим, что есть форма, в которой необходимо проверить корректность данных перед отправкой. Компонент формы:
В родителе:
if (formRef.value.validate()) { submitForm(); }
Можно использовать этот паттерн и в более сложных случаях, например, при асинхронной валидации.
Фильтр с .reset ()
Часто в интерфейсах есть фильтры, которые пользователь должен сбрасывать по кнопке.
Родитель теперь может сбросить фильтры одним вызовом:
filterPanelRef.value.reset();
Как вообще работает defineExpose ()
Все таки с этим нужно разобраться немного подробнее.
Когда Vue создаёт экземпляр компонента, он формирует внутреннюю структуру — набор связей, реактивных переменных, методов, локального состояния. Всё это — приватное, изолированное. И тут важно: даже если ты определил
ref
, объявил функциюfocus()
или создалcomputed
, родитель не получит доступ к этим штукам просто так черезref
на компонент. Vue их не прокидывает наружу по умолчанию.Именно для этого внутри у каждого компонента есть специальное хранилище —
ctx.exposed
. Это объект, который существует исключительно для того, чтобы родитель черезref.value
мог достучаться до только тех методов и переменных, которые ты явно разрешил. В классическомsetup()
ты управляешь этим вручную — получаешьexpose()
из аргументов и сам передаёшь туда, что хочешь экспортировать:setup(props, { expose }) { const focus = () => { input.value?.focus() } expose({ focus }); }
Когда ты используешь
, ты не видишь
setup()
напрямую. Но Vue всё равно генерирует его. И если ты пишешьdefineExpose({ focus })
, это буквально превращается вexpose({ focus })
внутри сгенерированной функции…Теперь пример для ясности:
defineExpose({ validate, reset, isOpen });
Что получит родитель? Только эти три метода/переменных.
ref.value.validate()
— работает.ref.value.reset()
— тоже.ref.value.anythingElse()
— уже нет. Это и есть суть: ты объявляешь API компонента, ты отвечаешь за его контракт.Дальше интересней. Всё, что передается в
defineExpose
, Vue сохраняет не в каком-то абстрактном пространстве, а в конкретном месте:vnode.component.exposed
. Это свойство объекта, в котором Vue аккуратно хранит публичный интерфейс компонента. Когда родитель рендерит, Vue создаёт VNode, вызывает
setup()
, заполняетinstance.exposed
, и в момент связывания просто делаетchildRef.value = instance.exposed
.Вот почему
ref.value
— это не сам компонент, неthis
, а именно тот объект, который явно передали черезexpose()
илиdefineExpose()
.Ещё нюанс: Vue не делает проксирование локального состояния. Он просто копирует ссылки. То есть если ты передаёшь
ref
, родитель получит его.value
и будет видеть обновления. Но если передадим обычную переменную, напримерlet visible = true
, то родитель увидит снимок на момент связывания — и не больше.Пример:
const count = ref(0); const reset = () => { count.value = 0 }; defineExpose({ count, reset });
Теперь родитель может вызывать
childRef.value.reset()
и читатьchildRef.value.count.value
. Ноincrement()
— если ты его не экспознул — останется приватным. Vue ничего не угадывает, и это делает поведение абсолютно предсказуемым.А если ты вообще не вызвал
defineExpose()
или передал туда пустой объект —ref.value
будетundefined
или{}
. Это частая ошибка у тех, кто только начал использоватьи надеется, что методы будут проброшены автоматически. Не будут. Vue не прокидывает наружу ничего, если ты об этом не попросил.
Плюс:
defineExpose()
работает только если родитель указалref
на компонент. Без этого всё, что ты экспознешь, будет просто лежать внутри компонента — недоступно извне. И это нормально: Vue создаёт API не «на авось», а по запросу. Кроме того,defineExpose()
не влияет ни наv-model
, ни на события, ни на слоты, ни наprovide/inject
. Это — исключительно про доступ к API черезref.value
.Теперь логичный вопрос: что происходит, когда компонент уничтожается? Здесь Vue тоже сработал грамотно. При
unmount
компонента, он сам очищаетref.value
у родителя. То мы не обязаны вручную сбрасывать ссылку — Vue позаботится об адекватной очистке.А вы уже использовали
defineExpose()
в своих проектах? Поделитесь, где и как в комментариях.Если вы работаете с Vue на практике — у вас наверняка есть задачи, которые не ограничиваются чистой теорией. Именно для таких случаев Otus проводит открытые уроки: короткие, по делу и на живых примерах. Ниже — два ближайших. Присоединяйтесь, если тема в фокусе вашей текущей работы или стоит в бэклоге.
14 апреля — Автосохранение в Vue: реализация через localStorage на примере боевого интерфейса
22 апреля — Vue.js 3: быстрый старт с актуальным стеком и подходами
Habrahabr.ru прочитано 6597 раз