Перейти к основному содержимому

РЕАЛЬНОЕ FRONTEND VUE СОБЕСЕДОВАНИЕ НА ЗП 280К (REACT МЕРТВ)! MIDDLE/SENIOR ФРОНТЕНД СОБЕСЕДОВАНИЕ!

· 72 мин. чтения

Сегодня мы разберём реальное собеседование на позицию middle+ Vue-разработчика, в ходе которого кандидат последовательно прошёл несколько этапов: обсуждение опыта и командных процессов, кодревью двух компонентов на Vue 3 с акцентом на реактивность, обработку событий и утечки памяти, а также блок теоретических вопросов по CSS, TypeScript, браузерным API и архитектурным подходам, включая FSD и чистые компоненты. В завершающей части кандидат продемонстрировал практические навыки, реализовав класс EventEmitter и функцию для безопасного извлечения вложенных свойств объекта, после чего получил положительный фидбэк и приглашение на финальный этап в офис.

Вопрос 1. Как распределяется команда по ролям и сколько человек в ней?

Таймкод: 00:01:11

Ответ собеседника: Правильный. Команда состоит из около 12 человек: два фронтенд-разработчика, один тимлид фронтенда, три бэкенд-разработчика, аналитик, системный аналитик, бизнес-аналитик, дизайнер, тестировщик, проектный менеджер и продакт-оунер.

Правильный ответ:

Описанная структура команды является классической для кросс-функциональной команды (cross-functional team) в продуктовой разработке. Давайте разберём её подробнее.

Роли и их функции

Бэкенд-разработка — три разработчика и тимлид фронтенда. Тимлид выполняет роль технического руководителя фронтенда, но при этом сам может писать код. Три бэкенд-разработчика отвечают за серверную часть — API, бизнес-логику, работу с базами данных, интеграции с внешними сервисами.

Фронтенд-разработка — два фронтенд-разработчика под руководством тимлида фронтенда. Они реализуют пользовательский интерфейс, обеспечивают адаптивность, работают с состоянием приложения.

Аналитика — три вида аналитиков покрывают разные уровни:

  • Бизнес-аналитик работает с бизнес-требованиями, метриками, ROI
  • Системный аналитик описывает технические требования, проектирует архитектуру системы
  • Аналитик (возможно, продуктовый аналитик) работает с данными, A/B-тестами, пользовательским поведением

Дизайнер — UX/UI дизайнер проектирует интерфейсы, создаёт макеты, проводит исследования пользовательского опыта.

Тестировщик — QA-инженер обеспечивает качество продукта, пишет тесты, проводит ручное и автоматизированное тестирование.

Управление — проектный менеджер координирует процессы, управляет сроками и рисками. Продакт-оунер определяет стратегию продукта, приоритезирует фичи.

Особенности такой структуры

Размер команды в 12 человек находится на верхней границе рекомендуемого размера по Scrum (5-9 человек). Однако для зрелого продукта с полным циклом разработки это допустимо. Ключевое — чёткое разделение ответственности и минимизация зависимостей между ролями.

Для Golang-разработчика важно понимать контекст взаимодействия: бэкенд-разработчики работают в тесном контакте с системным аналитиком для проектирования API, с фронтенд-разработчиками для определения контрактов, с тестировщиком для обеспечения покрытия тестами.

Вопрос 2. Как проходят ежедневные встречи (дейлики) в команде?

Таймкод: 00:01:01:44

Ответ собеседника: Правильный. Все участники команды приходят на дейлик вместе, кроме девопса, так как он распределен на несколько команд одновременно.

Правильный ответ:

Описанный формат дейликов соответствует стандартной практике. Давайте разберём детали.

Стандартный формат дейлика

Ежедневная встреча (daily standup) обычно длится 15 минут и проводится в одно и то же время. Каждый участник отвечает на три вопроса:

  1. Что я сделал вчера?
  2. Что планирую сделать сегодня?
  3. Есть ли блокеры?

Распределённые ресурсы

Ситуация с DevOps-инженером, распределённым на несколько команд, распространена. Он может присутствовать на дейлике только при наличии актуальных вопросов. Вместо этого команда может использовать асинхронную коммуникацию — вопросы к DevOps-инженеру отправляются в чат, а ответы приходят в течение дня.

Рекомендации для эффективных дейликов

Дейлик — не статус-митинг для менеджера. Это встреча для команды, чтобы синхронизировать работу. Если возникает обсуждение конкретной проблемы, оно продолжается после дейлика только с релевантными участниками.

Для бэкенд-разработчика на Golang важно чётко формулировать блокеры: если задача задерживается из-за зависимости от другой команды или проблем с инфраструктурой, это должно быть озвучено сразу.

Альтернативные форматы

В некоторых командах используют асинхронные дейлики через мессенджеры или инструменты типа Geekbot, особенно при распределённой работе. Это экономит время и сохраняет прозрачность.

Вопрос 3. Имеется ли опыт работы с микрофронтендами в рамках Vue?

Таймкод: 00:02:10

Ответ собеседника: Правильный. Опыт работы с микрофронтендами есть, но он получен на некоммерческом проекте при самостоятельном изучении Module Federation в Vue.

Правильный ответ:

Микрофронтенды — это архитектурный подход, при котором монолитное фронтенд-приложение разбивается на независимые части, разрабатываемые отдельными командами.

Module Federation

Webpack Module Federation — основной инструмент для реализации микрофронтендов. Он позволяет динамически загружать модули из разных бандлов во время выполнения.

Пример конфигурации для Vue:

// webpack.config.js (Host)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
}),
],
};

// webpack.config.js (Remote)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.vue',
},
}),
],
};

Альтернативные подходы

Помимо Module Federation существуют:

  • iframe — простой, но с ограничениями по интеграции
  • Web Components — стандартный браузерный подход
  • Single SPA — фреймворк для объединения микрофронтендов
  • qiankun — популярное решение на основе single-spa

Связь с бэкенд-архитектурой

Для Golang-разработчика важно понимать, что микрофронтенды часто идут рука об руку с микросервисной архитектурой на бэкенде. Каждый микрофронтенд может обращаться к своему микросервису через API Gateway. Это создаёт единообразие архитектуры и позволяет командам работать автономно.

Вопрос 4. Какие задачи предпочитает решать: технические или продуктовые?

Таймкод: 00:02:31

Ответ собеседника: Правильный. Предпочтение отдается продуктовым задачам (примерно 60/40 в пользу продуктовых), так как разработка функционала для бизнеса и пользователей приносит больше удовольствия. Технические задачи также нормально воспринимаются, но они менее интересны, так как часто сводятся к одноразовой настройке конфигураций.

Правильный ответ:

Данный ответ демонстрирует зрелый подход к работе — разработчик понимает ценность продуктовых задач и готов решать технические.

Продуктовые задачи

Это разработка функционала, который напрямую влияет на пользовательский опыт и бизнес-метрики. Примеры:

  • Реализация нового эндпоинта для мобильного приложения
  • Оптимизация скорости загрузки данных
  • Добавление новых фильтров и сортировок

Продуктовые задачи дают видимый результат и обратную связь от пользователей, что мотивирует.

Технические задачи

Это инфраструктурные и платформенные задачи:

  • Настройка CI/CD
  • Миграция на новую версию зависимостей
  • Рефакторинг унаследованного кода
  • Написание утилит и библиотек

Хотя кандидат правильно отмечает, что некоторые технические задачи сводятся к настройке конфигураций, важно понимать их ценность. Технический долг, если его не решать, замедляет разработку продуктовых фич.

Баланс в реальной работе

Оптимальное соотношение зависит от стадии проекта. На старте — 80/20 в пользу продуктовых. В зрелом проекте — 50/50 для поддержания здоровья кодовой базы. Ответ кандидата 60/40 показывает здоровый баланс и понимание бизнес-ценности работы.

Вопрос 5. Какие проекты или задачи вызывали наибольшую мотивацию и чувство эйфории от успешного завершения?

Таймкод: 00:03:50

Ответ собеседника: Правильный. Чувство эйфории возникает при успешном завершении крупных задач, над которыми работал 2-3 недели, особенно если они успешно проходят тестирование и попадают в продакшн. Также приносит удовольствие сборка проекта с нуля и настройка всех конфигураций (линтеры, CI/CD), когда в итоге все работает единообразно и без ошибок.

Правильный ответ:

Ответ показывает два типа мотивации, характерных для опытных разработчиков.

Завершение крупных задач

Работа над задачей 2-3 недели создаёт эмоциональную привязанность. Когда такая задача успешно проходит тестирование и деплоится в продакшн, это даёт сильное чувство удовлетворения. Это нормальная и здоровая реакция — она показывает, что разработчик вовлечён в результат.

Сборка проекта с нуля

Настройка инфраструктуры — отдельный вид удовольствия. Когда линтеры, CI/CD, тесты работают единообразно, это создаёт ощущение порядка и контроля. Пример минимального CI/CD для Golang-проекта:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- run: go vet ./...
- run: go test ./... -race -coverprofile=coverage.out
- run: go build -v ./...

Почему это важно на интервью

Ответ демонстрирует, что кандидат получает удовольствие не только от написания кода, но и от создания целостных систем. Это признак инженерного мышления — видеть картину целиком и доводить дела до конца.

Вопрос 6. Что может мешать в процессе разработки задачи и как относишься к отвлекающим факторам (например, срочным звонкам)?

Таймкод: 00:05:38

Ответ собеседника: Правильный. Мешать могут непредвиденные ошибки, которые сложно найти, а также срочные созвоны (например, груминг посередине дня), которые отвлекают от фокуса на задаче. К таким ситуациям отношение нормальное, так как от этого никуда не деться. Если звонок не требует активного участия, можно продолжать работу над задачей параллельно.

Правильный ответ:

Ответ демонстрирует прагматичный подход к неизбежным отвлечениям в командной работе.

Типичные блокеры в разработке

Технические блокеры:

  • Непредвиденные ошибки, которые сложно воспроизвести
  • Зависимость от другой команды или внешнего сервиса
  • Проблемы с окружением (dev/staging)
  • Непонятная документация или её отсутствие

Организационные блокеры:

  • Срочные встречи (груминг, планирование) посреди рабочего дня
  • Запросы на code review от коллег
  • Инциденты в продакшне
  • Уточнение требований от аналитика или продакта

Стратегии минимизации влияния

Time-blocking — выделять блоки времени для глубокой работы (deep work). Например, утро до 12:00 — только код, без встреч.

Правило двух минут — если задача занимает меньше двух минут, сделать её сразу. Если больше — запланировать.

Коммуникация статуса — если отвлечение критично, сообщить в командный чат: «Отвлечён на инцидент, задача X задержится на 2 часа».

Параллельная работа на звонках

Кандидат правильно отметил, что если звонок не требует активного участия, можно продолжать работу. Однако важно не злоупотреблять этим — если на звонке обсуждается задача, которая может повлиять на текущую работу, лучше слушать внимательно.

Вопрос 7. Как действуешь, если в процессе разработки выявляешь недочеты в ТЗ?

Таймкод: 00:07:57

Ответ собеседника: Правильный. В случае обнаружения недочетов в ТЗ, первым делом сообщаю об этом постановщику задачи, описываю ситуацию и проблему. Дальнейшие действия зависят от его решения, чтобы избежать необходимости переписывать код, если будет предложено другое решение.

Правильный ответ:

Данный подход является правильным и демонстрирует профессионализм. Давайте разберём подробнее.

Типичные недочёты в ТЗ

  • Противоречия между разделами требований
  • Отсутствие описания edge-cases (граничные случаи)
  • Неопределённость в бизнес-логике
  • Конфликт с существующей архитектурой системы
  • Невозможность реализации в установленные сроки

Алгоритм действий

1. Зафиксировать проблему. Записать конкретное место в ТЗ, описание недочёта и возможные последствия. Это важно для прозрачности.

2. Сообщить постановщику. Как правило, это системный аналитик или бизнес-аналитик. Лучше писать в письменном виде (чат, комментарий в задаче), чтобы сохранить историю.

3. Предложить варианты решения. Не просто указать на проблему, а предложить 1-2 варианта решения с оценкой влияния на сроки.

4. Получить решение и зафиксировать. После получения ответа обновить ТЗ или добавить комментарий в задачу.

5. Приостановить разработку по этому пункту. Пока решение не принято, не продолжать реализацию спорного функционала, чтобы избежать переделки.

Пример формулировки

Вместо «В ТЗ ошибка» лучше написать: «В разделе 3.2 описано поведение X, но в разделе 5.1 подразумевается поведение Y. Какой вариант корректный? Если Y — потребуется переработка схемы БД, сроки сдвинутся на 2 дня».

Такой подход экономит время всей команды и предотвращает переделку кода.

Вопрос 8. Можно ли открыть ссылку на код и расшарить экран для обсуждения?

Таймкод: 00:10:23

Ответ собеседника: Правильный. Да, код открыт (компонент Hello), готов расшарить экран и обсудить его.

Правильный ответ:

Готовность показать код и обсудить его — важный навык на техническом интервью. Это демонстрирует открытость и уверенность в своём коде.

Подготовка к обсуждению кода

Перед тем как расшарить экран, полезно:

  • Убедиться, что код компилируется и запускается
  • Закрыть ненужные вкладки и окна
  • Иметь под рукой документацию, если нужны ссылки
  • Подготовить краткое описание контекста: что делает компонент, какие задачи решает

Что оценивает интервьюер

При обсуждении кода интервьюер обращает внимание на:

  • Структуру и читаемость кода
  • Именование переменных и функций
  • Обработку ошибок
  • Понимание кандидатом своего кода
  • Способность объяснить принятые решения

Совет

Если код не идеален, лучше честно сказать: «Здесь я бы улучшил X, но в рамках задачи Y решил сделать проще». Это показывает инженерную зрелость и способность к рефлексии.

Вопрос 9. Что можно улучшить в компоненте поиска с точки зрения кода?

Таймкод: 00:10:43

Ответ собеседника: Правильный. Отсутствует ключ в списке, нужно использовать уникальный идентификатор (user.id), а не индекс. На сабмит нужно добавить модификатор prevent. Директиву v-html лучше не использовать из-за риска XSS-атак. Вместо ref лучше использовать reactive для ссылочных структур данных. В watch эффекте нужно следить за конкретным полем (select user) или использовать deep: true. В асинхронной функции нужно добавить try/catch/finally для обработки ошибок и перевода loading в false. Запросы через Axios лучше вынести в отдельный класс или state-менеджер, а не писать напрямую в компоненте.

Правильный ответ:

Кандидат дал развёрнутый и правильный ответ. Давайте систематизируем все замечания.

1. Ключ в списке (key)

Использование индекса массива как ключа — антипаттерн. Vue не сможет корректно отслеживать изменения элементов.

<!-- Плохо -->
<div v-for="(user, index) in users" :key="index">

<!-- Правильно -->
<div v-for="user in users" :key="user.id">

2. Модификатор prevent на форме

Без @submit.prevent форма вызовет перезагрузку страницы.

<!-- Плохо -->
<form @submit="handleSubmit">

<!-- Правильно -->
<form @submit.prevent="handleSubmit">

3. Директива v-html и XSS

v-html вставляет HTML напрямую, что создаёт уязвимость XSS, если данные содержат пользовательский ввод.

<!-- Плохо -->
<div v-html="user.bio"></div>

<!-- Правильно — использовать интерполяцию -->
<div>{{ user.bio }}</div>

<!-- Если HTML необходим — санитизировать -->
<div v-html="sanitize(user.bio)"></div>

4. ref vs reactive

Для объектов и массивов в Vue 3 Composition API лучше использовать reactive:

// Менее предпочтительно
const state = ref({ users: [], loading: false })

// Лучше
const state = reactive({
users: [],
loading: false,
})

5. Watch — слежение за конкретным полем

// Плохо — следит за всем объектом без необходимости
watch(state, () => { ... })

// Правильно — следит за конкретным полем
watch(() => state.selectedUser, (newVal) => { ... })

// Или для вложенных свойств
watch(() => state.filters.search, (newVal) => { ... }, { deep: true })

6. Обработка ошибок в асинхронных функциях

// Плохо
async function fetchUsers() {
this.loading = true
const response = await axios.get('/api/users')
this.users = response.data
this.loading = false
}

// Правильно
async function fetchUsers() {
this.loading = true
try {
const response = await axios.get('/api/users')
this.users = response.data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}

7. Вынос HTTP-запросов

// api/users.js
import axios from 'axios'

export const usersApi = {
search: (query) => axios.get('/api/users', { params: { q: query } }),
getById: (id) => axios.get(`/api/users/${id}`),
}

// В компоненте
import { usersApi } from '@/api/users'

async function fetchUsers() {
this.loading = true
try {
const { data } = await usersApi.search(this.query)
this.users = data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}

Дополнительные улучшения

  • Debounce для поиска — не отправлять запрос на каждый символ
  • Отмена запросов — использовать AbortController или CancelToken для отмены предыдущего запроса
  • Пагинация — при большом количестве результатов
  • Виртуальный скролл — для длинных списков
  • Accessibility — ARIA-атрибуты для доступности

Вопрос 10. Какие есть минусы у v-html и что использовать вместо ref для ссылочных структур данных?

Таймкод: 00:14:49

Ответ собеседника: Правильный. v-html уязвим к XSS-атакам, так как позволяет внедрить вредоносный код в HTML. Для ссылочных структур данных вместо ref лучше использовать reactive.

Правильный ответ:

Кандидат дал правильный краткий ответ. Разберём подробнее.

Проблемы v-html

XSS (Cross-Site Scripting) — главная уязвимость. Если данные содержат пользовательский вредоносный ввод, он будет выполнен как HTML/JavaScript.

<!-- Пользователь ввёл: <script>alert('XSS')</script> -->
<div v-html="userInput"></div>
<!-- Скрипт выполнится в браузере -->

Потеря реактивности. Элементы, вставленные через v-html, не обрабатываются Vue как шаблон. Директивы, обработчики событий, компоненты внутри v-html не работают.

SEO-проблемы. Контент, вставленный через v-html, может быть невидим для поисковых роботов, если загружается асинхронно.

Альтернативы v-html

<!-- Простой текст -->
<div>{{ text }}</div>

<!-- Если нужен форматированный текст — санитизация -->
<div v-html="sanitize(htmlContent)"></div>

<!-- Если нужны компоненты — render-функция или <component> -->
<component :is="dynamicComponent" />

Функция sanitize может использовать библиотеку DOMPurify:

import DOMPurify from 'dompurify'

function sanitize(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
})
}

ref vs reactive в Vue 3

import { ref, reactive } from 'vue'

// ref — для примитивов и одиночных значений
const count = ref(0)
const name = ref('John')
const isLoading = ref(false)

// reactive — для объектов и массивов
const state = reactive({
users: [],
filters: {
search: '',
role: null,
},
pagination: {
page: 1,
limit: 20,
},
})

// Важно: reactive нельзя использовать с примитивами
// const count = reactive(0) // Ошибка!

// Деструктуризация reactive теряет реактивность
const { users } = state // потеря реактивности
const { users } = toRefs(state) // правильно

Когда что использовать

  • ref — примитивы, значения, которые будут перезаписываться целиком
  • reactive — объекты и массивы, где меняются отдельные свойства

Для Golang-разработчика аналогия: ref — это как указатель *int, reactive — как структура struct, поля которой можно менять по ссылке.

Вопрос 11. Что нужно сделать для оптимизации запросов при быстром вводе текста в поиске?

Таймкод: 00:19:53

Ответ собеседника: Правильный. Нужно использовать дебаунс для минимизации количества запросов на сервер, чтобы запрос отправлялся только после того, как пользователь перестал печатать на определённое время (например, 1 секунду).

Правильный ответ:

Кандидат правильно назвал debounce. Разберём полный набор оптимизаций для поиска.

Debounce

Откладывает выполнение функции на указанное время после последнего события.

function debounce(fn, delay) {
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(this, args), delay)
}
}

// Использование
const searchUsers = debounce(async (query) => {
const { data } = await usersApi.search(query)
this.users = data
}, 300)

Throttle

Ограничивает частоту вызова — не чаще одного раза в указанный интервал.

function throttle(fn, delay) {
let lastCall = 0
return function (...args) {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
fn.apply(this, args)
}
}
}

AbortController для отмены запросов

let controller = null

async function searchUsers(query) {
// Отменяем предыдущий запрос
if (controller) {
controller.abort()
}
controller = new AbortController()

try {
const { data } = await axios.get('/api/users', {
params: { q: query },
signal: controller.signal,
})
this.users = data
} catch (error) {
if (!axios.isCancel(error)) {
this.error = error.message
}
}
}

Минимальная длина запроса

Не отправлять запрос, пока пользователь не ввёл минимум N символов.

async function searchUsers(query) {
if (query.length < 3) {
this.users = []
return
}
// ... выполнить запрос
}

Кэширование результатов

const cache = new Map()

async function searchUsers(query) {
if (cache.has(query)) {
this.users = cache.get(query)
return
}
const { data } = await usersApi.search(query)
cache.set(query, data)
this.users = data
}

Debounce в Vue 3 с Composition API

<script setup>
import { ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'

const searchQuery = ref('')
const users = ref([])
const loading = ref(false)

const debouncedSearch = useDebounceFn(async (query) => {
loading.value = true
try {
const { data } = await usersApi.search(query)
users.value = data
} finally {
loading.value = false
}
}, 300)

watch(searchQuery, (newQuery) => {
if (newQuery.length >= 3) {
debouncedSearch(newQuery)
} else {
users.value = []
}
})
</script>

Рекомендуемые значения debounce

  • Поиск по списку: 200-300 мс
  • Поиск с автодополнением: 150-250 мс
  • Поиск по большим данным: 300-500 мс

Debounce vs Throttle

  • Debounce — когда нужен результат после завершения ввода (поиск)
  • Throttle — когда нужна регулярная обработка (scroll, resize)

Для поиска debounce — правильный выбор, как и отметил кандидат.

Вопрос 12. Что такое condition (гонка запросов) и как с ней бороться без AbortController?

Таймкод: 00:21:22

Ответ собеседника: Неполный. Condition (гонка запросов) — это ситуация, когда предыдущий запрос ещё не выполнился, а уже отправлен следующий. Для решения можно использовать AbortController для отмены предыдущего запроса. Без AbortController можно помечать каждый запрос идентификатором (например, датой отправки) и сравнивать их, беря самый актуальный запрос.

Правильный ответ:

Кандидат правильно описал проблему и один из способов решения. Дополним ответ.

Race Condition (гонка запросов)

Это ситуация, когда результат зависит от порядка выполнения асинхронных операций. Пользователь ввёл «а», потом «аб», потом «абв». Запросы уходят последовательно, но ответы могут прийти в другом порядке: «абв» обработался первым, а «а» последним. В результате отображаются устаревшие данные.

Способы борьбы без AbortController

1. Счётчик запросов (request counter)

let requestId = 0

async function searchUsers(query) {
const currentId = ++requestId

const { data } = await axios.get('/api/users', { params: { q: query } })

// Обновляем данные только если это последний запрос
if (currentId === requestId) {
this.users = data
}
}

2. Флаг isLatest

let isLatest = true

async function searchUsers(query) {
isLatest = false
const currentRequest = true
isLatest = currentRequest

const { data } = await axios.get('/api/users', { params: { q: query } })

if (isLatest === currentRequest) {
this.users = data
}
}

3. Отмена через флаг (для не-HTTP операций)

let cancelled = false

async function longRunningTask() {
cancelled = false

const result = await step1()
if (cancelled) return

const processed = await step2(result)
if (cancelled) return

return processed
}

function cancelTask() {
cancelled = true
}

4. Использование библиотеки RxJS

import { switchMap, debounceTime } from 'rxjs/operators'

searchQuery$.pipe(
debounceTime(300),
switchMap(query => usersApi.search(query))
).subscribe(users => {
this.users = users
})

switchMap автоматически отменяет предыдущую подписку при новом значении.

5. Vue 3 — watch с cleanup

import { watch, ref } from 'vue'

const searchQuery = ref('')

watch(searchQuery, (newQuery, _, onCleanup) => {
let cancelled = false

onCleanup(() => {
cancelled = true
})

usersApi.search(newQuery).then(data => {
if (!cancelled) {
users.value = data
}
})
})

Сравнение подходов

ПодходОтмена запросаСложность
AbortControllerДаСредняя
Request counterНет (игнорирует)Низкая
Флаг isLatestНет (игнорирует)Низкая
RxJS switchMapДаВысокая
Vue onCleanupНет (игнорирует)Низкая

Для Golang-разработчика аналогия: это похоже на использование context.Context с context.WithCancel для отмены горутин.

Вопрос 13. Что не хватает в компоненте статистики при код-ревью?

Таймкод: 00:23:06

Ответ собеседника: Неполный. В коде на экране видно начало ответа, но он обрывается. Из контекста понятно, что речь идёт о компоненте с вебсокетами и чартами, где все инициализации и подписки сделаны в onMounted, но не хватает очистки при размонтировании компонента (отписка от вебсокетов, очистка интервалов).

Правильный ответ:

Кандидат правильно указал на отсутствие очистки. Дополним полный список типичных проблем в таких компонентах.

1. Отсутствие cleanup в onUnmounted

Это главная проблема. Подписки и соединения должны быть очищены.

import { onMounted, onUnmounted, ref } from 'vue'

export default {
setup() {
let socket = null
let intervalId = null

onMounted(() => {
socket = new WebSocket('ws://localhost:8080/stats')
socket.onmessage = (event) => {
data.value = JSON.parse(event.data)
}

intervalId = setInterval(fetchStats, 5000)
})

onUnmounted(() => {
if (socket) {
socket.close()
}
if (intervalId) {
clearInterval(intervalId)
}
})
},
}

2. Утечка памяти при пересоздании компонента

Если компонент монтируется и размонтируется многократно (например, при навигации), каждая итерация создаёт новое соединение без закрытия предыдущего.

3. Отсутствие обработки ошибок WebSocket

socket.onerror = (error) => {
console.error('WebSocket error:', error)
// Показать уведомление пользователю
}

socket.onclose = (event) => {
if (!event.wasClean) {
// Попытка переподключения
setTimeout(connect, 3000)
}
}

4. Отсутствие состояния загрузки и ошибки

<template>
<div v-if="loading">Загрузка статистики...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else>
<chart :data="stats" />
</div>
</template>

5. Отсутствие отмены запросов при размонтировании

Если компонент использует HTTP-запросы вместо или вместе с WebSocket:

let controller = null

async function fetchStats() {
controller?.abort()
controller = new AbortController()

try {
const { data } = await axios.get('/api/stats', {
signal: controller.signal,
})
stats.value = data
} catch (error) {
if (!axios.isCancel(error)) {
error.value = error.message
}
}
}

onUnmounted(() => {
controller?.abort()
})

6. Отсутствие debounce/throttle для частых обновлений

WebSocket может присылать данные очень часто. Обновление UI на каждое сообщение может вызвать лаги.

import { useThrottleFn } from '@vueuse/core'

const throttledUpdate = useThrottleFn((newData) => {
chartData.value = newData
}, 100)

socket.onmessage = (event) => {
throttledUpdate(JSON.parse(event.data))
}

7. Отсутствие переподключения

WebSocket-соединение может разорваться. Нужна логика reconnect с экспоненциальной задержкой.

function connect(retries = 0) {
socket = new WebSocket(WS_URL)

socket.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, retries), 30000)
setTimeout(() => connect(retries + 1), delay)
}
}

Аналогия с Golang

Для Golang-разработчика это похоже на забытый defer conn.Close() или отсутствие <-ctx.Done() в горутине — ресурс утекает, и со временем система исчерпывает лимиты на соединения или файловые дескрипторы.

Вопрос 14. Что не хватает в компоненте статистики при код-ревью?

Таймкод: 00:24:18

Ответ собеседника: Правильный. Нужно вынести логику в несколько onMounted хуков для разделения ответственности (работа с сокетами, инициализация графика). Главное — добавить onUnmounted для очистки: отписка от сокетов (removeEventListener), очистка интервалов (clearInterval) с сохранением ID интервала в переменную. Без этого при размонтировании компонента будут утечки памяти, накопление интервалов и подписок на сокеты при переходах между страницами, что приведёт к торможению интерфейса и отображению неактуальных данных.

Правильный ответ:

Кандидат дал полный и правильный ответ. Дополним деталями.

Разделение ответственности по хукам

В Vue 3 можно использовать несколько onMounted и onUnmounted — они выполнятся в порядке объявления.

import { onMounted, onUnmounted, ref } from 'vue'

export default {
setup() {
// Хук 1: WebSocket
let socket = null

onMounted(() => {
socket = new WebSocket('ws://localhost:8080/stats')
socket.onmessage = handleMessage
})

onUnmounted(() => {
socket?.close()
})

// Хук 2: Периодический опрос
let intervalId = null

onMounted(() => {
intervalId = setInterval(fetchStats, 5000)
})

onUnmounted(() => {
clearInterval(intervalId)
})

// Хук 3: Инициализация графика
let chartInstance = null

onMounted(() => {
chartInstance = initChart('#chart-container')
})

onUnmounted(() => {
chartInstance?.destroy()
})
},
}

Полный пример cleanup

import { onMounted, onUnmounted, ref } from 'vue'

export default {
setup() {
const stats = ref([])
const loading = ref(false)
const error = ref(null)

let socket = null
let intervalId = null
let abortController = null
let chartInstance = null

onMounted(() => {
// WebSocket
socket = new WebSocket('ws://localhost:8080/stats')
socket.onmessage = (event) => {
stats.value = JSON.parse(event.data)
}
socket.onerror = (err) => {
error.value = 'Ошибка соединения'
}

// Периодический опрос
intervalId = setInterval(async () => {
abortController = new AbortController()
try {
const { data } = await axios.get('/api/stats', {
signal: abortController.signal,
})
stats.value = data
} catch (err) {
if (!axios.isCancel(err)) {
error.value = err.message
}
}
}, 5000)

// График
chartInstance = new Chart(document.getElementById('chart'), {
type: 'line',
data: { datasets: [{ data: [] }] },
})
})

onUnmounted(() => {
socket?.close()
clearInterval(intervalId)
abortController?.abort()
chartInstance?.destroy()
})

return { stats, loading, error }
},
}

Последствия отсутствия cleanup

  • Утечка памяти — каждый переход на страницу создаёт новое соединение
  • Накопление интервалов — таймеры продолжают работать после ухода со страницы
  • Неактуальные данные — обработчики обновляют состояние размонтированного компонента
  • Ошибки в консоли — "Can't perform state update on unmounted component"

Аналогия с Golang

func handleConnection(ctx context.Context, conn net.Conn) {
defer conn.Close() // как onUnmounted

ctx, cancel := context.WithCancel(ctx)
defer cancel() // отмена горутин при завершении

go func() {
select {
case <-ctx.Done():
return // завершение при отмене
case data := <-ch:
process(data)
}
}()
}

Принцип одинаков: каждый ресурс должен быть освобождён при завершении работы с ним.

Вопрос 15. Какими инструментами пользуешься для дебага и поиска ошибок в коде?

Таймкод: 00:26:44

Ответ собеседника: Правильный. Использую консоль для отображения ошибок с навигацией по файлам и функциям. Для сложных случаев использую debugger с точками останова для пошагового прохода и выявления места падения ошибки. Также приходилось пользоваться вкладкой профилирования (Profiler) в React и инструментами браузера для анализа производительности.

Правильный ответ:

Кандидат назвал основные инструменты. Дополним полный набор инструментов для отладки.

Инструменты браузера

Consoleconsole.log, console.table, console.group, console.time для логирования и замера времени.

Sources/Debugger — точки останова (breakpoints), условные точки останова, watch expressions, call stack.

Network — анализ HTTP-запросов, фильтрация по типу, просмотр заголовков и тела ответа.

Performance — запись и анализ производительности, выявление узких мест.

Memory — снимки кучи (heap snapshots), поиск утечек памяти.

Application — просмотр localStorage, sessionStorage, cookies, кэша.

Расширения для браузера

  • Vue DevTools — инспекция компонентов, состояния, событий, производительности
  • React DevTools — аналогично для React
  • Redux DevTools — отладка состояния, time-travel debugging

Инструменты IDE

  • VS Code Debugger — встроенный отладчик с точками останова
  • GoLand / VS Code для Golang — отладка Go-кода с помощью Delve

Инструменты для отладки Golang

// Логирование
import "log"
log.Printf("Processing user %d: %+v", userID, user)

// Отладка с помощью pprof
import _ "net/http/pprof"

go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Доступные эндпоинты:

  • /debug/pprof/heap — профилирование памяти
  • /debug/pprof/goroutine — список горутин
  • /debug/pprof/profile — CPU профилирование

Delve — отладчик Go

# Запуск с отладчиком
dlv debug main.go

# Команды
(dlv) break main.processUser
(dlv) continue
(dlv) next
(dlv) print user.Name
(dlv) locals
(dlv) goroutines

Инструменты для анализа сетевых запросов

  • Postman / Insomnia — ручное тестирование API
  • Charles Proxy / Fiddler — перехват и модификация трафика
  • Wireshark — низкоуровневый анализ сетевых пакетов

Инструменты для анализа производительности

  • Lighthouse — аудит производительности веб-приложений
  • WebPageTest — анализ скорости загрузки
  • wrk / hey — нагрузочное тестирование API
# Нагрузочное тестирование Go-сервера
hey -n 10000 -c 100 http://localhost:8080/api/users

Инструменты для логирования

  • ELK Stack (Elasticsearch, Logstash, Kibana) — централизованное логирование
  • Grafana + Loki — логирование с визуализацией
  • Sentry — отслеживание ошибок в продакшне

Для Golang-разработчика важно уметь использовать pprof и Delve — это стандартные инструменты отладки в экосистеме Go.

Вопрос 16. Какие показатели производительности существуют и что может их ухудшить?

Таймкод: 00:28:08

Ответ собеседника: Правильный. Основные показатели: First Contentful Paint (FCP) — отображение первого пикселя, Time to Interactive (TTI) — время до интерактивности страницы, Largest Contentful Paint (LCP) — время отрисовки самого большого элемента интерфейса. Также в профайлере есть метрики длительности layout, painting, composition. Ухудшить показатели могут: большие списки/таблицы без пагинации и виртуализации, неоптимизированные изображения (20 МБ), отсутствие lazy loading для галерей, блокирующие синхронные функции (например, проход по массиву из 10 000 элементов с математическими операциями перед рендером).

Правильный ответ:

Кандидат дал полный ответ. Дополним деталями и метриками.

Core Web Vitals (Google)

LCP (Largest Contentful Paint) — время отрисовки самого большого видимого элемента. Хорошо: < 2.5 сек, плохо: > 4 сек.

FID (First Input Delay) — время от первого взаимодействия пользователя до реакции браузера. Хорошо: < 100 мс, плохо: > 300 мс.

CLS (Cumulative Layout Shift) — совокупное смещение макета. Хорошо: < 0.1, плохо: > 0.25.

Дополнительные метрики

FCP (First Contentful Paint) — время до отображения первого элемента контента (текст, изображение).

TTI (Time to Interactive) — время до полной интерактивности страницы.

TBT (Total Blocking Time) — суммарное время, когда основной поток был заблокирован > 50 мс.

Speed Index — скорость визуального заполнения страницы.

Фазы рендеринга

Layout (Reflow) — вычисление позиций и размеров элементов. Триггерится при изменении геометрии.

Paint — заполнение пикселей (цвета, тени, градиенты).

Composition — объединение слоёв для отображения на экране. Самая лёгкая фаза.

Что ухудшает производительность

На фронтенде:

  • Большие списки без виртуализации (react-window, vue-virtual-scroller)
  • Неоптимизированные изображения (использовать WebP, lazy loading)
  • Блокирующие операции на основном потоке
  • Избыточные ре-рендеры компонентов
  • Неиспользуемый JavaScript (code splitting)

На бэкенде (актуально для Golang):

  • N+1 запросов к базе данных
  • Отсутствие индексов
  • Блокирующие операции без контекста отмены
  • Утечка горутин
// Плохо — N+1 запросов
func getUsersWithOrders(users []User) {
for i, user := range users {
orders := db.Query("SELECT * FROM orders WHERE user_id = ?", user.ID)
users[i].Orders = orders
}
}

// Правильно — один запрос
func getUsersWithOrders(userIDs []int) {
// SELECT * FROM orders WHERE user_id IN (?, ?, ...)
}

Инструменты измерения

  • Lighthouse — встроен в Chrome DevTools
  • WebPageTest — детальный анализ загрузки
  • Chrome DevTools Performance — запись и анализ рендеринга
  • Calibre / SpeedCurve — мониторинг производительности

Для Golang-разработчика важно понимать, что производительность — это не только фронтенд. Медленный API напрямую влияет на LCP и TTI. Использование pprof для поиска узких мест в бэкенде — обязательный навык.

Вопрос 17. Что такое алгоритмическая сложность (О большое) и какая считается оптимальной?

Таймкод: 00:30:32

Ответ собеседника: Правильный. Оптимальная сложность — O(1) для доступа к элементу по индексу или ключу. O(N) — один проход по массиву, приемлемо. O(N²) — цикл в цикле, не предпочтительно.

Правильный ответ:

Кандидат правильно описал основные сложности. Дадим полное объяснение.

Определение O большое (Big O)

Big O описывает верхнюю границу роста времени выполнения или использования памяти алгоритмом в зависимости от размера входных данных N. Это не точное время, а оценка того, как алгоритм масштабируется.

Основные сложности

O(1) — Константная

Время выполнения не зависит от размера данных.

// Доступ по индексу
arr[5]

// Доступ по ключу в map
m["key"]

O(log N) — Логарифмическая

Данные делятся пополам на каждом шаге.

// Бинарный поиск
func binarySearch(arr []int, target int) int {
low, high := 0, len(arr)-1
for low <= high {
mid := (low + high) / 2
if arr[mid] == target {
return mid
} else if arr[mid] < target {
low = mid + 1
} else {
high = mid - 1
}
}
return -1
}

O(N) — Линейная

Время растёт пропорционально размеру данных.

// Поиск элемента в неотсортированном массиве
func linearSearch(arr []int, target int) int {
for i, v := range arr {
if v == target {
return i
}
}
return -1
}

O(N log N) — Линейно-логарифмическая

Эффективные алгоритмы сортировки.

// Сортировка слиянием, быстрая сортировка
sort.Ints(arr) // O(N log N) в среднем

O(N²) — Квадратичная

Вложенные циклы. Неприемлемо для больших данных.

// Пузырьковая сортировка
for i := 0; i < n; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}

O(2^N) — Экспоненциальная

Каждый элемент удваивает количество операций.

// Числа Фибоначчи (наивная рекурсия)
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}

O(N!) — Факториальная

Перестановки всех элементов. Крайне неэффективно.

Шкала приемлемости

СложностьN=10N=100N=1000Оценка
O(1)111Отлично
O(log N)3710Отлично
O(N)101001000Хорошо
O(N log N)336649966Нормально
O(N²)100100001000000Плохо
O(2^N)1024~1.27×10³⁰УжасноНеприемлемо

Практический пример оптимизации

// O(N²) — плохо
func hasDuplicateSlow(arr []int) bool {
for i := 0; i < len(arr); i++ {
for j := i + 1; j < len(arr); j++ {
if arr[i] == arr[j] {
return true
}
}
}
return false
}

// O(N) — хорошо
func hasDuplicateFast(arr []int) bool {
seen := make(map[int]bool)
for _, v := range arr {
if seen[v] {
return true
}
seen[v] = true
}
return false
}

Оптимальная сложность

O(1) — идеал, но не всегда достижим. Для большинства задач поиска — O(1) с хеш-таблицей или O(log N) с бинарным поиском. Для сортировки — O(N log N) является теоретическим минимумом для сравнительных алгоритмов.

Вопрос 18. Как оцениваешь свой уровень владения TypeScript и какие типы используешь?

Таймкод: 00:31:27

Ответ собеседника: Правильный. Уровень комфортный, так как практически все проекты были на TypeScript, и с ним работаю более 4 лет. Использую основные понятия: типы, интерфейсы, generics. Из утилитных типов обычно использую Record, Pick, Omit, Required, Partial. Например, Omit позволяет исключить поля из типа и создать новый тип с исключёнными полями.

Правильный ответ:

Кандидат продемонстрировал хорошее владение TypeScript. Дополним полный обзор.

Основные концепции

Типы и интерфейсы

// Тип
type User = {
id: number
name: string
email: string
}

// Интерфейс
interface Product {
id: number
title: string
price: number
}

Различие: интерфейс можно расширять через extends и он поддерживает declaration merging.

Generics

function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}

// Использование
const first = getFirst([1, 2, 3]) // number | undefined

// Generic с ограничением
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}

Утилитные типы

interface User {
id: number
name: string
email: string
password: string
}

// Partial — все поля необязательны
type UpdateUser = Partial<User>
// { id?: number; name?: string; email?: string; password?: string }

// Required — все поля обязательны
type RequiredUser = Required<User>

// Pick — выбрать поля
type PublicUser = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// Omit — исключить fields
type UserWithoutPassword = Omit<User, 'password'>
// { id: number; name: string; email: string }

// Record — создать тип словаря
type UserRoles = Record<string, 'admin' | 'user' | 'moderator'>

// Exclude — исключить из union типа
type NonNullableRole = Exclude<string | null | undefined, null | undefined>
// string

Продвинутые типы

// Conditional Types
type IsString<T> = T extends string ? true : false

// Mapped Types
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}

// Template Literal Types
type EventName = 'click' | 'focus'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus'

// Infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

Практический пример для API

// Типы для API
interface ApiResponse<T> {
data: T
success: boolean
message?: string
}

interface PaginatedResponse<T> {
items: T[]
total: number
page: number
limit: number
}

// Использование
async function fetchUsers(page: number): Promise<ApiResponse<PaginatedResponse<User>>> {
const response = await fetch(`/api/users?page=${page}`)
return response.json()
}

Связь с Golang

Для Golang-разработчика TypeScript будет полезен при работе с фронтенд-частью. Типизация в TypeScript помогает ловить ошибки на этапе компиляции, как и в Go. Концепции интерфейсов и generics имеют прямые аналоги в Go:

// Go — interface
type User interface {
GetName() string
}

// Go — generics (с версии 1.18)
func First[T any](arr []T) (T, bool) {
if len(arr) == 0 {
var zero T
return zero, false
}
return arr[0], true
}

Вопрос 19. Какой проект в компании самый сложный и какие задачи на нем решаются?

Таймкод: 00:34:06

Ответ собеседника: Правильный. Самый сложный проект — это Steam Retention, который занимается геймификацией, лояльностью и бонусами для удержания игроков. Среднее время выполнения задачи составляет 150-200 часов. В настоящее время идёт масштабирование и разделение стрима пополам с наймом второй команды фронтенда, бэкенда и тестирования.

Правильный ответ:

Кандидат описал масштабный проект. Разберём подробнее.

Проект Steam Retention

Это система геймификации и лояльности для удержания игроков. Такие системы включают:

Функциональность:

  • Бонусные программы (daily rewards, streak bonuses)
  • Система уровней и достижений
  • Программа лояльности (loyalty points, tiers)
  • Персонализированные предложения
  • Аналитика поведения игроков

Технические вызовы:

Высокая нагрузка — тысячи одновременных пользователей, которые проверяют бонусы, получают награды, проходят квесты.

Сложная бизнес-логика — правила начисления бонусов, условия активации, срок действия, ограничения.

Интеграции — платёжные системы, игровые сервисы, аналитические платформы.

Консистентность данных — бонусы должны начисляться корректно, без дублирования и потерь.

Масштабирование команды

Разделение стрима на две команды — типичный шаг при росте проекта. Это позволяет:

  • Ускорить разработку параллельных фич
  • Снизить зависимость между командами
  • Упростить code review и планирование

Среднее время задачи 150-200 часов

Это примерно 4-5 недель. Такие задачи обычно включают:

  • Проектирование архитектуры
  • Реализация бэкенда и фронтенда
  • Тестирование
  • Code review
  • Деплой и мониторинг

Пример архитектуры для подобного проекта на Golang

// Сервис бонусов
type BonusService struct {
repo BonusRepository
userService UserService
notifier Notifier
}

func (s *BonusService) AwardDailyBonus(ctx context.Context, userID int64) error {
// Проверка условий
streak, err := s.repo.GetStreak(ctx, userID)
if err != nil {
return fmt.Errorf("get streak: %w", err)
}

// Расчёт бонуса
bonus := calculateBonus(streak)

// Начисление
if err := s.repo.AwardBonus(ctx, userID, bonus); err != nil {
return fmt.Errorf("award bonus: %w", err)
}

// Уведомление
s.notifier.Notify(userID, "Вы получили бонус!")

return nil
}

Для Golang-разработчика такой проект — отличная возможность применить навыки проектирования микросервисов, работы с высокой нагрузкой и сложной бизнес-логикой.

Вопрос 20. Какие проблемы возникают из-за микрофронтендов и как они решаются?

Таймкод: 00:35:23

Ответ собеседника: Правильный. Основная проблема — связанность микрофронтендов через общий слой (монорепа), что не позволяет делать независимые релизы. В монорепу начали добавлять бизнес-логику и компоненты, из-за чего каждая команда пишет код в общем слое. Это приводит к необходимости собирать все альфа-версии, тестировать их и делать единый релиз. Задача до Нового года и первых кварталов следующего года — избавиться от зависимостей общего слоя, чтобы команды могли релизить независимо.

Правильный ответ:

Кандидат точно описал главную проблему микрофронтендов. Дополним полный обзор.

Основные проблемы микрофронтендов

1. Связанность через общий слой (Shared Library)

Как отметил кандидат, общая библиотека создаёт зависимость. Изменение в shared-компоненте требует пересборки и тестирования всех микрофронтендов.

2. Дублирование зависимостей

Каждый микрофронтенд может тащить свою копию Vue, Axios и других библиотек, увеличивая размер бандла.

3. Несогласованность UI

Разные команды могут использовать разные версии дизн-системы, что приводит к визуальным расхождениям.

4. Сложность отладки

Ошибка может возникать на границе между микрофронтендами, и сложно определить, кто виноват.

5. Производительность

Множество отдельных бандлов увеличивает количество HTTP-запросов и время загрузки.

Решения

Изоляция shared-слоя

// Вместо общего кода — версионированные пакеты
// package.json
{
"dependencies": {
"@company/ui-kit": "1.2.0", // фиксированная версия
"@company/utils": "2.0.1"
}
}

Каждый микрофронтенд зависит от конкретной версии shared-библиотеки и обновляет её независимо.

Web Components как контракт

// Общий компонент как Web Custom Element
class CompanyButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button class="btn"><slot></slot></button>`
}
}
customElements.define('company-button', CompanyButton)

Web Components работают в любом фреймворке, что снимает привязку к Vue/React.

Module Federation с версионированием

// webpack.config.js
new ModuleFederationPlugin({
name: 'teamA',
shared: {
vue: {
singleton: true,
requiredVersion: '^3.2.0',
eager: false, // загружается асинхронно
},
},
})

Независимые репозитории

Вместо монорепы — отдельный репозиторий для каждого микрофронтенда. Общие библиотеки публикуются как npm-пакеты.

Design System как источник правды

Единый UI-кит с чётким процессом версионирования и changelog. Обновление мажорной версии — только с согласия всех команд.

Мониторинг и observability

// Каждый микрофронтенд отправляет метрики с префиксом
statsd.increment('teamA.page.load')
statsd.timing('teamA.api.response_time', duration)

Для Golang-разработчика аналогия: микрофронтенды — это как микросервисы. Те же проблемы — связанность, версионирование, мониторинг. Решения похожи: чёткие контракты, версионированные API, независимый деплой.

Вопрос 21. Какая нагрузка на проекты и какие требования к производительности?

Таймкод: 00:38:51

Ответ собеседника: Правильный. Средняя нагрузка составляет 25-3.500 FPS, в пике может доходить до 50.000. Проект универсальный, ядро зорачивается на все 30 брендов. Требования к производительности высокие — должно работать как на старых Android-устройствах, так и на последних iPhone. Сложность поддержки стабильности связана с выходом новых версий iOS Safari и подгрузкой микрофронтендов.

Правильный ответ:

Кандидат описал серьёзные требования к производительности. Уточним детали.

Нагрузка

Скорее всего, имелось в виду RPS (Requests Per Second), а не FPS (Frames Per Second), так как речь о бэкенде:

  • Средняя: 25-3500 RPS
  • Пиковая: до 50 000 RPS

Это значительная нагрузка, требующая продуманной архитектуры.

Мультибрендовость

Одно ядро для 30 брендов — это multi-tenant архитектура. Каждый бренд имеет свою конфигурацию, но использует общий код.

// Пример multi-tenant middleware
func TenantMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
brandID := c.GetHeader("X-Brand-ID")
if brandID == "" {
brandID = detectBrandFromHost(c.Request.Host)
}

tenant := getTenantConfig(brandID)
c.Set("tenant", tenant)
c.Next()
}
}

Требования к производительности на разных устройствах

Старые Android-устройства:

  • Ограниченная память (1-2 ГБ RAM)
  • Медленный процессор
  • Старые версии браузеров
  • Медленное соединение (3G)

Последние iPhone:

  • Мощный процессор
  • Быстрое соединение (5G/WiFi 6)
  • Современный браузер

Оптимизация для слабых устройств автоматически даёт отличный результат на мощных.

Проблемы iOS Safari

  • Задержки с поддержкой новых Web API
  • Особенности работы с WebSocket
  • Ограничения на размер localStorage
  • Проблемы с прокруткой и viewport

Оптимизация для высокой нагрузки на Golang

// Connection pooling
db, err := sql.Open("postgres", dsn)
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)

// Graceful shutdown
srv := &http.Server{Addr: ":8080", Handler: handler}

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)

Кэширование для снижения нагрузки

// In-memory cache с TTL
type Cache struct {
data map[string]cacheItem
mu sync.RWMutex
}

type cacheItem struct {
value interface{}
expiration time.Time
}

func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.data[key]
if !exists || time.Now().After(item.expiration) {
return nil, false
}
return item.value, true
}

Для Golang-разработчика такой проект — отличная возможность поработать с высокой нагрузкой, multi-tenant архитектурой и оптимизацией производительности.

Вопрос 22. Бывают ли переработки и как они регулируются?

Таймкод: 00:39:22

Ответ собеседника: Правильный. Переработки бывают редко и только с согласия сотрудника. Если разработчик отказывается, команда управления фронтендом стоит на его стороне, независимо от бизнес-целей. В случае переработки или выхода на выходные оплачивается по двойному тарифу.

Правильный ответ:

Кандидат описал здоровую культуру работы. Это важный аспект при выборе работодателя.

Признаки здоровой культуры

Добровольность — переработки только с согласия сотрудника. Это означает, что компания уважает личные границы.

Защита со стороны руководства — тимлид стоит на стороне разработчика, а не бизнеса. Это создаёт доверие и безопасность.

Компенсация — двойной тариф за переработки и выходные — справедливая практика.

Редкость — переработки бывают редко, что говорит о хорошем планировании и реалистичных оценках.

Почему это важно

Систематические переработки — признак проблем:

  • Плохое планирование и оценка задач
  • Недостаточный штат
  • Нереалистичные ожидания бизнеса
  • Технический долг, который замедляет разработку

Как избежать переработок

  • Чёткие критерии готовности (Definition of Done)
  • Реалистичные оценки с учётом рисков
  • Приоритизация задач — не всё может быть срочным
  • Технический долг должен быть в бэклоге и регулярно погашаться

Для Golang-разработчика важно работать в компании, где ценится устойчивая разработка (sustainable pace) — это один из принципов Agile.

Вопрос 23. Как организован процесс релизов?

Таймкод: 00:40:07

Ответ собеседника: Правильный. Есть роль релиз-менеджера, на которую назначаются разработчики. Каждый день есть релизный менеджер, назначенный на разработчика. Процесс регламентирован, фактически это длинный день без разработки, так как требует активного участия в процессе релиза.

Правильный ответ:

Кандидат описал распространённую практику. Разберём подробнее.

Роль релиз-менеджера (Release Manager)

Это ротационная роль, когда разработчики по очереди отвечают за процесс релиза. Преимущества:

  • Каждый разработчик понимает процесс деплоя
  • Нет единой точки отказа
  • Команда разделяет ответственность за стабильность

Типичные обязанности релиз-менеджера

  • Сбор всех готовых фич в релизную ветку
  • Координация тестирования
  • Мониторинг деплоя
  • Откат при проблемах
  • Коммуникация с командами о статусе релиза

Типичный процесс релиза

1. Feature freeze — код замораживается
2. Сборка релизного билда
3. Развертывание на staging
4. Регрессионное тестирование
5. Развертывание в production
6. Мониторинг метрик и ошибок
7. Откат при необходимости

Проблема «день без разработки»

Кандидат правильно отметил, что день релиза — это день без разработки. Это нормально, но можно оптимизировать:

  • Автоматизация CI/CD — уменьшить ручные шаги
  • Feature flags — деплой и релиз фичи разделены
  • Canary releases — постепенное включение для части пользователей
// Пример feature flag
func handleRequest(c *gin.Context) {
if featureFlags.IsEnabled("new-checkout-flow", c.GetString("userID")) {
newCheckoutHandler(c)
} else {
oldCheckoutHandler(c)
}
}

Feature flags как решение

С feature flags можно деплоить код в production, но включать фичу для пользователей постепенно. Это снижает риск релиза и позволяет делать релизы чаще.

Для Golang-разработчика важно понимать процесс CI/CD и уметь настраивать пайплайны. Знание инструментов типа GitHub Actions, GitLab CI, ArgoCD будет плюсом.

Вопрос 24. Что такое специфичность селектора в CSS?

Таймкод: 00:41:19

Ответ собеседника: Правильный. Специфичность — это приоритет, с которым применяется тот или иной селектор. Самые приоритетные — !important, далее идут inline-стили, стили по идентификатору (id), потом стили по классу и стили по тегу.

Правильный ответ:

Кандидат правильно описал иерархию. Дадим полное объяснение.

Определение

Специфичность (specificity) — это алгоритм, по которому браузер определяет, какой CSS-правило применить, когда несколько правил target один и тот же элемент.

Иерархия специфичности

Специфичность вычисляется как кортеж (a, b, c, d):

УровеньПримерЗначение
!importantcolor: red !importantПереопределяет всё
Inline-стилиstyle="color: red"a=1, b=0, c=0, d=0
ID#headera=0, b=1, c=0, d=0
Класс, псевдокласс, атрибут.btn, :hover, [type="text"]a=0, b=0, c=1, d=0
Тег, псевдоэлементdiv, ::beforea=0, b=0, c=0, d=1
Универсальный селектор*a=0, b=0, c=0, d=0

Примеры вычисления

/* Специфичность: 0,0,0,1 */
div {
color: black;
}

/* Специфичность: 0,0,1,0 */
.button {
color: blue;
}

/* Специфичность: 0,0,1,1 */
div.button {
color: green;
}

/* Специфичность: 0,1,0,0 */
#submit-btn {
color: red;
}

/* Специфичность: 0,1,1,1 */
#submit-btn.button.primary {
color: purple;
}

/* Специфичность: 1,0,0,0 — максимум */
color: orange !important;

Правила

  • Больше цифра = выше приоритет
  • При равной специфичности побеждает последнее объявленное правило
  • !important переопределяет всё (но не рекомендуется использовать)
  • Inline-стили имеют высокий приоритет, кроме !important

Проблемы с высокой специфичностью

/* Плохо — слишком специфичный */
div#main-content ul.nav li a {
color: blue;
}

/* Чтобы переопределить, нужно ещё более специфичное правило */
div#main-content ul.nav li a.link {
color: red;
}

Рекомендации

  • Используйте классы вместо ID для стилизации
  • Избегайте !important
  • Избегайте глубокой вложенности селекторов
  • Используйте CSS-модули или scoped стили в Vue
<style scoped>
/* Стили применяются только к этому компоненту */
.button {
color: blue;
}
</style>

CSS Modules

/* Button.module.css */
.button {
color: blue;
}
<template>
<button :class="$style.button">Click</button>
</template>

<style module>
/* Класс будет хеширован: Button_button_x7k2 */
</style>

Для Golang-разработчика понимание CSS-специфичности важно при работе с фронтенд-частью, особенно при отладке стилей и интеграции с UI-библиотеками.

Вопрос 25. Как работают свойства Flex Grow и Flex Shrink?

Таймкод: 00:41:50

Ответ собеседника: Неполный. Flex Grow даёт элементу возможность расширяться на всю доступную ширину во флекс-контейнере. Flex Shrink при значении 1 позволяет элементу занимать только свой размер и не расширяться. Неполно описан механизм работы flex-shrink — он определяет способность элемента сжиматься, а не только не расширяться.

Правильный ответ:

Кандидат частично правильно описал свойства. Дадим полное объяснение.

Flex Grow

Определяет, как элемент будет расти относительно других элементов при наличии свободного пространства.

.container {
display: flex;
width: 600px;
}

.item-1 {
flex-grow: 1; /* Займёт 1 часть свободного пространства */
}

.item-2 {
flex-grow: 2; /* Займёт 2 части свободного пространства */
}

.item-3 {
flex-grow: 1; /* Займёт 1 часть свободного пространства */
}

Если свободное пространство = 200px, то:

  • item-1 получит 50px (1/4)
  • item-2 получит 100px (2/4)
  • item-3 получит 50px (1/4)

Flex Shrink

Определяет, как элемент будет сжиматься относительно других элементов при нехватке пространства.

.container {
display: flex;
width: 300px;
}

.item-1 {
width: 200px;
flex-shrink: 1; /* Будет сжиматься */
}

.item-2 {
width: 200px;
flex-shrink: 0; /* Не будет сжиматься */
}

Контейнер 300px, элементы суммарно 400px. Нужно сжать на 100px.

  • item-1 сожмётся на 100px → станет 100px
  • item-2 останется 200px

Flex Basis

Определяет начальный размер элемента до применения grow/shrink.

.item {
flex-basis: 200px; /* Начальный размер */
flex-grow: 1;
flex-shrink: 1;
}

Сокращённая запись

/* flex: grow shrink basis */
.item {
flex: 1 1 auto; /* Растёт, сжимается, базовый размер auto */
}

.item {
flex: 1; /* Аналогично flex: 1 1 0% */
}

.item {
flex: 0 0 auto; /* Не растёт, не сжимается, размер по содержимому */
}

Типичные паттерны

/* Равные колонки */
.column {
flex: 1;
}

/* Фиксированная боковая панель */
.sidebar {
flex: 0 0 250px; /* Не растёт, не сжимается, 250px */
}

.content {
flex: 1; /* Занимает оставшееся пространство */
}

/* Элемент, который не должен сжиматься */
.no-shrink {
flex-shrink: 0;
}

Визуализация

Контейнер 600px, 3 элемента по 100px (free space = 300px)

flex-grow: 1, 2, 1
┌──────────┬────────────────────┬──────────┐
│ 150px │ 200px │ 150px │
│ (100+50) │ (100+100) │ (100+50) │
└──────────┴────────────────────┴──────────┘

Контейнер 300px, 2 элемента по 200px (overflow = 100px)

flex-shrink: 1, 0
┌────────────────────┬────────────────────┐
│ 100px │ 200px │
│ (сжат на 100) │ (не сжимается) │
└────────────────────┴────────────────────┘

Для Golang-разработчика понимание Flexbox важно при работе с фронтенд-частью, особенно при создании адаптивных интерфейсов и работе с UI-компонентами.

Вопрос 26. Как работают CSS переменные и чем они отличаются от переменных препроцессоров?

Таймкод: 00:42:29

Ответ собеседника: Правильный. Отличия в синтаксисе: нативные CSS переменные вызываются через функцию var() и имеют префикс из двух черточек. В работе: переменные препроцессоров встраиваются на этапе сборки и не меняются, а нативные переменные можно менять во время выполнения (runtime), они наследуются и видны в консоли браузера.

Правильный ответ:

Кандидат дал правильный ответ. Дополним деталями.

CSS Custom Properties (нативные переменные)

/* Объявление */
:root {
--primary-color: #3498db;
--spacing-unit: 8px;
--font-size-base: 16px;
}

/* Использование */
.button {
background-color: var(--primary-color);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
font-size: var(--font-size-base);
}

/* Fallback значение */
color: var(--undefined-color, black);

Переменные препроцессоров (Sass/SCSS)

// Объявление
$primary-color: #3498db;
$spacing-unit: 8px;

// Использование
.button {
background-color: $primary-color;
padding: $spacing-unit ($spacing-unit * 2);
}

Ключевые отличия

ХарактеристикаCSS переменныеSass переменные
Время работыRuntimeCompile time
НаследованиеДа (каскад)Нет
Доступ из JSДаНет
Доступ в DevToolsДаНет (скомпилированы)
Область видимостиCSS-правилоБлок кода
Медиа-запросыМожно переопределитьНельзя

Изменение CSS переменных из JavaScript

// Получить значение
const root = document.documentElement
const primaryColor = getComputedStyle(root)
.getPropertyValue('--primary-color')
.trim()

// Установить значение
root.style.setProperty('--primary-color', '#e74c3c')

// Реактивное обновление — все элементы с var(--primary-color) обновятся

Пример: темизация с CSS переменными

:root {
--bg-color: #ffffff;
--text-color: #333333;
--primary-color: #3498db;
}

[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
--primary-color: #2980b9;
}

body {
background-color: var(--bg-color);
color: var(--text-color);
}
// Переключение темы
document.documentElement.setAttribute('data-theme', 'dark')

Пример: вычисления с CSS переменными

:root {
--base-spacing: 8px;
}

.card {
/* Вычисления прямо в CSS */
padding: calc(var(--base-spacing) * 2);
margin-bottom: calc(var(--base-spacing) * 3);
font-size: calc(var(--base-spacing) * 2);
}

Область видимости CSS переменных

/* Глобальная */
:root {
--color: blue;
}

/* Локальная — только для .card и потомков */
.card {
--color: red;
color: var(--color); /* red */
}

/* За пределами .card — синий */
.button {
color: var(--color); /* blue (от :root) */
}

Когда что использовать

CSS переменные:

  • Темизация
  • Динамические значения (из JS)
  • Значения, зависящие от медиа-запросов
  • Design tokens

Sass переменные:

  • Значения, которые не меняются в runtime
  • Сложные вычисления на этапе сборки
  • Миксины и функции

Для Golang-разработчика понимание CSS переменных важно при работе с фронтенд-частью, особенно при реализации темизации и динамических стилей.

Вопрос 27. Что такое фаза событий в браузерных API?

Таймкод: 00:44:19

Ответ собеседника: Правильный. Фаза событий — это процесс прохождения события через три фазы при вызове на элементе: погружение (capture), фаза цели (target) и фаза всплытия (bubble). Погружение по умолчанию отключено, для его включения нужно передать в addEventListener третьим параметром объект со значением capture: true.

Правильный ответ:

Кандидат дал правильный ответ. Дополним деталями и примерами.

Три фазы события

┌─────────────────────────────────────────────┐
│ document │
│ ┌───────────────────────────────────────┐ │
│ │ html │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ body │ │ │
│ │ │ ┌───────────────────────────┐ │ │ │
│ │ │ │ .container │ │ │ │
│ │ │ │ ┌─────────────────────┐ │ │ │ │
│ │ │ │ │ button (target) │ │ │ │ │
│ │ │ │ └─────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────┘ │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

1. CAPTURE PHASE (погружение): document → html → body → .container → button
2. TARGET PHASE (цель): button
3. BUBBLE PPHASE (всплытие): button → .container → body → html → document

addEventListener параметры

// По умолчанию — фаза всплытия
element.addEventListener('click', handler)
element.addEventListener('click', handler, false)

// Фаза погружения
element.addEventListener('click', handler, true)

// Объект с опциями
element.addEventListener('click', handler, {
capture: true, // фаза погружения
once: true, // выполнить один раз
passive: true, // не вызывать preventDefault()
})

Пример: разница между capture и bubble

<div id="outer">
<div id="inner">
<button id="btn">Click me</button>
</div>
</div>
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
const btn = document.getElementById('btn')

// Bubble (по умолчанию)
outer.addEventListener('click', () => console.log('outer bubble'))
inner.addEventListener('click', () => console.log('inner bubble'))
btn.addEventListener('click', () => console.log('btn bubble'))

// Capture
outer.addEventListener('click', () => console.log('outer capture'), true)
inner.addEventListener('click', () => console.log('inner capture'), true)
btn.addEventListener('click', () => console.log('btn capture'), true)

При клике на кнопку вывод:

outer capture
inner capture
btn bubble (target — bubble срабатывает раньше)
btn capture (target — capture тоже срабатывает)
inner bubble
outer bubble

Event Delegation (делегирование событий)

Использование всплытия для обработки событий на родителе:

// Плохо — обработчик на каждом элементе
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick)
})

// Правильно — один обработчик на родителе
document.getElementById('list').addEventListener('click', (event) => {
if (event.target.matches('.item')) {
handleClick(event)
}
})

Остановка распространения

element.addEventListener('click', (event) => {
event.stopPropagation() // Остановить всплытие/погружение
event.stopImmediatePropagation() // Остановить + не вызывать остальные обработчики на этом элементе
})

preventDefault vs stopPropagation

// preventDefault — отменить действие по умолчанию (например, переход по ссылке)
link.addEventListener('click', (e) => {
e.preventDefault() // Ссылка не откроется
})

// stopPropagation — остановить распространение события
button.addEventListener('click', (e) => {
e.stopPropagation() // Родитель не узнает о клике
})

Vue: модификаторы событий

<!-- Остановить всплытие -->
<button @click.stop="handleClick">Click</button>

<!-- Отменить действие по умолчанию -->
<form @submit.prevent="handleSubmit">...</form>

<!-- Фаза погружения -->
<div @click.capture="handleCapture">...</div>

<!-- Выполнить один раз -->
<button @click.once="handleOnce">Click</button>

passive: true

Оптимизация для touch-событий — браузер знает, что handler не вызовет preventDefault(), и может сразу начать прокрутку.

// Для touch событий — всегда используйте passive
document.addEventListener('touchstart', handler, { passive: true })

Для Golang-разработчика понимание фаз событий важно при работе с фронтенд-частью, особенно при отладке обработчиков событий и реализации делегирования.

Вопрос 28. Что такое свойства target и currentTarget в объекте события?

Таймкод: 00:44:58

Ответ собеседника: Правильный. target — это элемент, на котором произошло событие. currentTarget — это элемент, к которому привязан обработчик события.

Правильный ответ:

Кандидат дал правильный и точный ответ. Дополним примерами.

Определения

  • event.target — элемент, на котором фактически произошло событие (исходный источник)
  • event.currentTarget — элемент, к которому привязан текущий обработчик события

Пример: разница при всплытии

<div id="outer">
<div id="inner">
<button id="btn">
<span id="text">Click me</span>
</button>
</div>
</div>
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
const btn = document.getElementById('btn')

outer.addEventListener('click', (event) => {
console.log('target:', event.target.id) // text (span)
console.log('currentTarget:', event.currentTarget.id) // outer
})

inner.addEventListener('click', (event) => {
console.log('target:', event.target.id) // text (span)
console.log('currentTarget:', event.currentTarget.id) // inner
})

btn.addEventListener('click', (event) => {
console.log('target:', event.target.id) // text (span)
console.log('currentTarget:', event.currentTarget.id) // btn
})

При клике на текст «Click me» (span#text):

outer handler:
target: text ← исходный элемент
currentTarget: outer ← элемент с обработчиком

inner handler:
target: text ← тот же исходный элемент
currentTarget: inner ← элемент с обработчиком

btn handler:
target: text ← тот же исходный элемент
currentTarget: btn ← элемент с обработчиком

Когда target === currentTarget

Только когда клик происходит непосредственно на элементе с обработчиком (без всплытия):

btn.addEventListener('click', (event) => {
// При клике на саму кнопку (не на span внутри):
console.log(event.target === event.currentTarget) // true
})

Практическое применение: делегирование событий

document.getElementById('list').addEventListener('click', (event) => {
// event.currentTarget — всегда #list
// event.target — конкретный элемент, по которому кликнули

const item = event.target.closest('.list-item')
if (item) {
console.log('Clicked item:', item.dataset.id)
}
})

Vue: доступ к событию

<template>
<div @click="handleClick">
<button>Click me</button>
</div>
</template>

<script>
export default {
methods: {
handleClick(event) {
console.log(event.target.tagName) // BUTTON
console.log(event.currentTarget.tagName) // DIV
}
}
}
</script>

this vs currentTarget

В обычных функциях this равен currentTarget:

element.addEventListener('click', function(event) {
console.log(this === event.currentTarget) // true
})

// Стрелочная функция — this не привязан к элементу
element.addEventListener('click', (event) => {
console.log(this) // undefined или внешний контекст
console.log(event.currentTarget) // правильный способ
})

Для Golang-разработчика понимание разницы между target и currentTarget важно при работе с фронтенд-частью, особенно при реализации делегирования событий и обработке кликов на динамических элементах.

Вопрос 29. Как работают функции debounce и throttle?

Таймкод: 00:45:39

Ответ собеседника: Правильный. debounce используется для отправки запроса после того, как пользователь перестал вводить текст на определённое время (например, в поиске). throttle похож, но выполняет функцию не когда пользователь остановился, а через определённые интервалы. Можно сравнить с setTimeout (debounce) и setInterval (throttle).

Правильный ответ:

Кандидат дал правильный ответ. Дополним реализациями и примерами.

Debounce

Откладывает выполнение функции на указанное время после последнего вызова. Если функция вызвана повторно до истечения таймера — таймер сбрасывается.

function debounce(fn, delay) {
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(this, args), delay)
}
}

Throttle

Гарантирует, что функция выполняется не чаще одного раза в указанный интервал.

function throttle(fn, delay) {
let lastCall = 0
return function (...args) {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
fn.apply(this, args)
}
}
}

Визуализация работы

События: --x--x--x--x--------x--x-----x----->

Debounce (300ms):
Вызов: --------------------x-----------x---->
(ждёт паузу после последнего события)

Throttle (300ms):
Вызов: --x-----x-----x-----x--x-----x-----x--->
(выполняется каждые 300мс, игнорируя промежуточные)

Варианты debounce

// Debounce с немедленным первым вызовом (leading)
function debounceLeading(fn, delay) {
let timeoutId
return function (...args) {
if (!timeoutId) {
fn.apply(this, args)
}
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
}, delay)
}
}

// Debounce с обоими вариантами
function debounce(fn, delay, options = {}) {
let timeoutId
const { leading = false, trailing = true } = options

return function (...args) {
const isLeading = !timeoutId && leading

clearTimeout(timeoutId)

timeoutId = setTimeout(() => {
timeoutId = null
if (trailing) {
fn.apply(this, args)
}
}, delay)

if (isLeading) {
fn.apply(this, args)
}
}
}

Практические примеры

// Debounce для поиска
const searchInput = document.getElementById('search')
const debouncedSearch = debounce(async (query) => {
const results = await fetchResults(query)
displayResults(results)
}, 300)

searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value)
})

// Throttle для скролла
const throttledScroll = throttle(() => {
updateScrollPosition()
checkLazyLoad()
}, 100)

window.addEventListener('scroll', throttledScroll)

// Throttle для resize
const throttledResize = throttle(() => {
recalculateLayout()
}, 200)

window.addEventListener('resize', throttledResize)

Debounce в Vue 3 с Composition API

<script setup>
import { ref, watch } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'

const searchQuery = ref('')
const results = ref([])

// Использование VueUse
const debouncedSearch = useDebounceFn(async (query) => {
results.value = await fetchResults(query)
}, 300)

watch(searchQuery, (newQuery) => {
debouncedSearch(newQuery)
})
</script>

Throttle в Golang

Аналог throttle можно реализовать на Golang для rate limiting:

package main

import (
"sync"
"time"
)

type Throttle struct {
interval time.Duration
lastTime time.Time
mu sync.Mutex
}

func NewThrottle(interval time.Duration) *Throttle {
return &Throttle{interval: interval}
}

func (t *Throttle) Do(fn func()) {
t.mu.Lock()
defer t.mu.Unlock()

now := time.Now()
if now.Sub(t.lastTime) >= t.interval {
t.lastTime = now
fn()
}
}

// Использование
throttle := NewThrottle(300 * time.Millisecond)
for i := 0; i < 100; i++ {
throttle.Do(func() {
fmt.Println("Executed at:", time.Now())
})
time.Sleep(50 * time.Millisecond)
}

Когда что использовать

ЗадачаРешение
Поиск при вводе текстаDebounce
Автосохранение формыDebounce
Обработка скроллаThrottle
Обработка resizeThrottle
Кнопка с защитой от повторного кликаThrottle
Lazy loading изображенийThrottle
Отправка аналитикиThrottle

Для Golang-разработчика понимание debounce/throttle важно при работе с фронтенд-частью и при реализации rate limiting на бэкенде.

Вопрос 30. Что делает метод bind функции и где он используется?

Таймкод: 00:46:39

Ответ собеседния: Правильный. bind нужен для изменения контекста функции. Первым аргументом передаётся объект, который будет контекстом (this) в момент вызова. bind возвращает функцию с привязанным контекстом. Используется, например, при передаче метода объекта как обработчика события, чтобы сохранить контекст.

Правильный ответ:

Кандидат дал правильный ответ. Дополним деталями и примерами.

Определение

bind создаёт новую функцию с привязанным значением this и, опционально, предустановленными аргументами.

Синтаксис

const boundFn = fn.bind(thisArg, arg1, arg2, ...)

Проблема: потеря контекста

const user = {
name: 'John',
greet() {
console.log(`Hello, ${this.name}`)
}
}

user.greet() // "Hello, John"

// При передаче как колбэка — контекст теряется
const greetFn = user.greet
greetFn() // "Hello, undefined" (this === undefined в strict mode)

// Или в обработчике события
button.addEventListener('click', user.greet)
// this будет указывать на button, а не на user

Решение с bind

const boundGreet = user.greet.bind(user)
boundGreet() // "Hello, John"

button.addEventListener('click', user.greet.bind(user))
// this всегда будет user

bind с предустановленными аргументами (частичное применение)

function multiply(a, b) {
return a * b
}

const double = multiply.bind(null, 2)
const triple = multiply.bind(null, 3)

double(5) // 10
triple(5) // 15

bind в классах React

class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }

// Без bind — this будет undefined в handleClick
this.handleClick = this.handleClick.bind(this)
}

handleClick() {
this.setState({ count: this.state.count + 1 })
}

render() {
return <button onClick={this.handleClick}>Count: {this.state.count}</button>
}
}

Альтернативы bind

1. Стрелочные функции (не имеют своего this)

class Counter {
count = 0

// Стрелочная функция — this привязан к экземпляру
handleClick = () => {
this.count++
console.log(this.count)
}
}

2. Стрелочная функция в вызове

button.addEventListener('click', () => user.greet())

3. call и apply (немедленный вызов)

// call — вызов с контекстом и аргументами
user.greet.call(user, arg1, arg2)

// apply — вызов с контекстом и массивом аргументов
user.greet.apply(user, [arg1, arg2])

// bind — возврат новой функции (отложенный вызов)
const bound = user.greet.bind(user, arg1, arg2)
bound()

Сравнение

МетодВызовВозвращает
callНемедленныйРезультат функции
applyНемедленныйРезультат функции
bindОтложенныйНовую функцию

bind в Vue 2

export default {
data() {
return {
count: 0
}
},
created() {
// Привязка контекста для использования в watch или таймере
this.debouncedUpdate = this.update.bind(this)
},
methods: {
update() {
console.log(this.count) // this — экземпляр Vue
}
}
}

bind в Golang — аналогия

В Golang нет прямого аналога bind, но можно использовать замыкания:

type User struct {
Name string
}

func (u *User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}

func main() {
user := &User{Name: "John"}

// Замыкание — аналог bind
greetFunc := func() {
user.Greet()
}

greetFunc() // "Hello, John"
}

Для Golang-разработчика понимание bind важно при работе с фронтенд-частью, особенно при работе с обработчиками событий и колбэками.

Вопрос 31. В чем разница между ref и reactive в Vue?

Таймкод: 00:48:38

Ответ собеседника: Правильный. ref используется для оборачивания примитивов (числа, строки, булевы) и создаёт обёртку с полем value. reactive используется для объектов, но не делает реактивными вложенные объекты — при их изменении не будет ре-рендера.

Правильный ответ:

Кандидат правильно описал основные различия. Дополним деталями и исправим неточность.

Определения

ref — создаёт реактивную ссылку на значение. Оборачивает значение в объект с полем value.

reactive — создаёт реактивную копию объекта. Использует Proxy для отслеживания изменений.

Синтаксис

import { ref, reactive } from 'vue'

// ref — для примитивов
const count = ref(0)
const name = ref('John')
const isActive = ref(false)

// Доступ через .value
count.value++
console.log(count.value) // 1

// reactive — для объектов
const state = reactive({
count: 0,
name: 'John',
isActive: false
})

// Прямой доступ
state.count++
console.log(state.count) // 1

Ключевые различия

Характеристикаrefreactive
Тип данныхПримитивы и объектыТолько объекты
ДоступЧерез .valueПрямой
Переопределениеcount.value = 5Теряет реактивность
ДеструктурированиеСохраняет реактивностьТеряет реактивность
ШаблонАвтоматически разворачиваетсяПрямой доступ

Важно: reactive и вложенные объекты

Кандидат отметил, что reactive «не делает реактивными вложенные объекты». Это неточность — reactive делает реактивными вложенные объекты (deep reactivity):

const state = reactive({
user: {
name: 'John',
address: {
city: 'Moscow'
}
}
})

// Это вызовет ре-рендер
state.user.address.city = 'Saint Petersburg' // Реактивно!

Проблема reactive: потеря реактивности

const state = reactive({
count: 0,
name: 'John'
})

// Деструктурирование теряет реактивность
const { count, name } = state
count++ // Не реактивно!

// Переопределение теряет реактивность
let count = state.count
count++ // Не реактивно!

// Переопределение всего объекта
state = { count: 1, name: 'Jane' } // Ошибка! Потеря ссылки

Решение: toRefs

import { reactive, toRefs } from 'vue'

const state = reactive({
count: 0,
name: 'John'
})

// toRefs сохраняет реактивность при деструктурировании
const { count, name } = toRefs(state)
count.value++ // Реактивно!

Когда что использовать

// ref — для примитивов и одиночных значений
const isLoading = ref(false)
const error = ref(null)
const currentPage = ref(1)

// reactive — для объектов с несколькими связанными полями
const form = reactive({
email: '',
password: '',
rememberMe: false,
})

const filters = reactive({
search: '',
category: null,
sortBy: 'date',
})

ref для объектов

ref также работает с объектами — он автоматически вызывает reactive для значения:

const user = ref({
name: 'John',
age: 30
})

// Доступ через .value
user.value.name = 'Jane'

// В шаблоне — автоматически разворачивается
// <template>{{ user.name }}</template>

Практический пример

<script setup>
import { ref, reactive, toRefs } from 'vue'

// ref для отдельных значений
const loading = ref(false)
const error = ref(null)

// reactive для связанных данных
const pagination = reactive({
page: 1,
limit: 20,
total: 0,
})

// toRefs для деструктурирования
const { page, limit, total } = toRefs(pagination)

async function fetchUsers() {
loading.value = true
error.value = null
try {
const { data, total: totalCount } = await api.getUsers({
page: page.value,
limit: limit.value,
})
users.value = data
total.value = totalCount
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
</script>

Для Golang-разработчика аналогия: ref — как указатель *int, через который можно менять значение. reactive — как структура, поля которой можно менять напрямую, но при деструктурировании нужно использовать toRefs (аналог взятия адреса &field).

Вопрос 32. Что такое nextTick и зачем он нужен?

Таймкод: 00:49:51

Ответ собеседника: Неполный. nextTick — это метод Vue, который позволяет вызвать функцию на следующий цикл обновления DOM-дерева. Используется для обращения к дом-элементам до их обновления или сразу после изменения (например, для установки фокуса). Неполно описаны кейсы использования.

Правильный ответ:

Кандидат правильно определил nextTick, но не раскрыл кейсы использования. Дополним.

Определение

nextTick — это функция, которая откладывает выполнение колбэка до следующего цикла обновления DOM. Vue обновляет DOM асиннхронно — при изменении данных изменения не применяются мгновенно, а группируются и применяются в следующем «тике».

Как работает

import { ref, nextTick } from 'vue'

const count = ref(0)

count.value = 1
// DOM ещё не обновлён, count в DOM всё ещё 0

await nextTick()
// DOM обновлён, count в DOM = 1

Кейсы использования

1. Доступ к DOM после изменения данных

import { ref, nextTick } from 'vue'

const showInput = ref(false)
const inputRef = ref(null)

async function toggleInput() {
showInput.value = true

// inputRef.value ещё null — DOM не обновлён
console.log(inputRef.value) // null

await nextTick()

// Теперь можно работать с элементом
inputRef.value.focus()
}

2. Работа с обновлённым DOM после v-for

import { ref, nextTick } from 'vue'

const items = ref([1, 2, 3])

async function addItem() {
items.value.push(4)

await nextTick()

// Теперь можно получить последний добавленный элемент
const lastItem = document.querySelector('.item:last-child')
lastItem.scrollIntoView()
}

3. Получение размеров элемента после изменения

const isExpanded = ref(false)
const contentRef = ref(null)

async function toggleExpand() {
isExpanded.value = !isExpanded.value

await nextTick()

// Получить высоту контента после раскрытия
const height = contentRef.value.scrollHeight
animateHeight(height)
}

4. Работа с библиотеками, которые требуют готовый DOM

async function initChart() {
chartData.value = await fetchData()

await nextTick()

// Инициализация графика после обновления DOM
const ctx = document.getElementById('chart')
new Chart(ctx, { data: chartData.value })
}

5. Тестирование

import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'

test('updates DOM after data change', async () => {
const wrapper = mount(Component)

wrapper.vm.message = 'Hello'

await nextTick()

expect(wrapper.text()).toContain('Hello')
})

nextTick в Options API

export default {
data() {
return {
showInput: false
}
},
methods: {
async toggleInput() {
this.showInput = true
await this.$nextTick()
this.$refs.input.focus()
}
}
}

nextTick в Composition API

import { nextTick } from 'vue'

// С async/await
await nextTick()

// С колбэком (как в Vue 2)
nextTick(() => {
// DOM обновлён
})

Связь с Event Loop

1. Изменение данных (count.value = 1)
2. Vue помещает обновление DOM в очередь микротасок
3. Синхронный код продолжает выполняться
4. nextTick добавляет колбэк в очередь микротасок
5. После завершения синхронного кода — выполняются микротаски
6. Vue обновляет DOM
7. Выполняется колбэк nextTick

Аналогия с Golang

Для Golang-разработчика аналогия — runtime.Gosched() или работа с каналами:

// Аналогия: ожидание завершения горутины
done := make(chan struct{})
go func() {
// Асинхронная работа
updateDOM()
close(done)
}()
<-done // Ожидание завершения, как await nextTick()

Когда не нужен nextTick

Если вы работаете только с реактивными данными (не с DOM), nextTick не нужен:

// Не нужен nextTick — данные обновлены синхронно
const count = ref(0)
count.value = 1
console.log(count.value) // 1 — сразу

Для Golang-разработчика понимание nextTick важно при работе с фронтенд-частью, особенно при интеграции с DOM-библиотеками и при тестировании.

Вопрос 33. Что такое слоты в Vue и можно ли использовать слот в цикле?

Таймкод: 00:51:36

Ответ собеседника: Правильный. Слоты — это способ передавать контент из родителя в дочерние компоненты, не нарушая их структуру. Используются для создания переиспользуемых компонентов (например, карточек). Есть именованные слоты для передачи нескольких слотов и v-slot для передачи данных. Да, можно использовать слот в цикле через scoped slots для передачи данных от дочернего к родителю.

Правильный ответ:

Кандидат дал правильный ответ. Дополним примерами.

Определение

Слоты (slots) — механизм композиции компонентов, позволяющий родителю передавать контент в дочерний компонент.

Базовый слот

<!-- ChildComponent.vue -->
<template>
<div class="card">
<slot></slot> <!-- Контент из родителя -->
</div>
</template>

<!-- ParentComponent.vue -->
<template>
<ChildComponent>
<p>Этот текст будет внутри .card</p>
</ChildComponent>
</template>

Именованные слоты

<!-- LayoutComponent.vue -->
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- Имя по умолчанию -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>

<!-- ParentComponent.vue -->
<template>
<LayoutComponent>
<template #header>
<h1>Заголовок</h1>
</template>

<p>Основной контент</p>

<template #footer>
<p>Подвал</p>
</template>
</LayoutComponent>
</template>

Scoped slots (слоты с данными)

Передача данных от дочернего компонента к родителю:

<!-- UserList.vue -->
<template>
<div v-for="user in users" :key="user.id">
<!-- Передаём данные родителю через scoped slot -->
<slot :user="user" :index="index">
<!-- Fallback контент -->
<span>{{ user.name }}</span>
</slot>
</div>
</template>

<!-- ParentComponent.vue -->
<template>
<UserList :users="users">
<template #default="{ user, index }">
<div class="user-card">
<span>{{ index + 1 }}. {{ user.name }}</span>
<button @click="editUser(user)">Edit</button>
</div>
</template>
</UserList>
</template>

Слот в цикле

<!-- TableComponent.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.key">
<slot name="header" :column="column">
{{ column.title }}
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td v-for="column in columns" :key="column.key">
<slot name="cell" :row="row" :column="column">
{{ row[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>

<!-- ParentComponent.vue -->
<template>
<TableComponent :columns="columns" :rows="users">
<template #header="{ column }">
<strong>{{ column.title }}</strong>
</template>

<template #cell="{ row, column }">
<template v-if="column.key === 'email'">
<a :href="`mailto:${row.email}`">{{ row.email }}</a>
</template>
<template v-else-if="column.key === 'actions'">
<button @click="edit(row)">Edit</button>
<button @click="delete(row)">Delete</button>
</template>
<template v-else>
{{ row[column.key] }}
</template>
</template>
</TableComponent>
</template>

Renderless Components (компоненты без рендера)

Компоненты, которые только предоставляют данные через scoped slots:

<!-- FetchData.vue -->
<script setup>
import { ref, onMounted } from 'vue'

const props = defineProps(['url'])
const data = ref(null)
const loading = ref(true)
const error = ref(null)

onMounted(async () => {
try {
const response = await fetch(props.url)
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>

<template>
<slot :data="data" :loading="loading" :error="error"></slot>
</template>

<!-- ParentComponent.vue -->
<template>
<FetchData url="/api/users">
<template #default="{ data, loading, error }">
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<UserList v-else :users="data" />
</template>
</FetchData>
</template>

Аналогия с Golang

Для Golang-разработчика слоты — это как передача функции-колбэка:

// Renderless component — аналог
type DataFetcher[T any] struct {
URL string
}

func (f *DataFetcher) Fetch() (T, error) {
// Получение данных
}

// Родитель определяет, как отобразить данные
func renderUsers(fetcher *DataFetcher[[]User]) string {
users, err := fetcher.Fetch()
if err != nil {
return renderError(err)
}
return renderUserList(users)
}

Для Golang-разработчика понимание слотов важно при работе с фронтенд-частью, особенно при создании переиспользуемых компонентов и паттернов композиции.

Вопрос 34. Что такое FSD (Feature-Sliced Design) и каково твоё мнение о нём?

Таймкод: 00:53:54

Ответ собеседника: Правильный. FSD — это методология для создания архитектуры, подходящая для больших проектов. Плюсы: проект становится более масштабируемым и расширяемым благодаря чёткой структуре кода, разделению на независимые слои и слайсы с однонаправленными связями. Минусы: повышается сложность разработки, скорость может упасть, не подходит для мелких проектов или быстрого MVP. Могут возникать конфликты и путаница при определении куда вынести код.

Правильный ответ:

Кандидат дал развёрнутый ответ. Дополним деталями.

Определение

Feature-Sliced Design (FSD) — архитектурная методология для фронтенд-проектов, основанная на разделении кода по функциональным доменам (фичам), а не по техническим слоям.

Структура FSD

src/
├── app/ # Инициализация приложения
│ ├── providers/
│ ├── styles/
│ └── index.ts
├── pages/ # Страницы (роуты)
│ ├── home/
│ ├── profile/
│ └── catalog/
├── widgets/ # Самостоятельные блоки UI
│ ├── header/
│ ├── sidebar/
│ └── product-card/
├── features/ # Пользовательские сценарии
│ ├── auth/
│ ├── search/
│ └── add-to-cart/
├── entities/ # Бизнес-сущности
│ ├── user/
│ ├── product/
│ └── order/
└── shared/ # Переиспользуемый код
├── ui/
├── api/
├── lib/
└── config/

Слои FSD (снизу вверх)

shared — переиспользуемый код без бизнес-логики: UI-компоненты, утилиты, API-клиенты.

entities — бизнес-сущности: User, Product, Order. Содержат типы, модели, API.

features — пользовательские сценарии: авторизация, поиск, добавление в корзину.

widgets — самостоятельные блоки UI, составленные из entities и features.

pages — страницы приложения, собирающие widgets.

app — инициализация приложения, провайдеры, роутинг.

Правило импортов

Каждый слой может импортировать только из слоёв ниже:

app → pages → widgets → features → entities → shared

Нельзя: entities импортирует из features.

Пример: слой entities

// src/entities/user/index.ts
export type { User } from './model/types'
export { userApi } from './api/userApi'
export { UserCard } from './ui/UserCard'
export { useUser } from './model/useUser'

// src/entities/user/model/types.ts
export interface User {
id: number
name: string
email: string
}

// src/entities/user/api/userApi.ts
export const userApi = {
getById: (id: number) => fetch(`/api/users/${id}`),
getAll: () => fetch('/api/users'),
}

Пример: слой features

// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm'
export { useAuth } from './model/useAuth'

// src/features/auth/model/useAuth.ts
import { userApi } from '@/entities/user'

export function useAuth() {
const login = async (email: string, password: string) => {
const user = await userApi.login(email, password)
// ...
}
return { login }
}

Пример: слой widgets

// src/widgets/header/ui/Header.vue
<template>
<header>
<Logo />
<UserCard :user="currentUser" />
<CartButton />
</header>
</template>

<script setup>
import { UserCard } from '@/entities/user'
import { CartButton } from '@/features/cart'
</script>

Плюсы FSD

  • Чёткая структура — легко найти код
  • Масштабируемость — новые фичи добавляются как отдельные слайсы
  • Переиспользуемость — shared слой содержит общий код
  • Тестируемость — каждый слайс можно тестировать отдельно
  • Онбординг — новые разработчики быстрее понимают проект

Минусы FSD

  • Сложность для маленьких проектов
  • Накладные расходы на организацию файлов
  • Споры о том, куда отнести код (shared vs entities)
  • Кривая обучения для новых разработчиков

Когда использовать

FSD подходит для:

  • Больших проектов с множеством фич
  • Команд из 5+ человек
  • Долгоживущих продуктов
  • Проектов с частыми изменениями требований

Для Golang-разработчика аналогия: FSD похож на чистую архитектуру (Clean Architecture) или DDD (Domain-Driven Design) на бэкенде — разделение по доменам и слоям с однонаправленными зависимостями.

Вопрос 35. Что такое чистый компонент (pure component)?

Таймкод: 00:55:23

Ответ собеседника: Правильный. Чистый компонент — это компонент, который только отрисовывает интерфейс и внутри себя не имеет сайд-эффектов (или они вынесены в composables). Он отвечает только за отрисовку данных с минимальным количеством побочных эффектов.

Правильный ответ:

Кандидат дал правильный ответ. Дополним деталями.

Определение

Чистый компонент (Pure Component / Presentational Component / Dumb Component) — компонент, который только отображает данные и не содержит бизнес-логики и сайд-эффектов.

Характеристики чистого компонента

  • Получает данные через props
  • Отображает UI на основе props
  • Вызывает callback-функции из props при взаимодействии пользователя
  • Не имеет собственного состояния (или только UI-состояние)
  • Не делает запросы к API
  • Не использует глобальное состояние напрямую
  • Детерминированный — одинаковые props = одинаковый результат

Пример: чистый компонент

<!-- UserCard.vue — чистый компонент -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="$emit('edit', user)">Edit</button>
<button @click="$emit('delete', user.id)">Delete</button>
</div>
</template>

<script setup>
defineProps({
user: {
type: Object,
required: true,
},
})

defineEmits(['edit', 'delete'])
</script>

Пример: «грязный» компонент (с логикой)

<!-- UserCard.vue — грязный компонент -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="editUser">Edit</button>
<button @click="deleteUser">Delete</button>
</div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { userApi } from '@/api/user'

const props = defineProps({
user: { type: Object, required: true },
})

const router = useRouter()
const loading = ref(false)

async function editUser() {
router.push(`/users/${props.user.id}/edit`)
}

async function deleteUser() {
loading.value = true
try {
await userApi.delete(props.user.id)
// Как обновить список? Нужно пробросить callback...
} finally {
loading.value = false
}
}
</script>

Разделение: чистый компонент + composable

<!-- UserCard.vue — чистый компонент -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="$emit('edit', user)">Edit</button>
<button @click="$emit('delete', user.id)">Delete</button>
</div>
</template>

<script setup>
defineProps({
user: { type: Object, required: true },
})
defineEmits(['edit', 'delete'])
</script>
// composables/useUsers.js
import { ref } from 'vue'
import { userApi } from '@/api/user'

export function useUsers() {
const users = ref([])
const loading = ref(false)

async function fetchUsers() {
loading.value = true
try {
users.value = await userApi.getAll()
} finally {
loading.value = false
}
}

async function deleteUser(id) {
await userApi.delete(id)
users.value = users.value.filter(u => u.id !== id)
}

return { users, loading, fetchUsers, deleteUser }
}
<!-- UserList.vue — контейнерный компонент -->
<template>
<div v-if="loading">Loading...</div>
<UserCard
v-for="user in users"
:key="user.id"
:user="user"
@edit="handleEdit"
@delete="deleteUser"
/>
</template>

<script setup>
import { onMounted } from 'vue'
import UserCard from './UserCard.vue'
import { useUsers } from '@/composables/useUsers'

const { users, loading, fetchUsers, deleteUser } = useUsers()

onMounted(fetchUsers)
</script>

Плюсы чистых компонентов

  • Переиспользуемость — можно использовать в разных контекстах
  • Тестируемость — легко тестировать без моков API
  • Предсказуемость — одинаковые props = одинаковый результат
  • Читаемость — легко понять, что делает компонент
  • Отладка — проще находить ошибки

Паттерн: Container/Presentational

Container (умный) Presentational (чистый)
┌─────────────────┐ ┌─────────────────┐
│ - Логика │ │ - UI │
│ - API запросы │───────▶│ - Props │
│ - Состояние │ │ - Events │
│ - Обработка │ │ - Отрисовка │
└─────────────────┘ └─────────────────┘

Аналогия с Golang

Для Golang-разработчика это похоже на разделение на handler и view:

// Handler (container) — логика
func UserHandler(w http.ResponseWriter, r *http.Request) {
users, err := userService.GetAll(r.Context())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
renderTemplate(w, "users.html", users)
}

// Template (presentational) — только отрисовка
// users.html
// {{range .Users}}
// <div>{{.Name}}</div>
// {{end}}

Для Golang-разработчика понимание чистых компонентов важно при работе с фронтенд-частью, особенно при создании переиспользуемых UI-компонентов и тестировании.

Вопрос 36. В чем суть хорошей архитектуры?

Таймкод: 00:56:09

Ответ собеседника: Правильный. Суть хорошей архитектуры в том, чтобы проект было легко разрабатывать, поддерживать и масштабировать. Разработчикам должно быть комфортно работать на проекте, не возникало конфликтов, и чтобы для добавления новых фич не нужно было переписывать весь старый код. Для бизнеса тоже плюс — проект легко расширять, не нужно много времени уделять рефакторингу, и крупные фичи внедряются быстрее.

Правильный ответ:

Кандидат дал отличный ответ. Дополним принципами и примерами.

Принципы хорошей архитектуры

1. Разделение ответственности (Separation of Concerns)

Каждый модуль отвечает за одну вещь:

// Плохо — всё в одном месте
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Валидация
// Бизнес-логика
// Работа с БД
// Формирование ответа
}

// Правильно — разделение
type UserService struct {
repo UserRepository
validator UserValidator
}

func (s *UserService) CreateUser(ctx context.Context, dto CreateUserDTO) (*User, error) {
if err := s.validator.Validate(dto); err != nil {
return nil, err
}
return s.repo.Create(ctx, dto)
}

2. Слабая связанность (Low Coupling)

Модули должны зависеть от абстракций, а не от реализаций:

// Интерфейс — абстракция
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
}

// Реализация может быть заменена
type PostgresUserRepo struct{ db *sql.DB }
type MockUserRepo struct{ users map[int64]*User }

3. Высокая когезия (High Cohesion)

Связанный код должен быть рядом:

// Плохо — разбросано
/user/handler.go
/user/service.go
/user/repository.go
/product/model/user.go // модель user в папке product?

// Правильно — сгруппировано по доменам
/internal/
/user/
handler.go
service.go
repository.go
model.go
/product/
handler.go
service.go
repository.go
model.go

4. Принцип единственной ответственности (SRP)

Каждый модуль имеет одну причину для изменения:

// Плохо — несколько причин для изменения
type UserService struct{}
func (s *UserService) CreateUser() error { /* ... */ }
func (s *UserService) SendEmail() error { /* ... */ }
func (s *UserService) GenerateReport() error { /* ... */ }

// Правильно — разделено
type UserService struct{} // Управление пользователями
type EmailService struct{} // Отправка писем
type ReportService struct{} // Генерация отчётов

5. Принцип открытости/закрытости (OCP)

Открыт для расширения, закрыт для модификации:

// Интерфейс для уведомлений
type Notifier interface {
Send(ctx context.Context, msg string) error
}

// Расширение без изменения существующего кода
type EmailNotifier struct{}
type SMSNotifier struct{}
type PushNotifier struct{}

// Использование
func notifyUser(ctx context.Context, notifier Notifier, msg string) error {
return notifier.Send(ctx, msg)
}

6. Инверсия зависимостей (DIP)

Зависимости направлены внутрь, к абстракциям:

Presentation → Domain ← Infrastructure
↓ ↓ ↓
Handlers Entities Repositories
DTOs Services External APIs

Практические рекомендации

Чистая архитектура на Golang:

// internal/domain/user.go — бизнес-сущность
type User struct {
ID int64
Name string
Email string
}

// internal/domain/user_repository.go — интерфейс
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
}

// internal/service/user_service.go — бизнес-логика
type UserService struct {
repo UserRepository
}

func (s *UserService) CreateUser(ctx context.Context, name, email string) (*User, error) {
user := &User{Name: name, Email: email}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}

// internal/repository/postgres/user.go — реализация
type PostgresUserRepo struct {
db *sql.DB
}

func (r *PostgresUserRepo) Create(ctx context.Context, user *User) error {
return r.db.QueryRowContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
user.Name, user.Email,
).Scan(&user.ID)
}

// cmd/app/main.go — композиция
func main() {
db := connectDB()
repo := postgres.NewUserRepo(db)
service := service.NewUserService(repo)
handler := handler.NewUserHandler(service)
// ...
}

Метрики хорошей архитектуры

  • Время онбординга — новый разработчик разбирается в проекте за дни, не месяцы
  • Скорость добавления фич — новая фича добавляется без переписывания старого кода
  • Частота багов — изменения в одном месте не ломают другое
  • Время на ревью — code review проходит быстро из-за понятной структуры

Антипаттерны

  • God Object — один класс/структура делает всё
  • Spaghetti Code — хаотичные зависимости
  • Copy-Paste Programming — дублирование вместо абстракции
  • Premature Optimization — оптимизация до появления реальных проблем

Для Golang-разработчика понимание принципов архитектуры критически важно — это основа для проектирования масштабируемых и поддерживаемых систем.

Вопрос 37. Реализовать класс EventEmitter с методами on, off, emit

Таймкод: 00:57:13

Ответ собеседника: Правильный. Класс EventEmitter реализован с тремя методами: on (подписка на событие), off (отписка от события), emit (генерация события). Данные хранятся в виде объекта, где ключ — название события, значение — массив функций-обработчиков. Метод on возвращает функцию для отписки. Проверка показала правильную работу: при повторной подписке на событие обработчик вызывается соответствующее количество раз, при отписке перестаёт вызываться.

Правильный ответ:

Кандидат дал правильное описание. Приведём полную реализацию.

Реализация на JavaScript

class EventEmitter {
constructor() {
this.events = {}
}

/**
* Подписка на событие
* @param {string} event - Название события
* @param {Function} handler - Обработчик
* @returns {Function} Функция для отписки
*/
on(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(handler)

// Возвращаем функцию для отписки
return () => this.off(event, handler)
}

/**
* Отписка от события
* @param {string} event - Название события
* @param {Function} handler - Обработчик для удаления
*/
off(event, handler) {
if (!this.events[event]) return

this.events[event] = this.events[event].filter(h => h !== handler)

// Очищаем пустой массив
if (this.events[event].length === 0) {
delete this.events[event]
}
}

/**
* Генерация события
* @param {string} event - Название события
* @param {...any} args - Аргументы для обработчиков
*/
emit(event, ...args) {
if (!this.events[event]) return

this.events[event].forEach(handler => {
handler(...args)
})
}

/**
* Подписка на событие только один раз
* @param {string} event - Название события
* @param {Function} handler - Обработчик
*/
once(event, handler) {
const wrapper = (...args) => {
handler(...args)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
}

Использование

const emitter = new EventEmitter()

// Подписка
const unsubscribe = emitter.on('user:created', (user) => {
console.log('User created:', user.name)
})

// Несколько обработчиков
emitter.on('user:created', (user) => {
console.log('Send welcome email to:', user.email)
})

// Генерация события
emitter.emit('user:created', { name: 'John', email: 'john@example.com' })
// User created: John
// Send welcome email to: john@example.com

// Отписка
unsubscribe()
emitter.emit('user:created', { name: 'Jane', email: 'jane@example.com' })
// Send welcome email to: jane@example.com

// Одноразовая подписка
emitter.once('app:ready', () => {
console.log('App is ready!')
})

emitter.emit('app:ready') // App is ready!
emitter.emit('app:ready') // Ничего не произошло

Реализация на Golang

package event

import (
"fmt"
"sync"
)

type EventEmitter struct {
handlers map[string][]func(args ...any)
mu sync.RWMutex
}

func NewEventEmitter() *EventEmitter {
return &EventEmitter{
handlers: make(map[string][]func(args ...any)),
}
}

func (e *EventEmitter) On(event string, handler func(args ...any)) {
e.mu.Lock()
defer e.mu.Unlock()
e.handlers[event] = append(e.handlers[event], handler)
}

func (e *EventEmitter) Off(event string, handler func(args ...any)) {
e.mu.Lock()
defer e.mu.Unlock()

handlers, exists := e.handlers[event]
if !exists {
return
}

filtered := make([]func(args ...any), 0, len(handlers))
for _, h := range handlers {
// Сравнение функций в Go невозможно напрямую,
// поэтому используем другие подходы для отписки
if fmt.Sprintf("%v", h) != fmt.Sprintf("%v", handler) {
filtered = append(filtered, h)
}
}
e.handlers[event] = filtered
}

func (e *EventEmitter) Emit(event string, args ...any) {
e.mu.RLock()
defer e.mu.RUnlock()

for _, handler := range e.handlers[event] {
handler(args...)
}
}

Использование в Go

func main() {
emitter := NewEventEmitter()

// Подписка
emitter.On("user:created", func(args ...any) {
fmt.Println("User created:", args[0])
})

// Генерация события
emitter.Emit("user:created", "John")
// Output: User created: John
}

Расширенная версия с once

class EventEmitter {
constructor() {
this.events = {}
this.maxListeners = 10
}

on(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}

// Предупреждение о возможной утечке
if (this.events[event].length >= this.maxListeners) {
console.warn(`Max listeners exceeded for event "${event}"`)
}

this.events[event].push(handler)
return () => this.off(event, handler)
}

once(event, handler) {
const wrapper = (...args) => {
this.off(event, wrapper)
handler(...args)
}
return this.on(event, wrapper)
}

off(event, handler) {
if (!this.events[event]) return
this.events[event] = this.events[event].filter(h => h !== handler)
if (this.events[event].length === 0) {
delete this.events[event]
}
}

emit(event, ...args) {
if (!this.events[event]) return
// Копируем массив, чтобы безопасно итерировать при отписке внутри обработчика
const handlers = [...this.events[event]]
handlers.forEach(handler => handler(...args))
}

listenerCount(event) {
return this.events[event]?.length || 0
}

removeAllListeners(event) {
if (event) {
delete this.events[event]
} else {
this.events = {}
}
}
}

EventEmitter — фундаментальный паттерн, используемый в Node.js (модуль events), Vue (mitt), и многих других библиотеках. Для Golang-разработчика аналогия — каналы и select для коммуникации между горутинами.

Вопрос 38. Как добавить метод once, который выполняет обработчик только один раз?

Таймкод: 01:05:44

Ответ собеседника: Правильный. Можно передать дополнительный аргумент (например, once) и в объекте this.events хранить не только колбэк, но и флаг once: true/false. При вызове колбэк проверять флаг, и если он равен true, удалять обработчик из массива после выполнения.

Правильный ответ:

Кандидат описал один из подходов. Приведём оба варианта реализации.

Вариант 1: Обёртка (wrapper function)

Самый простой и чистый подход — обернуть обработчик в функцию, которая сама отпишется после первого вызова:

once(event, handler) {
const wrapper = (...args) => {
this.off(event, wrapper) // Отписываемся до вызова обработчика
handler(...args) // Вызываем оригинальный обработчик
}
return this.on(event, wrapper) // Подписываем wrapper
}

Преимущества:

  • Не меняем структуру хранения обработчиков
  • Код простой и понятный
  • Не нужно менять логику emit

Вариант 2: Флаг once в обработчике

Как предложил кандидат — хранить обработчики как объекты с флагом:

class EventEmitter {
constructor() {
this.events = {}
}

on(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}
// Храним как объект { handler, once }
this.events[event].push({ handler, once: false })
return () => this.off(event, handler)
}

once(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push({ handler, once: true })
return () => this.off(event, handler)
}

off(event, handler) {
if (!this.events[event]) return
this.events[event] = this.events[event].filter(item => item.handler !== handler)
}

emit(event, ...args) {
if (!this.events[event]) return

// Фильтруем обработчики с once: true
const remaining = []
for (const item of this.events[event]) {
item.handler(...args)
if (!item.once) {
remaining.push(item)
}
}
this.events[event] = remaining
}
}

Сравнение подходов

ХарактеристикаWrapperФлаг once
СложностьПростойСредняя
Изменение emitНетДа
Отписка по ссылкеСложнееПроще
ПроизводительностьЧуть медленнееЧуть быстрее

Рекомендация

Вариант с wrapper предпочтительнее — он проще, не требует изменения внутренней структуры данных и легче поддерживается.

Полная реализация с once

class EventEmitter {
constructor() {
this.events = {}
}

on(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(handler)
return () => this.off(event, handler)
}

once(event, handler) {
const wrapper = (...args) => {
this.off(event, wrapper)
handler(...args)
}
return this.on(event, wrapper)
}

off(event, handler) {
if (!this.events[event]) return
this.events[event] = this.events[event].filter(h => h !== handler)
}

emit(event, ...args) {
if (!this.events[event]) return
const handlers = [...this.events[event]]
handlers.forEach(handler => handler(...args))
}
}

Использование

const emitter = new EventEmitter()

// once — обработчик вызовется только один раз
emitter.once('app:ready', () => {
console.log('App is ready!')
})

emitter.emit('app:ready') // App is ready!
emitter.emit('app:ready') // Ничего не произошло

// Отписка до вызова
const unsubscribe = emitter.once('data', (data) => {
console.log('Data:', data)
})
unsubscribe() // Отписались до вызова
emitter.emit('data', { value: 42 }) // Ничего не произошло

Для Golang-разработчика аналогия: once — как sync.Once, который гарантирует выполнение функции только один раз:

var once sync.Once

once.Do(func() {
fmt.Println("Выполнится только один раз")
})

Вопрос 39. Написать функцию для получения свойства из объекта по пути

Таймкод: 01:07:25

Ответ собеседника: Правильный. Функция принимает объект и строку пути. Путь разбивается через split, затем происходит проход по ключам объекта с использованием цикла. Если ключ не существует, возвращается undefined. Решение использует проверку на существование ключа и возврат значения или undefined. Проверка показала правильную работу функции.

Правильный ответ:

Кандидат описал правильный подход. Приведём полную реализацию.

Базовая реализация

function get(obj, path) {
const keys = path.split('.')
let result = obj

for (const key of keys) {
if (result === undefined || result === null) {
return undefined
}
result = result[key]
}

return result
}

// Использование
const user = {
name: 'John',
address: {
city: 'Moscow',
coordinates: {
lat: 55.75,
lng: 37.62
}
}
}

get(user, 'name') // 'John'
get(user, 'address.city') // 'Moscow'
get(user, 'address.coordinates.lat') // 55.75
get(user, 'address.zip') // undefined
get(user, 'foo.bar.baz') // undefined

Реализация с поддержкой массивов

function get(obj, path) {
// Поддержка пути в виде массива или строки
const keys = Array.isArray(path) ? path : path.split('.')
let result = obj

for (const key of keys) {
if (result === undefined || result === null) {
return undefined
}
result = result[key]
}

return result
}

// Использование с массивом
const data = {
users: [
{ name: 'John', scores: [10, 20, 30] },
{ name: 'Jane', scores: [15, 25, 35] }
]
}

get(data, ['users', '0', 'name']) // 'John'
get(data, ['users', '1', 'scores', '2']) // 35

Реализация с квадратными скобками (lodash-стиль)

function get(obj, path, defaultValue = undefined) {
// Парсим путь: 'a.b[0].c' => ['a', 'b', '0', 'c']
const keys = Array.isArray(path)
? path
: path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean)

let result = obj

for (const key of keys) {
if (result === undefined || result === null) {
return defaultValue
}
result = result[key]
}

return result === undefined ? defaultValue : result
}

// Использование
const data = {
users: [
{ name: 'John', address: { city: 'Moscow' } }
]
}

get(data, 'users[0].name') // 'John'
get(data, 'users[0].address.city') // 'Moscow'
get(data, 'users[0].address.zip', 'N/A') // 'N/A'

Реализация на Golang

package main

import (
"fmt"
"strconv"
"strings"
)

func Get(obj any, path string, defaultValue ...any) any {
keys := strings.Split(path, ".")
var result any = obj

for _, key := range keys {
switch v := result.(type) {
case map[string]any:
val, exists := v[key]
if !exists {
if len(defaultValue) > 0 {
return defaultValue[0]
}
return nil
}
result = val
case []any:
index, err := strconv.Atoi(key)
if err != nil || index < 0 || index >= len(v) {
if len(defaultValue) > 0 {
return defaultValue[0]
}
return nil
}
result = v[index]
default:
if len(defaultValue) > 0 {
return defaultValue[0]
}
return nil
}
}

if result == nil && len(defaultValue) > 0 {
return defaultValue[0]
}
return result
}

func main() {
data := map[string]any{
"user": map[string]any{
"name": "John",
"address": map[string]any{
"city": "Moscow",
},
},
}

fmt.Println(Get(data, "user.name")) // John
fmt.Println(Get(data, "user.address.city")) // Moscow
fmt.Println(Get(data, "user.address.zip", "N/A")) // N/A
}

Тесты

const assert = require('assert')

const obj = {
a: {
b: {
c: 42,
d: null,
e: undefined
},
f: [{ g: 'hello' }, { g: 'world' }]
}
}

// Базовые случаи
assert.strictEqual(get(obj, 'a.b.c'), 42)
assert.strictEqual(get(obj, 'a.b.d'), null)
assert.strictEqual(get(obj, 'a.b.e'), undefined)
assert.strictEqual(get(obj, 'a.b.x'), undefined)

// С дефолтным значением
assert.strictEqual(get(obj, 'a.b.x', 'default'), 'default')
assert.strictEqual(get(obj, 'a.b.d', 'default'), null) // null !== undefined

// Массивы
assert.strictEqual(get(obj, 'a.f.0.g'), 'hello')
assert.strictEqual(get(obj, 'a.f.1.g'), 'world')

// Граничные случаи
assert.strictEqual(get(null, 'a.b'), undefined)
assert.strictEqual(get(undefined, 'a.b'), undefined)
assert.strictEqual(get({}, ''), {})

console.log('All tests passed!')

Сравнение с lodash.get

Библиотека lodash предоставляет аналогичную функцию _.get:

import _ from 'lodash'

_.get(obj, 'a.b.c') // 42
_.get(obj, 'a.b.x', 'default') // 'default'
_.get(obj, ['a', 'b', 'c']) // 42

Для Golang-разработчика аналогия: это как доступ к вложенным полям структуры через reflection:

// Аналогия с reflect
val := reflect.ValueOf(obj)
for _, key := range keys {
val = val.FieldByName(key)
}