Собеседование Frontend Javascript Vue | 150 тысяч
Сегодня мы разберём техническое собеседование кандидата на позицию фронтенд-разработчика, в ходе которого он продемонстрировал уверенное владение базовыми концепциями JavaScript и Vue.js — включая Event Loop, реактивность, жизненный цикл компонентов и механизмы передачи данных между ними. Интервьюер также подробно рассказал о структуре команды, стеке технологий (Vue 3, Laravel, PHP) и особенностях работы в крупной IT-компании, обслуживающей корпоративного заказчика. Беседа прошла в дружелюбной атмосфере и завершилась взаимным интересом: кандидат заинтересован в масштабных проектах, а команда заинтересована в усилении фронтенд-направления.
Вопрос 1. Расскажите о своём опыте разработки: какие интересные и сложные задачи приходилось решать, какие технологии использовали?
Таймкод: 00:01:28
Ответ собеседника: Правильный. Описан опыт разработки интернет-магазина на Vue 2 с нуля (фриланс): подключение платёжной системы, миграция с Express.js + MongoDB на Nuxt. Упоминается разработка игры на Phaser. На предыдущем месте работы с нуля разрабатывалось внутреннее приложение для сотрудников с тремя личными кабинетами и разными механиками авторизации. Продуктовый опыт — работа в компании «Все инструменты» над личным кабинетом, разработка фич по отгрузке товаров от поставщика на склад (расчёт габаритов, стоимости, времени доставки). На фрилансе в основном работа с ванильным JS и вёрсткой.
Правильный ответ:
Ответ собеседника является хорошим введением в опыт, но для позиции Go-разработчика он выглядит довольно слабо. Вот как стоило бы структурировать и усилить ответ, особенно с акцентом на Go.
1. Общая структура рассказа об опыте
Рассказ должен идти от релевантного к менее релевантному. Для позиции Go-разработчика нужно ставить во главу угла именно бэкенд-разработку на Go, а не фронтенд. Хронологический подход тоже работает: начать с последнего места работы и двигаться назад, либо начать с самого значимого проекта.
2. Что стоит подчеркнуть в рассказе
При описании каждого проекта или места работы нужно придерживаться структуры: контекст — задача — решение — результат. Это позволяет интервьюеру быстро оценить глубину вовлечённости и уровень ответственности.
- Контекст: что за продукт, какая бизнес-область, размер команды.
- Задача: какую конкретную проблему решали, какие ограничения были.
- Решение: какие технологии выбрали и почему, какие архитектурные решения приняли, что сделали лично.
- Результат: метрики, эффект для бизнеса, уроки.
3. Пример усиленного ответа с фокусом на Go
«Последние два года я работаю Go-разработчиком в компании X. Мы строим высоконагруженную платформу обработки заказов для e-commerce. Мой основной фокус — микросервис интеграции с логистическими провайдерами.
Одна из самых интересных задач — переписать монолитный сервис расчёта доставки на микросервисную архитектуру. Проблема была в том, что монолит не справлялся с нагрузкой в пиковые периоды (чёрная пятница — 10 000 RPS), а добавление новых провайдеров занимало недели из-за тесной связанности кода.
Я спроектировал новый сервис на Go с чистой архитектурой (handler → service → repository). Использовал gRPC для внутреннего взаимодействия и REST для внешнего API. Для кэширования тарифов подключил Redis с TTL-стратегией. Для асинхронной обработки запросов к медленным провайдерам использовал паттерн worker pool с каналами Go. В результате сервис стал выдерживать 15 000 RPS, а добавление нового провайдера сократилось с 2 недель до 2 дней — достаточно реализовать интерфейс Provider.
Также я занимался оптимизацией запросов к PostgreSQL. Один из критичных запросов выполнялся 800 мс. После анализа плана запроса (EXPLAIN ANALYZE) обнаружил отсутствие индекса по составному ключу (warehouse_id, delivery_zone_id). Добавил частичный индекс с условием по активным тарифам — время упало до 15 мс.
Из технологий в текущем проекте: Go 1.21, PostgreSQL, Redis, Kafka (для событий об изменении тарифов), Docker, Kubernetes, Prometheus + Grafana для мониторинга. Пишу тесты с использованием testify и моков через gomock. CI/CD на GitHub Actions.»
4. Как адаптировать свой опыт, если Go-опыта мало
Если Go-проектов действительно мало, нужно показать переносимые навыки:
- Понимание конкурентности (goroutines, channels vs async/await из JS или потоки из других языков).
- Опыт работы с SQL и оптимизацией запросов.
- Понимание микросервисной архитектуры и паттернов.
- Опыт с Docker, CI/CD, мониторингом.
Можно сказать: «Хотя большая часть моего опыта — фронтенд на Vue и Node.js, я активно изучаю Go. В пет-проектах реализовал REST API с использованием стандартной библиотеки и фреймворка chi, работал с PostgreSQL через pgx, писал unit-тесты и интеграционные тесты. В Node.js я уже решал задачи с высокой конкурентностью через worker threads и понимаю, как это по-другому выглядит в Go через goroutines.»
5. Чего избегать
- Не перечислять стек технологий без контекста.
- Не говорить «я делал всё» — лучше честно разделить зоны ответственности.
- Не уходить в детали фронтенда, если позиция бэкенд.
- Не забывать про метрики и измеримые результаты — они показывают зрелость инженера.
Вопрос 2. Как бы вы оценили свой уровень знания JavaScript по пятибалльной шкале?
Таймкод: 00:06:04
Ответ собеседника: Правильный. Оценил свой уровень как уверенный, между 4 и 5 баллами.
Правильный ответ:
Оценка уровня — это не только про число, но и про то, как человек может обосновать свою оценку. Для позиции Go-разработчика вопрос про JavaScript может быть уточняющим (например, если в стеке компании есть фронтенд-часть или fullstack-задачи), но всё равно стоит ответить чётко и с примерами.
1. Как правильно оценивать свой уровень
Самооценка должна быть подкреплена конкретными знаниями и опытом. Вот примерная шкала для JavaScript:
- 1 балл: базовый синтаксис, переменные, циклы, простые функции. Не писал реальных проектов.
- 2 балл: понимание замыканий, работа с DOM, базовые асинхронные операции (callbacks). Писал простые скрипты.
- 3 балла: уверенное владение ES6+, промисы, async/await, работа с fetch/XMLHttpRequest. Разрабатывал фронтенд-приложения на фреймворке.
- 4 балла: глубокое понимание event loop, прототипного наследования, модульных систем (CommonJS, ESM), опыт с Node.js на бэкенде. Писал сложные приложения, работал с производительностью.
- 5 балл: экспертное знание спецификации, понимание внутренней работы V8, опыт разработки библиотек и фреймворков, глубокое понимание оптимизации, работы с памятью, профилирования.
2. Пример развёрнутого ответа
«Я бы оценил свой JavaScript на 4 из 5. Я уверенно работаю с ES6+: деструктуризация, спред-операторы, классы, модули. Имею опыт разработки на Vue 2 и Nuxt — писал компоненты, работал с Vuex для управления состоянием, настраивал маршрутизацию. На бэкенде работал с Node.js и Express.js — писал REST API, подключал middleware, работал с MongoDB через Mongoose.
Понимаю, как работает event loop, чем микротаски отличаются от макротасок. Работал с промисами и async/await, знаю про обработку ошибок в цепочках промисов. Имею опыт работы с WebSocket для real-time обновлений.
Чего я знаю хуже — это глубокая оптимизация на уровне V8, написание собственных полифиллов, работа с WebAssembly. Поэтому 5 баллов я бы себе не поставил.»
3. Почему этот вопрос задают на интервью Go-разработчика
Интервьюер может проверять несколько вещей:
- Насколько человек честен в самооценке.
- Понимает ли разницу между «знаю синтаксис» и «понимаю, как работает язык».
- Есть ли опыт fullstack-разработки, который может быть полезен.
- Как человек аргументирует свою позицию — это показывает зрелость.
4. Чего избегать
- Не стоит говорить «5 из 5» без обоснования — это вызывает сомнения.
- Не стоит занижать оценку без причины — это может оттолкнуть.
- Не нужно уходить в длинные рассказы о фронтенде, если позиция чисто бэкенд на Go — достаточно краткого обоснования.
5. Связь с Go
Если хочется перевести разговор в более релевантное русло, можно добавить: «JavaScript я знаю хорошо, но сейчас фокус сместился на Go. Мне нравится, что в Go конкурентность реализована через goroutines и channels — это концептуально отличается от event loop в JavaScript, и я активно изучаю эти паттерны.»
Вопрос 3. Как работает Event Loop в JavaScript? Опишите механизм обработки микрозадач и макрозадач.
Таймкод: 00:07:06
Ответ собеседника: Правильный. Event Loop работает следующим образом: есть очередь микрозадач (промисы, queueMicrotask) и очередь макрозадач (setTimeout, setInterval, рендеринг, обработка событий). Сначала выполняется весь синхронный код в стеке вызовов, затем все микрозадачи, потом одна макрозадача, и цикл повторяется. События и таймеры хранятся в Web APIs и по истечении таймера попадают в соответствующие очереди.
Правильный ответ:
Ответ собеседника в целом верный, но можно дополнить деталями, которые показывают более глубокое понимание темы.
1. Архитектура Event Loop
JavaScript однопоточен — у него один стек вызовов (call stack) и одна куча (heap). Event Loop — это механизм, который позволяет выполнять асинхронный код без блокировки основного потока.
Ключевые компоненты:
- Call Stack — стек вызовов, где выполняется синхронный код.
- Web APIs — браузерные API (setTimeout, fetch, DOM-события), которые выполняют операции вне основного потока.
- Task Queue (Macrotask Queue) — очередь макрозадач.
- Microtask Queue — очередь микрозадач.
2. Типы задач
Макрозадачи (Macrotasks):
setTimeout,setIntervalsetImmediate(Node.js)- I/O операции
- Рендеринг (в браузере)
requestAnimationFrame(в браузере)
Микрозадачи (Microtasks):
Promise.then/catch/finallyqueueMicrotask()MutationObserver(в браузере)process.nextTick()(Node.js — имеет приоритет даже над микрозадачами)
3. Алгоритм Event Loop
Порядок выполнения в каждом цикле (тике):
- Выполнить весь синхронный код в call stack до опустошения.
- Выполнить все микрозадачи из Microtask Queue (очередь полностью опустошается).
- Выполнить одну макрозадачу из Task Queue.
- Обновить рендеринг (в браузере, если нужно).
- Повторить цикл.
Важный нюанс: после каждой макрозадачи Event Loop проверяет, не появились ли новые микрозадачи, и выполняет их перед следующей макрозадачей.
4. Пример для понимания
console.log('1'); // синхронный код
setTimeout(() => {
console.log('2'); // макрозадача
}, 0);
Promise.resolve().then(() => {
console.log('3'); // микрозадача
});
console.log('4'); // синхронный код
Вывод: 1, 4, 3, 2.
Пояснение:
- Сначала выполняется синхронный код:
1,4. setTimeoutпомещает callback в очередь макрозадач.Promise.thenпомещает callback в очередь микрозадач.- После завершения синхронного кода Event Loop опустошает очередь микрозадач:
3. - Затем берёт одну макрозадачу:
2.
5. Более сложный пример
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');
Вывод: script start, script end, promise1, promise2, setTimeout.
Второй .then() попадает в очередь микрозадач после выполнения первого, и поскольку очередь микрозадач опустошается полностью, promise2 выполнится раньше setTimeout.
6. Особенности Node.js
В Node.js есть дополнительный механизм — process.nextTick(), который имеет приоритет над микрозадачами. Также в Node.js есть фазы event loop: timers → pending callbacks → idle/prepare → poll → check → close callbacks.
7. Связь с Go
Для Go-разработчика полезно провести параллель: в Go вместо event loop используется многопоточная модель с goroutines и планировщиком (scheduler). Вместо очередей задач — каналы (channels) и многопоточное выполнение на реальных ОС-потоках. Это принципиально другой подход к конкурентности.
Вопрос 4. Что происходит при переполнении стека вызовов в браузере? Какой типичный сценарий к этому приводит?
Таймкод: 00:08:17
Ответ собеседника: Правильный. При переполнении стека вызовов возникает ошибка. Самый типичный сценарий — бесконечная рекурсия, когда функция вызывает саму себя без условия выхода.
Правильный ответ:
Ответ верный, но можно раскрыть тему глубже, что покажет более серьёзное понимание работы рантайма.
1. Что происходит при переполнении стека
Когда стек вызовов превышает свой лимит, движок JavaScript выбрасывает ошибку RangeError: Maximum call stack size exceeded. В браузере это приводит к остановке выполнения скрипта на данной вкладке. Сама вкладка может стать неотзывчивой, но другие вкладки и сам браузер продолжают работать, так как каждая вкладка обычно работает в отдельном процессе (в современных браузерах).
2. Типичные сценарии переполнения
Бесконечная рекурсия — самый очевидный случай:
function infiniteRecursion() {
infiniteRecursion();
}
infiniteRecursion();
// RangeError: Maximum call stack size exceeded
Рекурсия без правильного базового случая:
function factorial(n) {
// Забыли условие выхода для n <= 0
return n * factorial(n - 1);
}
factorial(5);
// Будет работать бесконечно, так как n уходит в отрицательные числа
Взаимная рекурсия (mutual recursion):
function foo() {
bar();
}
function bar() {
foo();
}
foo();
// RangeError: Maximum call stack size exceeded
Рекурсия с большим объёмом данных:
function sum(arr) {
if (arr.length === 0) return 0;
return arr[0] + sum(arr.slice(1));
}
// Массив из 100 000 элементов может вызвать переполнение
sum(new Array(100000).fill(1));
3. Размер стека
Размер стека вызовов зависит от движка JavaScript и среды выполнения:
- V8 (Chrome, Node.js): примерно 10 000–25 000 фреймов (зависит от платформы и настроек).
- SpiderMonkey (Firefox): похожие значения.
- JavaScriptCore (Safari): аналогично.
Точное значение можно проверить экспериментально:
function getStackSize() {
try {
return 1 + getStackSize();
} catch (e) {
return 0;
}
}
console.log(getStackSize());
4. Как избежать переполнения стека
Использование итерации вместо рекурсии:
// Рекурсивный подход (опасный)
function factorialRecursive(n) {
if (n <= 1) return 1;
return n * factorialRecursive(n - 1);
}
// Итеративный подход (безопасный)
function factorialIterative(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
Оптимизация хвостового вызова (Tail Call Optimization, TCO):
В спецификации ES6 предусмотрена оптимизация хвостовых вызовов, но на практике её поддерживает только Safari (JavaScriptCore). V8 и SpiderMonkey не реализовали TCO.
// Хвостовая рекурсия
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc); // хвостовой вызов
}
Использование трэмплина (trampoline):
function trampoline(fn) {
return function(...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result();
}
return result;
};
}
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return () => factorial(n - 1, n * acc); // возвращаем функцию, а не вызываем
}
const safeFactorial = trampoline(factorial);
console.log(safeFactorial(100000)); // Работает без переполнения стека
5. Отладка переполнения стека
В DevTools браузера можно использовать:
- Call Stack вкладка в Sources — показывает цепочку вызовов на момент ошибки.
- Break on exceptions — поставить точку останова при выбросе исключения.
- console.trace() — выводит текущий стек вызовов.
6. Аналогия в Go
В Go стек горутины начинается с 2 КБ и динамически растёт (до 1 ГБ по умолчанию). Это означает, что глубокая рекурсия в Go менее опасна, чем в JavaScript, но всё равно может привести к исчерпанию памяти. Планировщик Go использует сегментированные стеки, которые копируются и расширяются по мере необходимости.
Вопрос 4. Что происходит при переполнении стека вызовов в браузере? Какой типичный сценарий к этому приводит?
Таймкод: 00:08:17
Ответ собеседника: Правильный. При переполнении стека вызовов возникает ошибка. Самый типичный сценарий — бесконечная рекурсия, когда функция вызывает саму себя без условия выхода.
Правильный ответ:
Ответ верный, но можно раскрыть тему глубже, особенно с точки зрения практических аспектов и отладки.
1. Что происходит при переполнении стека
Когда стек вызовов превышает свой лимит, движок JavaScript выбрасывает ошибку RangeError: Maximum call stack size exceeded. В браузере это приводит к остановке выполнения скрипта — вся очередь задач Event Loop для данного контекста прекращается. В Node.js процесс завершается с аналогичной ошибкой.
2. Типичные сценарии переполнения
Бесконечная рекурсия — самый очевидный случай:
function infiniteRecursion() {
infiniteRecursion(); // нет базового случая
}
infiniteRecursion(); // RangeError: Maximum call stack size exceeded
Рекурсия без правильного базового случая:
function factorial(n) {
// забыли проверить n <= 1
return n * factorial(n - 1);
}
factorial(5); // уйдёт в минус и переполнит стек
Взаимная рекурсия (mutual recursion):
function foo() {
bar();
}
function bar() {
foo();
}
foo(); // переполнение стека
Рекурсивная обработка глубоко вложенных структур данных:
// Обход глубоко вложенного DOM-дерева или JSON
function traverse(node) {
if (node.children) {
node.children.forEach(child => traverse(child));
}
}
// Если дерево имеет глубину 10000+ уровней — переполнение
3. Размер стека
Размер стека зависит от движка и среды выполнения:
- V8 (Chrome, Node.js): примерно 10 000–25 000 фреймов (зависит от платформы и настроек).
- SpiderMonkey (Firefox): похожие значения.
- JavaScriptCore (Safari): аналогично.
Точное значение можно проверить экспериментально:
function getStackSize() {
try {
return 1 + getStackSize();
} catch (e) {
return 1;
}
}
console.log(getStackSize()); // ~10000-25000 в зависимости от среды
4. Как избежать переполнения
Использовать итерацию вместо рекурсии:
// Рекурсивный подход (опасен для больших n)
function factorialRecursive(n) {
if (n <= 1) return 1;
return n * factorialRecursive(n - 1);
}
// Итеративный подход (безопасен)
function factorialIterative(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
Оптимизация хвостового вызова (Tail Call Optimization, TCO):
В спецификации ES6 предусмотрена оптимизация хвостовых вызовов, но на практике её поддерживает только Safari (JavaScriptCore). V8 и SpiderMonkey не реализовали TCO.
// Хвостовой вызов (теоретически оптимизируется)
function factorialTCO(n, acc = 1) {
if (n <= 1) return acc;
return factorialTCO(n - 1, n * acc); // хвостовой вызов
}
Использовать трэмполинг (trampolining):
function trampoline(fn) {
return function(...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result();
}
return result;
};
}
const factorial = trampoline(function f(n, acc = 1) {
if (n <= 1) return acc;
return () => f(n - 1, n * acc); // возвращаем функцию, а не вызываем
});
console.log(factorial(100000)); // не переполнит стек
5. Отладка переполнения стека
В DevTools браузера можно использовать:
- Точку останова на исключениях (Pause on exceptions).
- Трассировку стека (stack trace) в консоли — покажет цепочку вызовов.
- Профайлер для анализа глубины рекурсии.
6. Аналогия в Go
В Go стек горутины начинается с 2 КБ и динамически растёт (до 1 ГБ по умолчанию). Это означает, что рекурсия в Go гораздо менее опасна с точки зрения переполнения стека. Тем не менее, бесконечная рекурсия в Go тоже приведёт к runtime: goroutine stack exceeds limit.
Вопрос 5. Чем отличаются обычные (именованные) функции от стрелочных в JavaScript?
Таймкод: 00:08:45
Ответ собеседника: Правильный. Два основных отличия: 1) Обычные функции поднимаются (hoisting), их можно вызвать до объявления в коде, стрелочные — нет. 2) Контекст this: у обычных функций this определяется в момент вызова (динамический), у стрелочных — привязан к лексическому окружению, в котором функция была создана, и не меняется при передаче в другой объекте.
Правильный ответ:
Ответ собеседника покрывает два ключевых отличия, но тема имеет больше нюансов, которые стоит знать.
1. Hoisting (поднятие)
Объявления функций (function declaration) поднимаются целиком, включая тело:
greet(); // Работает: "Hello!"
function greet() {
console.log('Hello!');
}
Стрелочные функции, объявленные через const или let, не поднимаются и попадают в Temporal Dead Zone:
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => console.log('Hello!');
2. Контекст this
Это самое важное отличие. Обычные функции имеют динамический this, стрелочные — лексический.
const obj = {
name: 'Alice',
// Обычная функция — this зависит от контекста вызова
greetRegular: function() {
console.log('Regular:', this.name);
},
// Стрелочная функция — this захвачен из внешнего окружения
greetArrow: () => {
console.log('Arrow:', this.name);
}
};
obj.greetRegular(); // "Regular: Alice"
obj.greetArrow(); // "Arrow: undefined" (this — глобальный объект или undefined в strict mode)
Практический пример с колбэками:
const team = {
name: 'Backend',
members: ['Alice', 'Bob', 'Charlie'],
// Неправильно: стрелочная функция внутри forEach
showTeamBroken: function() {
this.members.forEach(function(member) {
// this здесь — undefined (strict mode) или global
console.log(this.name + ': ' + member); // не работает
});
},
// Правильно: стрелочная функция сохраняет this
showTeamFixed: function() {
this.members.forEach((member) => {
console.log(this.name + ': ' + member); // "Backend: Alice", и т.д.
});
}
};
3. Объект arguments
Обычные функции имеют псевдомассив arguments, стрелочные — нет:
function regularFn() {
console.log(arguments); // [1, 2, 3]
}
regularFn(1, 2, 3);
const arrowFn = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};
arrowFn(1, 2, 3);
В стрелочных функциях вместо arguments используются rest-параметры:
const arrowFn = (...args) => {
console.log(args); // [1, 2, 3]
};
arrowFn(1, 2, 3);
4. Использование как конструктор
Обычные функции можно использовать с new, стрелочные — нет:
function Person(name) {
this.name = name;
}
const alice = new Person('Alice'); // Работает
const PersonArrow = (name) => {
this.name = name;
};
// const bob = new PersonArrow('Bob'); // TypeError: PersonArrow is not a constructor
5. Методы prototype
Обычные функции имеют свойство prototype, стрелочные — нет:
function Regular() {}
console.log(Regular.prototype); // { constructor: Regular }
const Arrow = () => {};
console.log(Arrow.prototype); // undefined
6. Дублирование параметров
Обычные функции в нестрогом режиме допускают дублирование имён параметров, стрелочные — нет:
function regular(a, a) { // Работает в non-strict mode
console.log(a);
}
regular(1, 2); // 2
const arrow = (a, a) => {}; // SyntaxError: Duplicate parameter name not allowed
7. Когда что использовать
- Стрелочные функции: колбэки, обработчики событий, методы внутри классов, где нужно сохранить
this. - Обычные функции: конструкторы, методы объектов, функции, которые должны быть подняты (hoisted), функции, использующие
arguments.
8. Связь с Go
В Go нет аналога this в том же смысле, но есть методы с приёмниками (receivers). Стрелочные функции в JavaScript по поведению ближе к замыканиям (closures) в Go, которые захватывают переменные из внешнего контекста:
func main() {
name := "Alice"
// Замыкание захватывает переменную name
greet := func() {
fmt.Println("Hello,", name)
}
greet() // "Hello, Alice"
}
Вопрос 6. Как выполнить глубокое копирование объекта в JavaScript? Какие способы существуют и какие у них ограничения?
Таймкод: 00:09:45
Ответ собеседника: Неполный. Можно использовать рекурсивный перебор объекта для глубокого копирования. Также упомянут способ через structuredClone (новый нативный метод), а также через spread-оператор, но он выполняет только поверхностное копирование. Отмечены проблемы с копированием функций и вложенных объектов при некоторых подходах.
Правильный ответ:
Тема глубокого копирования имеет много нюансов. Рассмотрим все основные способы с их ограничениями.
1. Spread-оператор и Object.assign — поверхностное копирование
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.b.c = 3;
console.log(original.b.c); // 3 — вложенный объект общий!
Spread и Object.assign копируют только первый уровень. Вложенные объекты передаются по ссылке.
2. JSON.parse(JSON.stringify()) — классический подход
const original = { a: 1, b: { c: 2 }, d: [1, 2, 3] };
const copy = JSON.parse(JSON.stringify(original));
copy.b.c = 3;
console.log(original.b.c); // 2 — работает корректно
Ограничения:
const original = {
date: new Date(),
fn: function() { return 42; },
undef: undefined,
nan: NaN,
infinity: Infinity,
regex: /test/gi,
symbol: Symbol('test'),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
circular: null
};
original.circular = original; // циклическая ссылка
const copy = JSON.parse(JSON.stringify(original));
// Результат:
// date → строка (не Date объект)
// fn → потеряна
// undef → потеряна
// nan → null
// infinity → null
// regex → пустой объект {}
// symbol → потеряна
// map → пустой объект {}
// set → пустой объект {}
// circular → TypeError: Converting circular structure to JSON
3. structuredClone() — нативный метод (современный стандарт)
Доступен в Node.js 17+ и современных браузерах.
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
arrayBuffer: new ArrayBuffer(8),
regexp: /test/gi
};
const copy = structuredClone(original);
console.log(copy.date instanceof Date); // true
console.log(copy.map instanceof Map); // true
console.log(copy.set instanceof Set); // true
Ограничения structuredClone:
const original = {
fn: function() { return 42; },
symbol: Symbol('test')
};
// structuredClone(original);
// TypeError: function could not be cloned
// Symbol тоже не клонируется
Также не поддерживает клонирование DOM-элементов и некоторых специфических объектов.
4. Рекурсивная функция — полный контроль
function deepClone(obj, hash = new WeakMap()) {
// Примитивы и null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Циклические ссылки
if (hash.has(obj)) {
return hash.get(obj);
}
// Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// Map
if (obj instanceof Map) {
const mapCopy = new Map();
hash.set(obj, mapCopy);
obj.forEach((value, key) => {
mapCopy.set(deepClone(key, hash), deepClone(value, hash));
});
return mapCopy;
}
// Set
if (obj instanceof Set) {
const setCopy = new Set();
hash.set(obj, setCopy);
obj.forEach(value => {
setCopy.add(deepClone(value, hash));
});
return setCopy;
}
// Array или Object
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], hash);
}
// Копируем символы-ключи
for (const sym of Object.getOwnPropertySymbols(obj)) {
clone[sym] = deepClone(obj[sym], hash);
}
return clone;
}
// Пример использования
const original = {
name: 'Alice',
date: new Date(),
pattern: /test/gi,
nested: { value: 42 },
map: new Map([['key', 'value']])
};
original.self = original; // циклическая ссылка
const copy = deepClone(original);
console.log(copy !== original); // true
console.log(copy.nested !== original.nested); // true
console.log(copy.self === copy); // true — циклическая ссылка сохранена
console.log(copy.date instanceof Date); // true
5. Библиотека lodash.cloneDeep
import cloneDeep from 'lodash/cloneDeep';
const copy = cloneDeep(original);
Lodash обрабатывает большинство краевых случаев: циклические ссылки, специальные объекты, прототипы. Это самый надёжный вариант для production.
6. Сравнение способов
| Способ | Циклические ссылки | Спец. объекты | Функции | Производительность |
|---|---|---|---|---|
| JSON.parse/stringify | ❌ TypeError | ❌ Теряются | ❌ Теряются | Быстро |
| structuredClone | ✅ | ✅ Частично | ❌ TypeError | Быстро |
| Рекурсивная функция | ✅ | ✅ Настраивается | ❌ Теряются | Средне |
| lodash.cloneDeep | ✅ | ✅ | ✅ Теряются | Средне |
7. Аналогия в Go
В Go глубокое копирование не является встроенной операцией. Для структур нужно либо копировать поля вручную, либо использовать сериализацию/десериализацию (например, через JSON или gob), либо библиотеки вроде github.com/jinzhu/copier.
type Person struct {
Name string
Age int
}
// Поверхностное копирование — просто присваивание значения
original := Person{Name: "Alice", Age: 30}
copy := original
// Для глубокого копирования со слайсами/мапами — ручная работу
Вопрос 7. Как организовать передачу данных и событий между компонентами в Vue: от родителя к дочернему, между дочерними компонентами, и от глубоко вложенного дочернего к родителю?
Таймкод: 00:11:03
Ответ собеседника: Правильный. Нисходящая передача (родитель → дочерний): через props. Если дочерний элемент глубоко вложен и props-проброс становится громоздким — использовать provide/inject. Передача события от дочернего компонента к родителю: через emit, а родитель слушает это событие. Между дочерними компонентами: через глобальное хранилище (Pinia/Vuex) или поднятие состояния к общему родителю и передачу через props.
Правильный ответ:
Ответ собеседника покрывает основные паттерны. Дополним деталями и альтернативными подходами.
1. Родитель → Дочерний: Props
Базовый механизм передачи данных сверху вниз:
<!-- Parent.vue -->
<template>
<ChildComponent :user="user" :is-active="isActive" />
</template>
<script>
export default {
data() {
return {
user: { name: 'Alice', age: 30 },
isActive: true
};
}
};
</script>
<!-- Child.vue -->
<template>
<div>{{ user.name }} - {{ isActive ? 'Active' : 'Inactive' }}</div>
</template>
<script>
export default {
props: {
user: {
type: Object,
required: true
},
isActive: {
type: Boolean,
default: false
}
}
};
</script>
2. Глубоко вложенный дочерний → Родитель: Provide/Inject
Когда props-проброс через много уровней становится громоздким (prop drilling):
<!-- GrandParent.vue -->
<template>
<ParentComponent />
</template>
<script>
export default {
provide() {
return {
theme: this.theme,
updateTheme: this.updateTheme
};
},
data() {
return {
theme: 'dark'
};
},
methods: {
updateTheme(newTheme) {
this.theme = newTheme;
}
}
};
</script>
<!-- DeepChild.vue (вложен на 3+ уровня) -->
<template>
<button @click="updateTheme('light')">Switch to Light</button>
</template>
<script>
export default {
inject: ['theme', 'updateTheme']
};
</script>
В Vue 3 Composition API с TypeScript:
<!-- GrandParent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
const updateTheme = (newTheme: string) => {
theme.value = newTheme;
};
provide('theme', theme);
provide('updateTheme', updateTheme);
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
import { inject } from 'vue';
const theme = inject<string>('theme');
const updateTheme = inject<(t: string) => void>('updateTheme');
</script>
3. Дочерний → Родитель: Events ($emit)
<!-- Child.vue -->
<template>
<button @click="$emit('update', { name: 'Bob', age: 25 })">
Update User
</button>
</template>
<script>
export default {
emits: ['update'] // явное объявление событий (рекомендуется)
};
</script>
<!-- Parent.vue -->
<template>
<ChildComponent @update="handleUpdate" />
</template>
<script>
export default {
methods: {
handleUpdate(user) {
this.user = user;
}
}
};
</script>
4. Между дочерними компонентами (Sibling Communication)
А. Поднятие состояния (State Lifting)
Общее состояние хранится в ближайшем общем предке:
<!-- Parent.vue -->
<template>
<ComponentA :shared-data="sharedData" @update="sharedData = $event" />
<ComponentB :shared-data="sharedData" />
</template>
Б. Pinia/Vuex — глобальное хранилище
// stores/user.js (Pinia)
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null,
isAuthenticated: false
}),
actions: {
login(user) {
this.currentUser = user;
this.isAuthenticated = true;
},
logout() {
this.currentUser = null;
this.isAuthenticated = false;
}
}
});
<!-- ComponentA.vue -->
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
function handleLogin() {
userStore.login({ name: 'Alice', age: 30 });
}
</script>
<!-- ComponentB.vue -->
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
</script>
<template>
<div v-if="userStore.isAuthenticated">
Welcome, {{ userStore.currentUser.name }}!
</div>
</template>
В. Event Bus (Vue 2, антипаттерн для новых проектов)
// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
// ComponentA.vue
EventBus.$emit('user-updated', user);
// ComponentB.vue
EventBus.$on('user-updated', (user) => {
this.user = user;
});
Event Bus не рекомендуется в Vue 3 — лучше использовать Pinia или внешние библиотеки вроде mitt.
5. v-model для двусторонней привязки
<!-- Parent.vue -->
<template>
<ChildInput v-model="searchText" />
</template>
<!-- ChildInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
};
</script>
6. Сравнение подходов
| Направление | Способ | Когда использовать |
|---|---|---|
| Родитель → Дочерний | Props | Всегда как основной способ |
| Родитель → Глубокий потомок | Provide/Inject | 3+ уровня вложенности, глобальные настройки |
| Дочерний → Родитель | $emit / emits | Уведомление о действиях пользователя |
| Дочерний → Дочерний | Pinia/Vuex | Общее состояние приложения |
| Дочерний → Дочерний | State Lifting | Простые случаи с общим родителем |
7. Связь с Go
Паттерны передачи данных в Vue концептуально похожи на паттерны в Go-сервисах:
- Props → параметры функции (явная передача).
- Provide/Inject → dependency injection (внедрение зависимостей).
- Pinia/Vuex → shared state / singleton (глобальное состояние).
- $emit → callback функции или каналы (уведомление о событиях).
Вопрос 8. Чем отличаются ref и reactive в Vue? В каких случаях используется каждый из них?
Таймкод: 00:12:31
Ответ собеседника: Неполный. reactive создаёт глубокую реактивность для объектов, ref под капотом тоже вызывает reactive для объектов. ref можно использовать для ссылок на DOM-элементы. reactive работает только с объектами. Также упомянут shallowRef, который создаёт поверхностную реактивность.
Правильный ответ:
Ответ затрагивает ключевые моменты, но есть важные нюансы, которые стоит раскрыть.
1. Основные отличия
ref — оборачивает значение в объект с полем .value. Работает с любыми типами данных (примитивы и объекты).
reactive — создаёт реактивную обёртку непосредственно над объектом. Работает только с объектами, массивами и коллекциями (Map, Set).
import { ref, reactive } from 'vue';
// ref — работает с примитивами
const count = ref(0);
console.log(count.value); // 0
count.value++;
// ref — работает с объектами (под капотом вызывает reactive)
const user = ref({ name: 'Alice', age: 30 });
console.log(user.value.name); // 'Alice'
// reactive — только объекты
const state = reactive({ count: 0, name: 'Bob' });
console.log(state.count); // 0 — без .value!
// reactive с примитивом — не работает
const primitive = reactive(42); // ⚠️ предупреждение, реактивность не работает
2. Доступ к значению
Ключевое практическое отличие — необходимость .value для ref:
// ref — нужен .value в JavaScript
const count = ref(0);
function increment() {
count.value++; // без .value не работает
}
// reactive — прямой доступ
const state = reactive({ count: 0 });
function increment() {
state.count++; // напрямую
}
В шаблоне Vue автоматически разворачивает (unwrap) ref, поэтому .value не нужен:
<template>
<div>{{ count }}</div> <!-- не count.value! -->
</template>
3. Замена значения целиком
Это критически важное отличие:
// ref — можно заменить целиком
const user = ref({ name: 'Alice' });
user.value = { name: 'Bob' }; // ✅ работает
// reactive — нельзя заменить целиком!
const state = reactive({ count: 0 });
state = { count: 1 }; // ❌ потеря реактивности!
// Правильный способ для reactive
Object.assign(state, { count: 1 }); // ✅
4. Деструктурирование
// reactive теряет реактивность при деструктурировании
const state = reactive({ count: 0, name: 'Alice' });
const { count, name } = state; // ❌ count и name — обычные значения
// Решение — toRefs
import { toRefs } from 'vue';
const { count, name } = toRefs(state); // ✅ count и name — теперь ref
// ref при деструктурировании — просто значение
const countRef = ref(0);
const value = countRef.value; // обычное число
5. Мониторинг изменений
// ref — следим за .value
watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} → ${newVal}`);
});
// reactive — следим за конкретным полем
watch(() => state.count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} → ${newVal}`);
});
// Следим за всем reactive объектом (глубоко)
watch(state, (newVal) => {
console.log('state changed', newVal);
}, { deep: true });
6. Когда что использовать
Используйте ref:
- Для примитивных значений (string, number, boolean).
- Когда нужно заменять значение целиком.
- Для ссылок на DOM-элементы и компоненты.
- Для возврата значений из composables (лучшая практика).
// Composable с ref — стандартная практика
function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value--;
return { count, increment, decrement };
}
// DOM-элемент
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus();
});
<template>
<input ref="inputRef" />
</template>
Используйте reactive:
- Для группировки связанных полей в единый объект (например, форма).
- Когда не нужна замена объекта целиком.
- Для сложных вложенных структур с глубокой реактивностью.
// Форма — хороший кандидат для reactive
const form = reactive({
email: '',
password: '',
confirmPassword: '',
errors: {
email: null,
password: null
}
});
function validate() {
form.errors.email = form.email.includes('@') ? null : 'Invalid email';
form.errors.password = form.password.length >= 8 ? null : 'Too short';
}
7. shallowRef и shallowReactive
// shallowRef — реактивен только при замене .value целиком
const shallowUser = shallowRef({ name: 'Alice', nested: { age: 30 } });
shallowUser.value.name = 'Bob'; // ❌ не вызовет обновление
shallowUser.value = { name: 'Bob', nested: { age: 25 } }; // ✅ вызовет обновление
// shallowReactive — реактивны только верхнеуровневые свойства
const shallowState = shallowReactive({
count: 0,
nested: { value: 42 }
});
shallowState.count++; // ✅ реактивно
shallowState.nested.value = 100; // ❌ не реактивно
8. Рекомендация Vue 3
Официальная рекомендация Vue 3 — использовать ref по умолчанию. Это связано с:
- Предсказуемостью (всегда
.value). - Возможностью замены значения целиком.
- Лучшей совместимостью с composables.
- Меньшим количеством краевых случаев.
9. Аналогия в Go
Концептуально ref похож на указатель в Go — вы работаете с обёрткой, а значение доступно через разыменование (.value для ref, *ptr для указателя). reactive же ближе к структуре, переданной по ссылке, но с потерей самой ссылки при переприсваивании.
// ref похож на указатель
count := 0
ptr := &count
*ptr++ // аналог ref.value++
// reactive похож на структуру по ссылке
type State struct {
Count int
}
state := State{Count: 0}
state.Count++ // прямой доступ
// state = State{Count: 1} — это новая переменная, старая не изменилась
Вопрос 9. Как отрисовать элемент компонента в другом месте DOM-дерева? Какая конструкция Vue для этого используется?
Таймкод: 00:14:23
Ответ собеседника: Правильный. Используется компонент Teleport. Компонент оборачивается в Teleport, передаётся атрибут to с указанием селектора целевого элемента, и содержимое отрисовывается в указанном месте DOM-дерева.
Правильный ответ:
Ответ верный. Дополним практическими примерами и деталями использования.
1. Базовое использование Teleport
<template>
<div class="app">
<h1>Основной контент</h1>
<ModalComponent />
</div>
<!-- Целевой элемент вне компонента -->
<div id="modal-root"></div>
</template>
<!-- ModalComponent.vue -->
<template>
<Teleport to="#modal-root">
<div class="modal-overlay">
<div class="modal-content">
<h2>Модальное окно</h2>
<p>Этот контент отрисован в #modal-root, а не внутри .app</p>
<button @click="$emit('close')">Закрыть</button>
</div>
</div>
</Teleport>
</template>
2. Зачем нужен Teleport
Основные сценарии:
- Модальные окна — чтобы избежать проблем с
overflow: hiddenиz-indexв родительских контейнерах. - Тосты/уведомления — отрисовка в фиксированной позиции вне основного layout.
- Dropdown-меню — когда выпадающий список должен выходить за границы родительского контейнера.
- Tooltips — позиционирование относительно viewport.
3. Динамический целевой элемент
<template>
<Teleport :to="targetSelector">
<div class="popup">Содержимое</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue';
const targetSelector = ref('#desktop-target');
// Можно менять цель на лету
function moveToMobile() {
targetSelector.value = '#mobile-target';
}
</script>
4. Отключение Teleport
Атрибут disabled позволяет условно отключить teleport:
<template>
<Teleport to="#modal-root" :disabled="isInline">
<div class="modal">
<p>Это модальное окно</p>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue';
// На мобильных отображаем inline, на десктопе — в телепорт
const isInline = ref(window.innerWidth < 768);
</script>
5. Несколько Teleport в одну цель
Порядок вставки сохраняется — элементы добавляются в порядке их появления в DOM:
<!-- ComponentA.vue -->
<Teleport to="#shared-target">
<div class="first">Первый</div>
</Teleport>
<!-- ComponentB.vue -->
<Teleport to="#shared-target">
<div class="second">Второй</div>
</Teleport>
Результат в #shared-target:
<div id="shared-target">
<div class="first">Первый</div>
<div class="second">Второй</div>
</div>
6. Важные нюансы
Реактивность и события работают как обычно:
<!-- Parent.vue -->
<template>
<div>
<Teleport to="#target">
<ChildComponent @click="handleClick" :data="reactiveData" />
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue';
const reactiveData = ref({ count: 0 });
function handleClick() {
reactiveData.value.count++;
// Событие всплывает как обычно, несмотря на Teleport
}
</script>
Целевой элемент должен существовать:
<script setup>
import { onMounted, ref } from 'vue';
const targetReady = ref(false);
onMounted(() => {
// Создаём целевой элемент, если его нет
if (!document.getElementById('dynamic-target')) {
const div = document.createElement('div');
div.id = 'dynamic-target';
document.body.appendChild(div);
}
targetReady.value = true;
});
</script>
<template>
<Teleport v-if="targetReady" to="#dynamic-target">
<div>Содержимое</div>
</Teleport>
</template>
7. Полный пример: модальное окно
<!-- Modal.vue -->
<template>
<Teleport to="#modal-root">
<Transition name="modal">
<div v-if="isOpen" class="modal-overlay" @click.self="close">
<div class="modal-container">
<header class="modal-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="close-btn" @click="close">×</button>
</header>
<main class="modal-body">
<slot />
</main>
<footer class="modal-footer">
<slot name="footer">
<button @click="close">Отмена</button>
<button @click="$emit('confirm')">Подтвердить</button>
</slot>
</footer>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
defineProps({
isOpen: Boolean,
title: String
});
const emit = defineEmits(['close', 'confirm']);
function close() {
emit('close');
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-container {
background: white;
border-radius: 8px;
min-width: 400px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
}
/* Анимации */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>
8. Аналогия в React и Go
В React аналогом является ReactDOM.createPortal:
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return createPortal(
<div className="modal">{children}</div>,
document.getElementById('modal-root')
);
}
В Go нет прямого аналога, так как Go не работает с DOM. Концептуально это ближе к паттерну «рендер в произвольное место», который в серверных приложениях реализуется через шаблонизаторы с возможностью вставки контента в разные секции страницы (например, template в Go html/template).
Вопрос 10. Для чего используется nextTick в Vue? В каких случаях он необходим?
Таймкод: 00:14:59
Ответ собеседника: Правильный. nextTick используется для гарантии работы с полностью отрендеренным DOM после последнего изменения состояния. Он позволяет выполнить код после того, как Vue обновил DOM в результате реактивных изменений.
Правильный ответ:
Ответ верный. Дополним деталями о работе nextTick и практическими примерами.
1. Почему nextTick необходим
Vue обновляет DOM асинхронно и батчит изменения. Когда вы изменяете реактивные данные, Vue не обновляет DOM мгновенно — он помещает обновление в очередь и применяет все изменения за один тик в конце текущего цикла Event Loop. Это оптимизация, которая предотвращает лишние ре-рендеры.
const count = ref(0);
count.value = 5;
// В этот момент DOM ещё НЕ обновлён!
console.log(document.getElementById('counter').textContent); // старое значение
await nextTick();
// Теперь DOM обновлён
console.log(document.getElementById('counter').textContent); // "5"
2. Как работает nextTick под капотом
nextTick возвращает Promise, который резолвится после следующего цикла обновления DOM. Он использует микрозадачу (microtask) — аналогично Promise.resolve().then().
// Vue 3 внутренняя реализация (упрощённо)
function nextTick(fn) {
const p = Promise.resolve();
return fn ? p.then(fn) : p;
}
3. Практические сценарии использования
А. Фокус на элемент после его появления:
<script setup>
import { ref, nextTick } from 'vue';
const showInput = ref(false);
const inputRef = ref(null);
async function toggleInput() {
showInput.value = true;
// Без nextTick — inputRef.value будет null
await nextTick();
inputRef.value.focus();
}
</script>
<template>
<button @click="toggleInput">Показать инпут</button>
<input v-if="showInput" ref="inputRef" />
</template>
Б. Работа с обновлёнными размерами элемента:
<script setup>
import { ref, nextTick } from 'vue';
const items = ref([]);
const listRef = ref(null);
async function addItem() {
items.value.push('Новый элемент');
await nextTick();
// Теперь можно получить актуальную высоту
const height = listRef.value.scrollHeight;
listRef.value.scrollTop = height; // прокрутка вниз
}
</script>
<template>
<div ref="listRef" class="list">
<div v-for="item in items" :key="item">{{ item }}</div>
</div>
<button @click="addItem">Добавить</button>
</template>
В. Инициализация сторонних библиотек:
<script setup>
import { ref, onMounted, nextTick } from 'vue';
const chartRef = ref(null);
const chartInstance = ref(null);
async function loadData() {
// Загрузка данных...
const newData = await fetchChartData();
// Обновляем реактивные данные
chartData.value = newData;
// Ждём, пока Vue обновит DOM
await nextTick();
// Инициализируем или обновляем стороннюю библиотеку
if (!chartInstance.value) {
chartInstance.value = new Chart(chartRef.value, {
type: 'line',
data: chartData.value
});
} else {
chartInstance.value.update();
}
}
</script>
Г. Тестирование компонентов:
// Тест с Vue Test Utils
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
test('обновляет сообщение после клика', async () => {
const wrapper = mount(MyComponent);
await wrapper.find('button').trigger('click');
// Без nextTick — DOM ещё не обновлён
await nextTick();
expect(wrapper.find('.message').text()).toBe('Обновлено!');
});
4. nextTick vs setTimeout
// ❌ Неправильно — setTimeout менее надёжен
count.value = 5;
setTimeout(() => {
// DOM может быть обновлён, а может и нет
// Зависит от задержки и нагрузки
}, 0);
// ✅ Правильно — nextTick гарантирует обновление DOM
count.value = 5;
await nextTick();
// DOM точно обновлён
nextTick предпочтительнее setTimeout, потому что:
- Он срабатывает раньше (микрозадача vs макрозадача).
- Он гарантированно срабатывает после обновления DOM Vue.
- Не зависит от производительности устройства.
5. Несколько изменений подряд
Vue батчит изменения, поэтому даже множество изменений вызовут только один ре-рендер:
async function updateMultiple() {
firstName.value = 'Alice';
lastName.value = 'Smith';
age.value = 30;
// Все три изменения применятся за один ре-рендер
await nextTick();
// DOM обновлён со всеми тремя изменениями
}
6. Использование с Composition API
<script setup>
import { ref, nextTick } from 'vue';
const count = ref(0);
// Вариант 1: async/await
async function incrementAndLog() {
count.value++;
await nextTick();
console.log('DOM обновлён');
}
// Вариант 2: callback (как в Vue 2)
function incrementAndLogOld() {
count.value++;
nextTick(() => {
console.log('DOM обновлён');
});
}
</script>
7. Аналогия в Go
В Go нет прямого аналога nextTick, так как Go работает с синхронным выполнением и не имеет реактивного DOM. Однако концептуально это похоже на ожидание завершения горутины через канал:
done := make(chan struct{})
go func() {
// Асинхронная работа
updateData()
close(done)
}()
// Ждём завершения
<-done
// Теперь можно работать с обновлёнными данными
В контексте веб-разработки на Go (например, с использованием htmx или Alpine.js) аналогом может быть ожидание завершения HTTP-запроса или WebSocket-сообщения перед обновлением UI.
Вопрос 11. Чем отличаются хуки created и mounted в жизненном цикле компонента Vue?
Таймкод: 00:15:36
Ответ собеседника: Правильный. На момент created компонент уже создан, инициализирован Options API, готов виртуальный DOM, созданы узлы, но компонент ещё не вставлен в реальный DOM. На момент mounted компонент уже полностью отрендерен и вставлен в реальный DOM, доступны ссылки на DOM-элементов.
Правильный ответ:
Ответ верный. Дополним полной картиной жизненного цикла и практическими примерами.
1. Жизненный цикл компонента Vue
Полная последовательность хуков:
setup (Composition API) / beforeCreate (Options API)
↓
created
↓
beforeMount
↓
mounted
↓
beforeUpdate (при изменении реактивных данных)
↓
updated
↓
beforeUnmount
↓
unmounted
2. Что доступно на каждом этапе
created (Options API) / setup (Composition API):
- Реактивные данные инициализированы.
- Вычисляемые свойства (computed) доступны.
- Методы доступны.
- Нет доступа к DOM-элементам.
- Нет доступа к
$el,$refs, дочерним компонентам.
mounted:
- Компонент вставлен в реальный DOM.
- Доступны
$el,$refs, дочерние компоненты. - Можно измерять размеры элементов, инициализировать библиотеки.
3. Примеры использования
created — инициализация данных, API-запросы:
<script>
export default {
data() {
return {
users: [],
loading: false
};
},
created() {
// Хорошее место для загрузки данных
this.fetchUsers();
},
methods: {
async fetchUsers() {
this.loading = true;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} finally {
this.loading = false;
}
}
}
};
</script>
mounted — работа с DOM:
<script>
export default {
data() {
return {
chartWidth: 0
};
},
mounted() {
// Измерение размеров элемента
this.chartWidth = this.$refs.chartContainer.offsetWidth;
// Инициализация сторонней библиотеки
this.chart = new Chart(this.$refs.canvas, {
type: 'bar',
data: this.chartData
});
// Подписка на глобальные события
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
// Очистка
window.removeEventListener('resize', this.handleResize);
this.chart?.destroy();
},
methods: {
handleResize() {
this.chartWidth = this.$refs.chartContainer.offsetWidth;
this.chart?.resize();
}
}
};
</script>
<template>
<div ref="chartContainer">
<canvas ref="canvas"></canvas>
</div>
</template>
4. Composition API — onMounted и хуки setup
В Composition API хуки жизненного цикла вызываются внутри setup() или <script setup>:
<script setup>
import { ref, onMounted, onBeforeMount, onBeforeUnmount, onUnmounted } from 'vue';
const users = ref([]);
const containerRef = ref(null);
// setup() выполняется на этапе created
// Загрузка данных
async function fetchUsers() {
const response = await fetch('/api/users');
users.value = await response.json();
}
fetchUsers();
onBeforeMount(() => {
console.log('beforeMount — DOM ещё не создан');
});
onMounted(() => {
console.log('mounted — DOM доступен');
console.log(containerRef.value.offsetWidth); // работает!
});
onBeforeUnmount(() => {
console.log('beforeUnmount — очистка ресурсов');
});
onUnmounted(() => {
console.log('unmounted — компонент удалён');
});
</script>
<template>
<div ref="containerRef">
<div v-for="user in users" :key="user.id">{{ user.name }}</div>
</div>
</template>
5. Когда использовать created, а когда mounted
Используйте created (или setup):
- Загрузка данных с API.
- Инициализация реактивных данных.
- Подписка на события (если не нужен DOM).
- Настройка watchers.
Используйте mounted:
- Доступ к DOM-элементам (
$refs,querySelector). - Измерение размеров элементов (
offsetWidth,getBoundingClientRect). - Инициализация сторонних библиотек (карты, графики, редакторы).
- Подписка на события, связанные с DOM (
resize,scroll). - Установка фокуса на элемент.
6. SSR (Server-Side Rendering)
Важный нюанс: при SSR хуки mounted, beforeMount, updated, beforeUpdate НЕ вызываются на сервере. Только created и beforeCreate выполняются на сервере.
export default {
created() {
// Выполняется и на сервере, и на клиенте
console.log('created');
},
mounted() {
// Выполняется ТОЛЬКО на клиенте
console.log('mounted');
// Безопасный доступ к window
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.handleResize);
}
}
};
7. Частая ошибка: доступ к DOM в created
export default {
created() {
// ❌ Не работает — DOM ещё не создан
console.log(this.$refs.myInput); // undefined
this.$refs.myInput.focus(); // TypeError
},
mounted() {
// ✅ Работает — DOM создан
console.log(this.$refs.myInput); // <input>
this.$refs.myInput.focus(); // OK
}
};
8. Аналогия в Go
Концептуально хуки жизненного цикла Vue похожи на паттерны инициализации в Go:
- created → конструктор структуры или
init()функция (инициализация полей). - mounted → метод
Start()после полной инициализации (работа с внешними ресурсами). - unmounted → метод
Close()илиdefer(освобождение ресурсов).
type Server struct {
db *sql.DB
router *mux.Router
}
// created — инициализация полей
func NewServer() *Server {
return &Server{
router: mux.NewRouter(),
}
}
// mounted — запуск после полной инициализации
func (s *Server) Start(addr string) error {
db, err := sql.Open("postgres", "connection_string")
if err != nil {
return err
}
s.db = db
return http.ListenAndServe(addr, s.router)
}
// unmounted — освобождение ресурсов
func (s *Server) Close() error {
return s.db.Close()
}
Вопрос 12. Как при создании компонента модального окна, который будет использоваться многократно в приложении, задать каждому экземпляру уникальный ID в DOM-дереве?
Таймкод: 00:16:34
Ответ собеседния: Неполный. Предложен вариант с глобальным стейтом и счётчиком (Vuex/Pinia), где при каждом создании компонента счётчик увеличивается. Не был упомянут встроенный механизм Vue — свойство $uid (или useId в Vue 3.5+), которое автоматически генерирует уникальный идентификатор для каждого компонента.
Правильный ответ:
Ответ собеседника рабочий, но избыточный. Vue предоставляет встроенные механизмы для этой задачи.
1. Vue 3.5+ — useId (рекомендуемый способ)
Начиная с Vue 3.5, доступна функция useId, которая генерирует уникальный идентификатор для каждого экземпляра компонента:
<script setup>
import { useId } from 'vue';
const id = useId();
// id будет уникальным для каждого экземпляра компонента
// Например: "v-0", "v-1", "v-2" и т.д.
</script>
<template>
<div :id="`modal-${id}`" class="modal">
<label :for="`modal-title-${id}`">Заголовок</label>
<input :id="`modal-title-{id}`" type="text" />
<button :aria-controls="`modal-${id}`">Открыть</button>
</div>
</template>
2. Vue 3 до 3.5 — ручная генерация
Если версия Vue ниже 3.5, можно использовать composable:
// composables/useId.js
import { getCurrentInstance } from 'vue';
let globalId = 0;
export function useId(prefix = 'v') {
const instance = getCurrentInstance();
if (!instance._uid) {
instance._uid = ++globalId;
}
return `${prefix}-${instance._uid}`;
}
<script setup>
import { useId } from '@/composables/useId';
const modalId = useId('modal');
</script>
<template>
<div :id="modalId" class="modal">
<!-- содержимое -->
</div>
</template>
3. Vue 2 — $uid
В Vue 2 каждый компонент автоматически получает свойство $uid:
<template>
<div :id="`modal-${$uid}`" class="modal">
<h2 :id="`modal-title-${$uid}`">Заголовок</h2>
</div>
</template>
4. Полный пример: модальное окно с уникальным ID
<!-- Modal.vue -->
<script setup>
import { useId } from 'vue';
const props = defineProps({
title: String,
isOpen: Boolean
});
const emit = defineEmits(['close']);
const id = useId();
const titleId = `${id}-title`;
const descId = `${id}-desc`;
</script>
<template>
<Teleport to="#modal-root">
<Transition name="modal">
<div
v-if="isOpen"
:id="id"
class="modal-overlay"
role="dialog"
:aria-labelledby="titleId"
:aria-describedby="descId"
aria-modal="true"
@click.self="emit('close')"
>
<div class="modal-content">
<header class="modal-header">
<h2 :id="titleId">{{ title }}</h2>
<button
@click="emit('close')"
:aria-controls="id"
aria-label="Закрыть"
>
×
</button>
</header>
<main :id="descId" class="modal-body">
<slot />
</main>
<footer class="modal-footer">
<slot name="footer">
<button @click="emit('close')">Отмена</button>
<button @click="emit('confirm')">Подтвердить</button>
</slot>
</footer>
</div>
</div>
</Transition>
</Teleport>
</template>
Использование:
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';
const showModal1 = ref(false);
const showModal2 = ref(false);
const showModal3 = ref(false);
</script>
<template>
<div>
<button @click="showModal1 = true">Открыть модалку 1</button>
<button @click="showModal2 = true">Открыть модалку 2</button>
<button @click="showModal3 = true">Открыть модалку 3</button>
<!-- Каждая модалка получит уникальный ID -->
<Modal title="Первая" :is-open="showModal1" @close="showModal1 = false" />
<Modal title="Вторая" :is-open="showModal2" @close="showModal2 = false" />
<Modal title="Третья" :is-open="showModal3" @close="showModal3 = false" />
</div>
<div id="modal-root"></div>
</template>
5. Почему не стоит использовать Pinia/Vuex для этого
Решение с глобальным стейтом и счётчиком:
- Избыточно — требует создания store, мутаций, действий.
- Не работает с SSR — счётчик будет общим для всех запросов на сервере.
- Сложнее в поддержке — дополнительный код и зависимости.
- Не учитывает демонтирование компонентов — ID никогда не освобождаются.
6. Альтернатива: передача ID через props
Если нужен контроль над ID извне:
<!-- Modal.vue -->
<script setup>
import { useId } from 'vue';
const props = defineProps({
id: String, // опциональный внешний ID
title: String,
isOpen: Boolean
});
const internalId = useId();
const modalId = props.id || internalId;
</script>
<template>
<div :id="modalId" class="modal">
<!-- ... -->
</div>
</template>
Использование:
<!-- С автоматическим ID -->
<Modal title="Авто" :is-open="show" @close="show = false" />
<!-- С явным ID -->
<Modal id="user-edit-modal" title="Редактирование" :is-open="show" @close="show = false" />
7. Аналогия в Go
В Go для генерации уникальных идентификаторов обычно используют:
import "github.com/google/uuid"
func createModal() Modal {
return Modal{
ID: uuid.New().String(),
}
}
Или для последовательных ID — sync/Atomic:
var globalID uint64
func nextID() uint64 {
return atomic.AddUint64(&globalID, 1)
}
Vue useId работает по аналогичному принципу, но автоматически привязывает ID к экземпляру компонента и корректно обрабатывает SSR.
Вопрос 13. Какие хуки роутера Vue Router вы знаете? Для чего они используются?
Таймкод: 00:18:44
Ответ собеседника: Правильный. Упомянуты хуки-гарды (navigation guards) в Vue Router. Они используются для проверки условий перед переходом на страницу: проверка токена авторизации, проверка заполнения определённых этапов, ограничение доступа к страницам. Если условия не выполнены — пользователь перенаправляется (redirect).
Правильный ответ:
Ответ покрывает основное назначение хуков роутера. Дополним полным списком хуков и их классификацией.
1. Классификация хуков Vue Router
Хуки роутера делятся на три категории:
- Глобальные гарды (Global Guards) — применяются ко всем маршрутам.
- Гарды маршрута (Route-specific Guards) — применяются к конкретному маршруту.
- Гарды компонента (Component Guards) — вызываются внутри компонента.
2. Глобальные гарды
beforeEach — выполняется перед каждой навигацией:
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
{ path: '/admin', component: Admin, meta: { requiresAuth: true, role: 'admin' } }
]
});
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
// Проверка авторизации
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return next({ path: '/login', query: { redirect: to.fullPath } });
}
// Проверка роли
if (to.meta.role && authStore.userRole !== to.meta.role) {
return next({ path: '/403' });
}
// Проверка заполнения профиля
if (to.meta.requiresProfile && !authStore.isProfileComplete) {
return next({ path: '/complete-profile' });
}
next(); // обязательно вызвать next()
});
beforeResolve — после разрешения всех async-гардов компонентов:
router.beforeResolve((to, from) => {
// Выполняется после того, как все beforeEnter и beforeRouteEnter разрешены
// Полезно для финальных проверок перед активацией маршрута
if (to.meta.requiresPayment && !hasActiveSubscription()) {
return { path: '/billing' };
}
});
afterEach — после успешной навигации:
router.afterEach((to, from) => {
// Аналитика
gtag('config', 'GA_TRACKING_ID', { page_path: to.fullPath });
// Заголовок страницы
document.title = to.meta.title || 'Default Title';
// Скролл вверх
window.scrollTo(0, 0);
// Закрытие мобильного меню
const uiStore = useUIStore();
uiStore.closeMobileMenu();
});
onError — обработка ошибок навигации:
router.onError((error) => {
if (error.message.includes('Failed to fetch dynamically imported module')) {
// Обработка ошибки загрузки ленивого компонента
window.location.reload();
}
console.error('Router error:', error);
});
3. Гарды конкретного маршрута
beforeEnter — выполняется при входе в конкретный маршрут:
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
const authStore = useAuthStore();
if (!authStore.isAdmin) {
next({ path: '/403' });
} else {
next();
}
}
}
];
// Можно указать массив гардов
const adminGuard = (to, from, next) => { /* ... */ };
const logGuard = (to, from, next) => { /* ... */ };
{
path: '/admin',
component: Admin,
beforeEnter: [adminGuard, logGuard]
}
4. Гарды внутри компонента
beforeRouteEnter — вызывается до рендеринга компонента:
<script>
export default {
beforeRouteEnter(to, from, next) {
// Нет доступа к this — компонент ещё не создан
// Можно получить доступ через next(vm => { ... })
fetchUserData(to.params.id).then(user => {
next(vm => {
vm.user = user;
});
});
}
};
</script>
beforeRouteUpdate — при изменении параметров маршрута:
<script>
export default {
beforeRouteUpdate(to, from) {
// Есть доступ к this
// Вызывается, когда маршрут изменился, но компонент переиспользуется
// Например: /users/1 → /users/2
this.fetchUser(to.params.id);
},
methods: {
fetchUser(id) {
// Загрузка данных пользователя
}
}
};
</script>
beforeRouteLeave — при уходе с текущего маршрута:
<script>
export default {
data() {
return {
isDirty: false
};
},
beforeRouteLeave(to, from, next) {
if (this.isDirty) {
const answer = window.confirm('У вас есть несохранённые изменения. Уйти?');
if (answer) {
next();
} else {
next(false); // отмена навигации
}
} else {
next();
}
}
};
</script>
5. Composition API — onBeforeRouteUpdate, onBeforeRouteLeave
В Vue 3 Composition API:
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
import { ref } from 'vue';
const isDirty = ref(false);
onBeforeRouteLeave((to, from) => {
if (isDirty.value) {
const answer = window.confirm('Уйти без сохранения?');
if (!answer) {
return false;
}
}
});
onBeforeRouteUpdate((to, from) => {
// Обновление данных при изменении параметров
fetchData(to.params.id);
});
</script>
6. Полный пример: защищённый маршрут
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { guestOnly: true }
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
},
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin', 'moderator'] }
},
{
path: '/profile',
component: () => import('@/views/Profile.vue'),
meta: { requiresAuth: true }
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFound.vue')
}
]
});
// Глобальная гарда авторизации
router.beforeEach(async (to, from) => {
const authStore = useAuthStore();
// Инициализация пользователя при первом заходе
if (!authStore.initialized) {
await authStore.initialize();
}
// Страницы только для гостей (login, register)
if (to.meta.guestOnly && authStore.isAuthenticated) {
return { path: '/dashboard' };
}
// Защищённые страницы
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return { path: '/login', query: { redirect: to.fullPath } };
}
// Проверка ролей
if (to.meta.roles && !to.meta.roles.includes(authStore.userRole)) {
return { path: '/403' };
}
});
// После навигации — обновление заголовка
router.afterEach((to) => {
const defaultTitle = 'My App';
const title = to.meta.title ? `${to.meta.title} | ${defaultTitle}` : defaultTitle;
document.title = title;
});
export default router;
7. Порядок выполнения гард
1. beforeEach (глобальный)
2. beforeEnter (маршрут)
3. beforeRouteEnter (компонент)
4. beforeResolve (глобальный)
5. afterEach (глобальный)
При изменении параметров (переиспользование компонента):
1. beforeEach
2. beforeRouteUpdate (компонент)
3. beforeResolve
4. afterEach
8. Аналогия в Go
Концепция гардов похожа на middleware в Go:
// Gin middleware
router := gin.Default()
// Глобальная гарда (beforeEach)
router.Use(authMiddleware())
// Гарда для группы маршрутов (beforeEnter)
admin := router.Group("/admin")
admin.Use(adminOnlyMiddleware())
{
admin.GET("/users", listUsers)
admin.GET("/settings", settings)
}
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.Redirect(302, "/login")
c.Abort()
return
}
c.Next()
}
}
