Mock-interview #2 с начинающим Фронтенд разработчиком Георгием - JavaScript, React
Сегодня мы разберём собеседование с кандидатом Георгием, который уверенно продемонстрировал практические навыки работы с React и DOM на протяжении серии задач разной сложности. Интервьюер Серёжа провёл глубокий технический разбор, затронув виртуальный DOM, синтетические события, управление состоянием и внутренние механизмы рендеринга, в итоге рекомендовав кандидата к найму.
Вопрос 1. Расскажи о своём техническом бэкграунде: какие технологии изучал, над какими задачами работал, в каких командах?
Таймкод: 00:01:43
Ответ собеседника: Правильный. Начал обучение в сентябре 2020 года на курсах, где изучал MSS, JavaScript, React и сопутствующие технологии. После курсов прошёл стажировку, а затем работал в компании над проектами онлайн-магазинов на фреймворке Oracle Commerce Cloud (OCC), где занимался компонентами, логикой, вёрсткой и админ-частью. Команда была из 5-6 человек, работа шла над отдельными задачами по страницам.
Правильный ответ:
Ответ собеседника структурированный и даёт общее представление о бэкграунде. Для позиции Go-разработчика этот ответ можно было бы усилить следующими деталями, которые показали бы глубину понимания инженерных процессов и техническую зрелость:
1. Конкретизация стека технологий
Стоит не просто перечислять технологии, а объяснять, как они взаимодействовали в архитектуре проекта. Например, если использовался React на фронтенде — каким образом фронтенд взаимодействовал с бэкендом, какие API-протоколы использовались (REST, GraphQL), как была организована аутентификация и авторизация.
2. Описание роли и зоны ответственности
Вместо общей фразы «занимался компонентами, логикой, вёрсткой» — полезно уточнить, какие именно бизнес-задачи решал. Например: реализация каталога товаров с фильтрацией и пагинацией, интеграция с платёжными шлюзами, разработка корзины покупок с учётом скидок и промокодов, админ-панель для управления контентом.
3. Работа с данными и инфраструктурой
Если был опыт работы с базами данных, кэшированием, очередями сообщений или CI/CD-пайплайнами — это важно озвучить. Даже на фронтенд-позиции понимание того, как данные хранятся, как устроена миграция схемы БД, как настроен деплой — демонстрирует системное мышление.
4. Процессы разработки
Упоминание методологии (Scrum, Kanban), инструментов управления задачами (Jira, Trello), code review, тестирования (unit, интеграционные, e2e) — всё это показывает зрелость инженера. Например: «В команде использовали Scrum с двухнедельными спринтами, code review было обязательным, покрытие тестами контролировалось через SonarQube».
5. Масштаб и сложность проектов
Стоит упомянуть объём трафика, количество пользователей, объём каталога товаров, количество интеграций с внешними системами (ERP, CRM, платёжные провайдеры, службы доставки). Это помогает интервьюеру оценить уровень сложности, с которым кандидат сталкивался.
6. Переход к Go
Если кандидат переходит с фронтенд-стека на Go-бэкенд — важно объяснить мотивацию и подготовку: какие ресурсы использовал для изучения Go, какие pet-проекты реализовал, какие концепции языка освоил (goroutines, channels, interfaces, error handling, context).
Пример усиленного ответа:
«Начал обучение в сентябре 2020 года на курсах по веб-разработке, где изучал JavaScript, React, TypeScript. После курсов прошёл стажировку, а затем работал в компании над проектами электронной коммерции на Oracle Commerce Cloud. Команда из 5-6 человек работала по Scrum с двухнедельными спринтами. Моей зоной ответственности была разработка клиентской части: каталог товаров с фильтрацией, корзина, чекаут, личный кабинет. Интегрировался с backend через REST API. Активно участвовал в code review, писал unit-тесты на Jest. Проект обслуживал десятки тысяч пользователей в месяц, каталог содержал более 100 тысяч SKU. В процессе работы заинтересовался backend-разработкой, начал изучать Go: прошёл курсы, реализовал несколько pet-проектов — REST API с авторизацией, worker для обработки задач из очереди, микросервис с gRPC-взаимодействием. Хочу развиваться именно в backend-разработке на Go, потому что привлекает производительность языка, простота конкурентности и сильная стандартная библиотека.»
Вопрос 2. Был ли code review и тестировщики в процессе сдачи задач на предыдущем месте работы?
Таймкод: 00:04:36
Ответ собеседника: Правильный. Code review не было как налаженного процесса. Тестировщиков не было. Создавался pull request, но чаще всего его никто не смотрел. Были только автоматические проверки. Из-за отсутствия возможности для развития и code review ушёл из компании.
Правильный ответ:
Ответ собеседника честный и демонстрирует зрелую позицию — он осознаёт важность инженерных практик и готов уходить из среды, где эти практики отсутствуют. Это позитивный сигнал для интервьюера. Однако ответ можно было бы усилить следующими деталями:
1. Code review как инженерная практика
Code review — это не просто «кто-то смотрит твой код». Это системный процесс, который включает:
- Формализованные критерии приёмки кода: соответствие стилю кодирования (gofmt, golint для Go), отсутствие дублирования, корректная обработка ошибок, покрытие тестами, отсутствие утечек ресурсов.
- Распределение ответственности: каждый pull request должен проходить проверку как минимум одного разработчика с достаточным уровнем экспертизы.
- Автоматизация: CI-пайплайн, который запускает линтеры, тесты, проверку покрытия кода — это минимальный барьер, через который должен пройти каждый PR.
- Культура обратной связи: code review — это не критика, а обмен знаниями. Хороший PR содержит описание изменений, ссылку на задачу, скриншоты (если UI), пояснение неочевидных решений.
2. Роль тестировщиков и тестирования
Отсутствие QA-команды — это не всегда недостаток. Во многих современных командах практика shift-left testing предполагает, что разработчики сами пишут тесты:
- Unit-тесты — проверка отдельных функций и методов.
- Интеграционные тесты — проверка взаимодействия компонентов (например, сервис и база данных).
- E2E-тесты — проверка сценариев пользователя.
- Contract-тесты — проверка API-контрактов между сервисами.
Пример для Go:
// Пример unit-теста для сервиса
func TestUserService_CreateUser(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
repo := NewUserRepository(db)
service := NewUserService(repo)
user, err := service.CreateUser("test@example.com", "password123")
assert.NoError(t, err)
assert.NotEmpty(t, user.ID)
assert.Equal(t, "test@example.com", user.Email)
}
3. Автоматические проверки в CI/CD
Собеседник упомянул, что «были только автоматические проверки». Стоит уточнить, что именно проверялось:
- Линтеры (golangci-lint для Go)
- Форматирование кода (gofmt)
- Запуск тестов
- Проверка покрытия кода
- Сборка проекта
- Проверка зависимостей (govulncheck)
4. Мотивация ухода
Упоминание того, что кандидат ушёл из-за отсутствия code review и возможностей для развития — это сильный сигнал. Это показывает, что он ценит инженерную культуру и стремится к профессиональному росту. В современных командах code review — это не опция, а обязательная практика.
Пример усиленного ответа:
«Code review формально существовал — мы создавали pull requests, но на практике их никто не смотрел. Были только автоматические проверки в CI: линтеры, тесты, сборка. Тестировщиков в команде не было, вся ответственность за качество кода лежала на разработчиках. Из-за отсутствия системного code review, обмена знаниями и инженерных практик я принял решение уйти. Для меня важно работать в команде, где code review — это не формальность, а часть культуры, где можно учиться у коллег и делиться своим опытом.»
Вопрос 3. Что такое Jest и пользовался ли ты им в рабочих проектах?
Таймкод: 00:05:58
Ответ собеседника: Правильный. Jest — инструмент для тестирования. В рабочих проектах не использовал, тесты не писали. Пользовался Jest только во время обучения на курсах, писал тесты для компонентов.
Правильный ответ:
Ответ собеседника корректный, но поверхностный. Для позиции Go-разработчика важно показать понимание тестирования как концепции, даже если опыт был на другом стеке. Вот как можно раскрыть тему глубже:
1. Jest — фреймворк для тестирования JavaScript/TypeScript
Jest — это фреймворк для тестирования, разработанный Facebook (ныне Meta). Он используется для написания и запуска тестов в JavaScript и TypeScript проектах. Основные возможности Jest:
- Unit-тестирование — тестирование отдельных функций, компонентов, модулей.
- Snapshot-тестирование — сравнение снимков UI-компонентов для обнаружения неожиданных изменений.
- Mocking — подстановка заглушек вместо реальных зависимостей (API-вызовы, таймеры, модули).
- Code coverage — отчёт о покрытии кода тестами.
- Watch mode — автоматический перезапуск тестов при изменении файлов.
2. Аналог Jest в Go
В Go тестирование является частью стандартной библиотеки — пакет testing. Для расширения возможностей используются:
- testify — библиотека с удобными ассертами (
assert.Equal,require.NoError) - gomock / mockery — генерация моков для интерфейсов
- httptest — тестирование HTTP-хендлеров
- testcontainers — интеграционные тесты с Docker-контейнерами
Пример теста на Go (аналог Jest-теста):
// user_service.go
type UserService struct {
repo UserRepository
}
func (s *UserService) CreateUser(email string) (*User, error) {
if email == "" {
return nil, ErrInvalidEmail
}
return s.repo.Save(email)
}
// user_service_test.go
func TestUserService_CreateUser(t *testing.T) {
mockRepo := NewMockUserRepository(t)
mockRepo.EXPECT().Save("test@example.com").Return(&User{ID: 1, Email: "test@example.com"}, nil)
service := NewUserService(mockRepo)
user, err := service.CreateUser("test@example.com")
assert.NoError(t, err)
assert.Equal(t, "test@example.com", user.Email)
}
3. Типы тестов
Понимание пирамиды тестирования — важный навык для любого разработчика:
- Unit-тесты (основа пирамиды) — быстрые, изолированные, проверяют отдельные функции. В Go — пакет
testing. - Интеграционные тесты — проверяют взаимодействие компонентов (сервис + БД, сервис + очередь).
- E2E-тесты (вершина пирамиды) — проверяют полные сценарии пользователя. Медленные, дорогие в поддержке.
4. Значение тестирования для Go-разработчика
Go имеет встроенную поддержку тестирования, что делает написание тестов естественной частью разработки. Команда go test запускает все тесты в проекте. Флаг -cover показывает покрытие кода. Флаг -race включает детектор гонок данных.
Пример усиленного ответа:
«Jest — это JavaScript-фреймворк для тестирования, разработанный Meta. Он предоставляет возможности для unit-тестирования, snapshot-тестирования, мокирования и измерения покрытия кода. В рабочих проектах я не использовал Jest, так как в команде не писали тесты. На курсах я писал unit-тесты для React-компонентов с использованием Jest и React Testing Library. Понимаю, что тестирование — важная часть разработки. В Go тестирование встроено в стандартную библиотеку через пакет testing, а для удобства используются библиотеки вроде testify для ассертов и mockery для генерации моков. Готов применять эти практики в работе.»
Вопрос 4. Вывести данные из JSON-файла persons в таблицу на экран, добавив колонку с цветом
Таймкод: 00:08:55
Ответ собеседника: Неполный. Приступил к выполнению задачи, начал писать код для вывода данных из файла persons.json в таблицу. Возникли затруднения с импортом данных — не получалось сразу вывести данные, возможно из-за неправильного пути или формата импорта. Задача не была завершена в отведённое время.
Правильный ответ:
Задача на чтение JSON и вывод данных в таблицу — типичное упражнение для проверки базовых навыков работы с файлами, структурами данных и форматированием вывода. Вот полное решение:
1. Структура JSON-файла
Предположим, файл persons.json имеет следующую структуру:
[
{
"id": 1,
"name": "Иван Петров",
"age": 30,
"email": "ivan@example.com"
},
{
"id": 2,
"name": "Мария Сидорова",
"age": 25,
"email": "maria@example.com"
},
{
"id": 3,
"name": "Алексей Козлов",
"age": 35,
"email": "alexey@example.com"
}
]
2. Решение на Go
package main
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
)
// Person представляет структуру данных человека
type Person struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
// Color возвращает цвет в зависимости от возраста
func (p Person) Color() string {
switch {
case p.Age < 25:
return "зелёный"
case p.Age < 35:
return "жёлтый"
default:
return "красный"
}
}
func main() {
// Чтение файла
data, err := os.ReadFile("persons.json")
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка чтения файла: %v\n", err)
os.Exit(1)
}
// Десериализация JSON
var persons []Person
if err := json.Unmarshal(data, &persons); err != nil {
fmt.Fprintf(os.Stderr, "Ошибка парсинга JSON: %v\n", err)
os.Exit(1)
}
// Вывод таблицы
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
fmt.Fprintln(w, "ID\tName\tAge\tEmail\tColor")
fmt.Fprintln(w, "--\t----\t---\t-----\t-----")
for _, p := range persons {
fmt.Fprintf(w, "%d\t%s\t%d\t%s\t%s\n", p.ID, p.Name, p.Age, p.Email, p.Color())
}
w.Flush()
}
3. Альтернативный вариант с цветным выводом в терминал
Если требуется именно цветной вывод (ANSI-коды):
package main
import (
"encoding/json"
"fmt"
"os"
)
type Person struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
// Цвета ANSI
const (
ColorReset = "\033[0m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorRed = "\033[31m"
)
func ageColor(age int) string {
switch {
case age < 25:
return ColorGreen
case age < 35:
return ColorYellow
default:
return ColorRed
}
}
func main() {
data, err := os.ReadFile("persons.json")
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка чтения файла: %v\n", err)
os.Exit(1)
}
var persons []Person
if err := json.Unmarshal(data, &persons); err != nil {
fmt.Fprintf(os.Stderr, "Ошибка парсинга JSON: %v\n", err)
os.Exit(1)
}
fmt.Printf("%-4s %-20s %-6s %-25s %s\n", "ID", "Name", "Age", "Email", "Color")
fmt.Println("---- -------------------- ------ ------------------------- ------")
for _, p := range persons {
color := ageColor(p.Age)
fmt.Printf("%s%-4d %-20s %-6d %-25s %s%s\n",
color, p.ID, p.Name, p.Age, p.Email, ageColor(p.Age), ColorReset)
}
}
4. Ключевые моменты при решении задачи
- Обработка ошибок: всегда проверяйте ошибки при чтении файла и парсинге JSON. В Go это делается явно через
if err != nil. - Структуры данных: используйте структуры с тегами
jsonдля корректного маппинга полей. - Форматирование вывода: пакет
text/tabwriterудобен для выровненных таблиц. Для цветного вывода используйте ANSI-коды. - Путь к файлу: если файл не найден, проверьте текущую рабочую директорию (
os.Getwd()) и используйте абсолютный путь при необходимости.
5. Типичные ошибки
- Неправильный путь к файлу — используйте
os.Getwd()для проверки текущей директории. - Несоответствие структуры JSON и Go-структуры — проверьте теги
json. - Забыли проверить ошибку — в Go это приведёт к панике или некорректному поведению.
Вопрос 5. Для чего используются ключи (key) в React при рендеринге списков и что будет, если их не указывать?
Таймкод: 00:12:51
Ответ собеседника: Неполный. Ключи используются для сравнения объектов виртуального DOM между предыдущим и новым значением, чтобы оптимизировать обновление компонентов. Без ключей возможны проблемы с производительностью. Кандидат не смог детально объяснить механизм: при отсутствии ключей React перерисовывает все элементы списка, а с ключами — только изменившиеся, добавленные или удалённые.
Правильный ответ:
Ответ собеседника в целом верный, но требует углубления в механизм работы React и последствия отсутствия ключей. Вот полное объяснение:
1. Что такое key и зачем он нужен
Ключ (key) — это специальный атрибут, который React использует для идентификации элементов списка. Когда React рендерит список, он создаёт виртуальный DOM — дерево элементов. При следующем рендере React сравнивает новое дерево с предыдущим (процесс reconciliation) и определяет, какие элементы нужно обновить.
Ключ позволяет React понять:
- Какой элемент был добавлен
- Какой элемент был удалён
- Какой элемент изменился
- Какой элемент остался прежним
2. Что происходит без ключей
Если ключи не указаны, React использует индекс элемента в массиве как ключ по умолчанию. Это приводит к проблемам:
- Неправильное обновление состояния: если элемент добавляется в начало списка, React считает, что все элементы изменились, потому что их индексы сдвинулись.
- Потеря фокуса и ввода: если в списке есть интерактивные элементы (input, checkbox), при перестановке элементов фокус может перескочить на другой элемент.
- Проблемы с анимациями: анимации могут работать некорректно, потому что React не может отследить перемещение элементов.
Пример проблемы:
// Плохо: использование индекса как ключа
const UserList = ({ users }) => (
<ul>
{users.map((user, index) => (
<li key={index}>{user.name} - <input type="text" /></li>
))}
</ul>
);
// Если добавить пользователя в начало списка:
// Было: [Alice, Bob, Carol] с индексами [0, 1, 2]
// Стало: [Dave, Alice, Bob, Carol] с индексами [0, 1, 2, 3]
// React подумает, что Alice стала Bob, Bob стал Carol, Carol — новый элемент
// Инпуты переместятся неправильно
3. Правильное использование ключей
Ключ должен быть:
- Уникальным среди элементов одного списка
- Стабильным между рендерами (не меняться при каждом рендере)
- Предсказуемым (не генерировать случайные значения)
// Хорошо: использование уникального идентификатора
const UserList = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - <input type="text" /></li>
))}
</ul>
);
4. Механизм reconciliation
React использует алгоритм diff для сравнения деревьев. Ключ — это основной критерий для сопоставления узлов:
- Если ключи совпадают — React обновляет существующий элемент
- Если ключ появился — React создаёт новый элемент
- Если ключ исчез — React удаляет элемент
5. Аналогия в Go
В Go нет виртуального DOM, но аналогия — это работа с коллекциями. Когда вы итерируете по слайсу и изменяете его, важно отслеживать элементы по уникальному идентификатору, а не по индексу:
type User struct {
ID int
Name string
}
// Правильное обновление по ID
func updateUser(users []User, id int, newName string) []User {
for i, u := range users {
if u.ID == id {
users[i].Name = newName
break
}
}
return users
}
Пример усиленного ответа:
«Ключи в React — это механизм для идентификации элементов списка при рендеринге. React использует их в процессе reconciliation — сравнения виртуального DOM между рендерами. Без ключей React использует индекс элемента, что приводит к проблемам: неправильное обновление состояния при добавлении/удалении элементов, потеря фокуса в интерактивных элементах, некорректная работа анимаций. Ключ должен быть уникальным и стабильным — обычно используется идентификатор из данных. Никогда не стоит использовать случайные значения или индекс массива как ключ, если список может изменяться.»
Вопрос 6. Как сделать отображение цвета в виде прямоугольника только для определённого пола, используя условный рендеринг? Как сделать компонент переиспользуемым с кастомным поведением извне (render props)?
Таймкод: 00:16:48
Ответ собеседника: Неполный. Кандидат предложил добавить div с background-color и использовать условный рендеринг. Для переиспользуемости предположил передачу флага через пропсы, но не знал паттерна render props. После подсказки начал реализовывать, но не завершил. Не смог объяснить преимущества render props перед передачей готового значения.
Правильный ответ:
Задача проверяет понимание условного рендеринга и паттернов переиспользования компонентов в React. Вот полное решение:
1. Условный рендеринг для определённого пола
Простейший способ — использовать условный оператор внутри JSX:
const PersonCard = ({ person }) => {
return (
<div className="person-card">
<span>{person.name}</span>
{person.gender === 'male' && (
<div
style={{
width: 20,
height: 20,
backgroundColor: 'blue'
}}
/>
)}
</div>
);
};
Или с использованием тернарного оператора:
const ColorRectangle = ({ gender }) => {
const color = gender === 'male' ? 'blue' :
gender === 'female' ? 'pink' : 'gray';
return (
<div
style={{
width: 20,
height: 20,
backgroundColor: color
}}
/>
);
};
const PersonCard = ({ person }) => (
<div>
<span>{person.name}</span>
{person.gender && <ColorRectangle gender={person.gender} />}
</div>
);
2. Render Props — паттерн для переиспользования логики
Render props — это паттерн, при котором компонент принимает функцию как проп и вызывает её для рендеринга. Это позволяет отделить логику от представления.
Пример: компонент с render props
// Компонент с render props
const PersonRenderer = ({ person, renderColor }) => {
return (
<div className="person-card">
<span>{person.name}</span>
<span>{person.age}</span>
{renderColor && renderColor(person)}
</div>
);
};
// Использование: кастомное поведение извне
const App = () => {
const people = [
{ id: 1, name: 'Иван', age: 30, gender: 'male' },
{ id: 2, name: 'Мария', age: 25, gender: 'female' },
{ id: 3, name: 'Алексей', age: 35, gender: 'male' },
];
return (
<div>
{people.map(person => (
<PersonRenderer
key={person.id}
person={person}
renderColor={(p) =>
p.gender === 'male' ? (
<div style={{ width: 20, height: 20, backgroundColor: 'blue' }} />
) : (
<div style={{ width: 20, height: 20, backgroundColor: 'pink' }} />
)
}
/>
))}
</div>
);
};
3. Преимущества render props
- Гибкость: компонент не знает, как именно будет отображаться цвет. Это решает потребитель.
- Переиспользование логики: один и тот же компонент может рендериться по-разному в разных местах приложения.
- Разделение ответственности: компонент отвечает за данные, потребитель — за отображение.
Сравнение подходов:
// Плохо: жёстко зашитое поведение
const PersonCard = ({ person }) => (
<div>
{person.gender === 'male' && <BlueRectangle />}
</div>
);
// Лучше: через пропс
const PersonCard = ({ person, showColor }) => (
<div>
{showColor && <ColorRectangle gender={person.gender} />}
</div>
);
// Лучше всего: render props
const PersonCard = ({ person, renderColor }) => (
<div>
{renderColor ? renderColor(person) : null}
</div>
);
4. Аналогия в Go
В Go аналог render props — это передача функции как аргумента (callback, strategy pattern):
package main
import "fmt"
type Person struct {
Name string
Age int
Gender string
}
// RenderFunc — функция для рендеринга цвета
type RenderFunc func(Person) string
// PersonRenderer — аналог компонента с render props
func PersonRenderer(person person, renderColor RenderFunc) string {
result := fmt.Sprintf("Name: %s, Age: %d", person.Name, person.Age)
if renderColor != nil {
result += ", Color: " + renderColor(person)
}
return result
}
func main() {
people := []Person{
{Name: "Иван", Age: 30, Gender: "male"},
{Name: "Мария", Age: 25, Gender: "female"},
}
// Кастомное поведение извне
colorRenderer := func(p Person) string {
if p.Gender == "male" {
return "blue"
}
return "pink"
}
for _, p := range people {
fmt.Println(PersonRenderer(p, colorRenderer))
}
}
5. Когда использовать render props
- Когда компонент должен быть гибким и переиспользуемым
- Когда логика рендеринга зависит от контекста использования
- Когда нужно разделить логику получения данных и их отображение
В современном React многие случаи render props заменяются хуками (hooks), но паттерн всё ещё актуален, особенно для классовых компонентов или когда нужна инверсия контроля.
Вопрос 7. Как сделать компонент переиспользуемым, если в разных частях приложения нужно разное условие отображения цвета — чтобы можно было задавать кастомное поведение извне? Что такое render props?
Таймкод: 00:18:48
Ответ собеседника: Неполный. Кандидат предположил, что можно передать флаг через пропсы, но не знал паттерна render props. После подсказки начал реализовывать — передавать функцию как проп, которая возвращает JSX с кастомным условием. Реализация была начата, но кандидат не смог самостоятельно предложить этот паттерн.
Правильный ответ:
Этот вопрос дублирует предыдущий, поэтому кратко повторю ключевые моменты и добавлю дополнительные детали:
Render props — это паттерн, при котором компонент принимает функцию через пропсы и вызывает её для рендеринга части UI. Это позволяет передать логику отображения извне.
Пример реализации:
// Компонент с render props
const PersonCard = ({ person, renderColor }) => {
return (
<div className="card">
<h3>{person.name}</h3>
<p>Возраст: {person.age}</p>
{renderColor && renderColor(person)}
</div>
);
};
// Использование 1: цвет по полу
<PersonCard
person={person}
renderColor={(p) => p.gender === 'male'
? <div style={{ width: 20, height: 20, background: 'blue' }} />
: null
}
/>
// Использование 2: цвет по возрасту
<PersonCard
person={person}
renderColor={(p) => p.age > 30
? <div style={{ width: 20, height: 20, background: 'red' }} />
: null
}
/>
Преимущества render props:
- Инверсия контроля: компонент не решает, что рендерить — это делает потребитель
- Переиспользование: один компонент, разное поведение в разных контекстах
- Тестируемость: легко тестировать, подменяя render-функцию
Аналогия в Go — паттерн Strategy:
type Person struct {
Name string
Age int
Gender string
}
type ColorStrategy func(Person) string
func PersonCard(person person, colorStrategy ColorStrategy) string {
result := fmt.Sprintf("%s, %d лет", person.Name, person.Age)
if color := colorStrategy(person); color != "" {
result += fmt.Sprintf(", цвет: %s", color)
}
return result
}
// Разные стратегии
func ByGender(p Person) string {
if p.Gender == "male" {
return "синий"
}
return ""
}
func ByAge(p Person) string {
if p.Age > 30 {
return "красный"
}
return ""
}
В современном React многие случаи render props заменяются хуками, но паттерн остаётся полезным для понимания принципов композиции и инверсии контроля.
Вопрос 8. В чём разница между передачей в компонент готового JSX-значения и передачей функции (render props), которая возвращает JSX? В чём преимущество render props?
Таймкод: 00:22:38
Ответ собеседника: Неправильный. Кандидат не смог объяснить разницу и преимущества render props перед передачей готового значения. Признал, что у него нет идей на этот счёт.
Правильный ответ:
Это важный вопрос, который проверяет понимание разницы между данными и поведением, между статическим и динамическим контентом. Вот детальное объяснение:
1. Передача готового JSX — статический подход
Когда вы передаёте готовый JSX как проп, вы передаёте уже вычисленный, статический результат:
// Родительский компонент
const Parent = () => {
const colorBox = <div style={{ width: 20, height: 20, background: 'blue' }} />;
return <PersonCard person={person} colorBox={colorBox} />;
};
// Дочерний компонент
const PersonCard = ({ person, colorBox }) => {
return (
<div>
<h3>{person.name}</h3>
{colorBox}
</div>
);
};
Проблема: JSX вычисляется в родителе и не имеет доступа к состоянию дочернего компонента.
2. Render props — динамический подход
Когда вы передаёте функцию, вы передаёте поведение, которое будет вычислено позже — в контексте дочернего компонента:
// Родительский компонент
const Parent = () => {
return (
<PersonCard
person={person}
renderColor={(p) => p.gender === 'male'
? <div style={{ width: 20, height: 20, background: 'blue' }} />
: null
}
/>
);
};
3. Ключевые различия
| Аспект | Готовый JSX | Render Props |
|---|---|---|
| Момент вычисления | В родителе | В дочернем компоненте |
| Доступ к состоянию | Только родителя | Родителя и дочернего |
| Гибкость | Статический | Динамический |
| Переиспользование | Ограниченное | Высокое |
4. Практический пример: доступ к данным дочернего компонента
Представим, что дочерний компонент имеет внутреннее состояние (например, выбранный фильтр):
// Компонент с render props
const FilteredList = ({ items, renderItem }) => {
const [filter, setFilter] = useState('all');
const filteredItems = filter === 'all'
? items
: items.filter(item => item.category === filter);
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">Все</option>
<option value="active">Активные</option>
<option value="inactive">Неактивные</option>
</select>
{/* renderItem имеет доступ к отфильтрованным данным */}
{filteredItems.map(item => renderItem(item, filter))}
</div>
);
};
// Использование
<FilteredList
items={users}
renderItem={(item, currentFilter) => (
<div key={item.id}>
{item.name} — {item.status}
{currentFilter === 'active' && <span>✓</span>}
</div>
)}
/>
Если бы мы передавали готовый JSX, он не имел бы доступа к currentFilter — внутреннему состоянию FilteredList.
5. Преимущества render props
- Инверсия контроля: дочерний компонент предоставляет данные, родитель решает, как их отобразить
- Доступ к контексту: функция вызывается внутри дочернего компонента и имеет доступ к его состоянию
- Отложенное вычисление: JSX создаётся только когда нужно (lazy evaluation)
- Композируемость: можно комбинировать несколько render props
6. Аналогия в Go
В Go это разница между передачей значения и передачей функции (callback, strategy pattern):
// Передача готового значения (статическое)
func RenderPerson(name string, color string) string {
return fmt.Sprintf("%s: %s", name, color)
}
// Передача функции (динамическое)
func RenderPersonWithStrategy(name string, colorFunc func(string) string) string {
color := colorFunc(name) // Вычисляется внутри, имеет доступ к контексту
return fmt.Sprintf("%s: %s", name, color)
}
// Использование
colorFunc := func(name string) string {
if strings.HasPrefix(name, "А") {
return "красный"
}
return "синий"
}
result := RenderPersonWithStrategy("Анна", colorFunc)
7. Когда что использовать
- Готовый JSX: когда контент полностью определяется родителем и не зависит от состояния дочернего компонента
- Render props: когда контент зависит от внутреннего состояния дочернего компонента или нужна гибкость в отображении
В современном React многие случаи render props заменяются хуками (hooks), но понимание этого паттерна важно для работы с классовыми компонентами и для понимания принципов композиции.
Вопрос 9. Сделать счётчик кликов, который считает только клики внутри белой области (блока с классом box). Как слушать клики на всём документе и нужно ли чистить обработчик при размонтировании?
Таймкод: 00:25:40
Ответ собеседника: Неполный. Кандидат предложил повесить обработчик на обёртку и проверять event.target. Задача не была полностью завершена из-за технических проблем. Верно понял необходимость очистки обработчика на window через useEffect с cleanup-функцией при размонтировании компонента.
Правильный ответ:
Задача проверяет понимание работы с событиями в DOM, делегирования событий и управления жизненным циклом компонентов. Вот полное решение:
1. Решение на React с использованием useEffect
import React, { useState, useEffect, useRef } from 'react';
const ClickCounter = () => {
const [count, setCount] = useState(0);
const boxRef = useRef(null);
useEffect(() => {
const handleClick = (event) => {
// Проверяем, был ли клик внутри блока с классом box
if (boxRef.current && boxRef.current.contains(event.target)) {
setCount(prev => prev + 1);
}
};
// Слушаем клики на всём документе
document.addEventListener('click', handleClick);
// Cleanup-функция: удаляем обработчик при размонтировании
return () => {
document.removeEventListener('click', handleClick);
};
}, []);
return (
<div>
<p>Кликов внутри блока: {count}</p>
<div
ref={boxRef}
className="box"
style={{
width: 200,
height: 200,
background: 'white',
border: '1px solid black'
}}
>
Кликай сюда
</div>
</div>
);
};
2. Альтернативный подход: проверка через closest
useEffect(() => {
const handleClick = (event) => {
// Проверяем, есть ли у элемента или его предков класс box
if (event.target.closest('.box')) {
setCount(prev => prev + 1);
}
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
3. Зачем слушать клики на всём документе?
Есть несколько причин слушать клики на document вместо конкретного элемента:
- Делегирование событий: обработчик один, но может обрабатывать клики на множество элементов
- Динамические элементы: если элементы добавляются/удаляются динамически, не нужно вешать обработчики заново
- Клики вне элемента: часто нужно отслеживать клики вне определённой области (например, закрытие модального окна)
4. Зачем чистить обработчик?
Если не удалить обработчик при размонтировании компонента:
- Утечка памяти: обработчик продолжает существовать в памяти, даже когда компонент удалён
- Ошибки: обработчик может пытаться обновить состояние несуществующего компонента
- Накладные расходы: лишние обработчики замедляют работу приложения
5. Аналогия в Go
В Go аналогия — это работа с каналами и горутинами. Если вы запускаете горутину, которая слушает канал, вы должны её корректно остановить:
package main
import (
"fmt"
"sync"
"time"
)
type ClickCounter struct {
count int
clicks chan struct{}
done chan struct{}
mu sync.Mutex
}
func NewClickCounter() *ClickCounter {
c := &ClickCounter{
clicks: make(chan struct{}, 100),
done: make(chan struct{}),
}
go c.process()
return c
}
func (c *ClickCounter) process() {
for {
select {
case <-c.clicks:
c.mu.Lock()
c.count++
fmt.Printf("Кликов: %d\n", c.count)
c.mu.Unlock()
case <-c.done:
fmt.Println("Остановка обработчика")
return
}
}
}
func (c *ClickCounter) Click() {
c.clicks <- struct{}{}
}
func (c *ClickCounter) Stop() {
close(c.done) // Аналог cleanup-функции в React
}
func main() {
counter := NewClickCounter()
// Симулируем клики
for i := 0; i < 5; i++ {
counter.Click()
time.Sleep(100 * time.Millisecond)
}
counter.Stop() // Важно: останавливаем горутину
time.Sleep(200 * time.Millisecond)
}
6. Ключевые моменты
- Cleanup в useEffect: всегда возвращайте функцию очистки, если добавляете глобальные обработчики
- Проверка цели клика: используйте
ref.current.contains(event.target)илиevent.target.closest('.class') - Зависимости useEffect: если обработчик не зависит от пропсов/состояния, используйте пустой массив зависимостей
[] - Производительность: один обработчик на document эффективнее, чем множество обработчиков на отдельных элементах
Вопрос 10. Как переписать обработчик клика так, чтобы слушать клики на всём документе (через window или document), а не только на конкретном элементе? Что произойдёт при удалении компонента — нужно ли чистить обработчик?
Таймкод: 00:31:55
Ответ собеседника: Правильный. Кандидат предложил повесить обработчик на window через useEffect. Верно понял, что при удалении компонента обработчик на window останется, что может привести к утечкам памяти. Предложил использовать useEffect с функцией очистки (cleanup) для удаления обработчика при размонтировании компонента.
Правильный ответ:
Ответ собеседника корректный. Это вопрос на закрепление предыдущей темы, поэтому кратко подведу итог:
1. Решение
useEffect(() => {
const handleClick = (event) => {
// Обработка клика
};
// Вешаем обработчик на document или window
document.addEventListener('click', handleClick);
// Cleanup-функция — обязательна!
return () => {
document.removeEventListener('click', handleClick);
};
}, []);
2. Почему необходим cleanup
- Утечка памяти: обработчик продолжает существовать после удаления компонента
- Ошибки: обработчик может вызвать setState на несуществующем компоненте
- Производительность: накопление мёртвых обработчиков замедляет приложение
3. Аналогия в Go
В Go это аналог закрытия каналов и остановки горутин — если не остановить, будет утечка ресурсов:
func worker(done chan struct{}) {
for {
select {
case <-done:
return // Корректная остановка
default:
// Работа
}
}
}
// Использование
done := make(chan struct{})
go worker(done)
close(done) // Аналог cleanup в React
Собеседник продемонстрировал правильное понимание управления жизненным циклом компонентов и важности очистки ресурсов.
Вопрос 11. Что произойдёт при удалении компонента, если обработчик события был повешен на window/document? Нужно ли что-то чистить при размонтировании компонента?
Таймкод: 00:33:09
Ответ собеседника: Правильный. Кандидат верно понял, что при удалении компонента обработчик на window останется, что может привести к утечкам памяти. Предложил использовать useEffect с функцией очистки (cleanup) для удаления обработчика при размонтировании компонента.
Правильный ответ:
Ответ собеседника полностью корректный. Это повторение предыдущего вопроса для закрепления материала. Краткое резюме:
1. Последствия отсутствия cleanup
- Обработчик события продолжает существовать в памяти
- При клике вызывается функция, которая пытается обновить состояние несуществующего компонента
- Со временем накапливаются мёртвые обработчики, что замедляет приложение
2. Правильный паттерн
useEffect(() => {
const handler = (e) => { /* ... */ };
document.addEventListener('click', handler);
// Cleanup — обязательно при работе с глобальными обработчиками
return () => document.removeEventListener('click', handler);
}, []);
Собеседник стабильно демонстрирует понимание этого важного аспекта работы с побочными эффектами в React.
Вопрос 12. Работал ли с порталами в React? Соотносится ли виртуальный DOM с реальным DOM один к одному?
Таймкод: 00:34:43
Ответ собеседника: Неправильный. Кандидат подтвердил использование порталов для модальных окон. Однако неверно предположил, что виртуальный DOM и реальный DOM всегда соотносятся один к одному. Не учёл, что портал в виртуальном DOM находится внутри родительского компонента, а в реальном DOM рендерится в другой элемент.
Правильный ответ:
Вопрос проверяет понимание работы виртуального DOM и механизма порталов — важной концепции в React.
1. Порталы в React
Порталы позволяют рендерить дочерние элементы в DOM-узел, который находится вне иерархии DOM родительского компонента.
import ReactDOM from 'react-dom';
const Modal = ({ children, isOpen }) => {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.getElementById('modal-root') // Рендерим в другой элемент
);
};
// Использование
const App = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Открыть модалку</button>
<Modal isOpen={isOpen}>
<h2>Модальное окно</h2>
<button onClick={() => setIsOpen(false)}>Закрыть</button>
</Modal>
</div>
);
};
// index.html
// <div id="root"></div>
// <div id="modal-root"></div>
2. Виртуальный DOM vs Реальный DOM
Виртуальный DOM и реальный DOM не всегда соотносятся один к одному. Портал — наглядный пример:
Виртуальный DOM (React tree):
<App>
<div>
<button>Открыть модалку</button>
<Modal>
<div className="modal-overlay">
<div className="modal-content">
<h2>Модальное окно</h2>
<button>Закрыть</button>
</div>
</div>
</Modal>
</div>
</App>
Реальный DOM (HTML):
<body>
<div id="root">
<div>
<button>Открыть модалку</button>
</div>
</div>
<div id="modal-root">
<div class="modal-overlay">
<div class="modal-content">
<h2>Модальное окно</h2>
<button>Закрыть</button>
</div>
</div>
</div>
</body>
3. Зачем нужны порталы
- Всплывающие элементы: модальные окна, тултипы, дропдауны — часто нужно рендерить их в
<body>, чтобы избежать проблем сoverflow: hiddenилиz-indexродительских элементов. - Семантика: модальное окно логически является дочерним элементом компонента, но визуально должно быть в корне документа.
- События: несмотря на то что портал рендерится в другом месте DOM, события всплывают по React-дереву, а не по DOM-дереву.
4. Другие примеры несоответствия виртуального и реального DOM
- Фрагменты (
<>...</>): в виртуальном DOM есть узел-фрагмент, в реальном DOM его нет - Условный рендеринг:
{condition && <Component />}— узел может появляться и исчезать - Списки: React использует ключи для оптимизации, что может приводить к переиспользованию DOM-узлов
5. Аналогия в Go
В Go аналогия — это отделение логической структуры данных от физического представления:
// Логическая структура (аналог виртуального DOM)
type Component struct {
Name string
Children []Component
}
// Физическое представление (аналог реального DOM)
// Может отличаться от логической структуры
func RenderToDifferentTarget(comp Component, target string) string {
// Рендерим компонент в другую "цель"
return fmt.Sprintf("Rendering %s to %s", comp.Name, target)
}
Вывод:
Виртуальный DOM — это логика компонентов и их взаимосвязей. Реальный DOM — это физическое представление в браузере. Порталы наглядно демонстрируют, что эти два дерева могут иметь разную структуру. Это важно понимать для отладки и оптимизации React-приложений.
Вопрос 13. Соотносится ли виртуальный DOM с реальным DOM один к одному? Всегда ли структура виртуального DOM точно повторяет структуру реального DOM?
Таймкод: 00:35:24
Ответ собеседника: Неправильный. Кандидат предположил, что виртуальный DOM и реальный DOM соотносятся один к одному, но не смог объяснить, почему это может быть не так. После подсказки про порталы вспомнил, что портал в виртуальном DOM находится внутри родительского компонента, а в реальном DOM рендерится в совершенно другой элемент.
Правильный ответ:
Это повторение предыдущего вопроса для закрепления. Ответ уже был подробно раскрыт в вопросе 12. Краткое резюме:
Виртуальный DOM и реальный DOM не всегда соотносятся один к одному.
Основные причины:
1. Порталы — компонент логически находится в одном месте дерева React, но рендерится в другой DOM-узел:
// В виртуальном DOM: Modal внутри App
// В реальном DOM: Modal рендерится в #modal-root, а не внутри #root
<Modal isOpen={true}>
<h2>Содержимое модалки</h2>
</Modal>
2. Фрагменты — в виртуальном DOM есть узел-фрагмент, в реальном DOM его нет:
// Виртуальный DOM: Fragment как отдельный узел
// Реальный DOM: элементы идут сразу в родителя
<>
<li>Пункт 1</li>
<li>Пункт 2</li>
</>
3. Условный рендеринг — узлы могут появляться и исчезать:
// Виртуальный DOM: узел есть или нет в зависимости от условия
// Реальный DOM: элемент добавляется или удаляется
{isVisible && <div>Видимый блок</div>}
4. Скрытые элементы — display: none или visibility: hidden не влияют на виртуальный DOM, но элементы остаются в реальном DOM.
Ключевой вывод:
Виртуальный DOM — это представление о компонентах и их логических связях. Реальный DOM — это физическое дерево элементов в браузере. Порталы — яркий пример того, что эти два дерева могут иметь разную структуру, и это нормально.
Вопрос 14. Как работают фазы событий в DOM? Что такое SyntheticEvent в React?
Таймкод: 00:39:21
Ответ собеседника: Неполный. Кандидат верно описал фазы событий (погружение, целевая фаза, всплытие) и ответил, что для отслеживания всех кликов нужно слушать на фазе погружения. Знает про SyntheticEvent как обёртку над нативными событиями, но не смог назвать конкретные преимущества (кросс-браузерная совместимость, нормализация).
Правильный ответ:
Вопрос проверяет глубокое понимание механизма событий в DOM и абстракций React. Вот полное объяснение:
1. Три фазы событий в DOM
Когда происходит событие (например, клик), оно проходит через три фазы:
Фаза 1: Захват (Capturing / Capture phase)
- Событие идёт сверху вниз:
document→html→body→ ... →target - Обработчики с
useCapture: trueилиaddEventListener(type, handler, true)срабатывают на этой фазе - По умолчанию большинство обработчиков НЕ используют эту фазу
Фаза 2: Целевая фаза (Target phase)
- Событие достигает целевого элемента
- Все обработчики на целевом элементе срабатывают (независимо от
useCapture)
Фаза 3: Всплытие (Bubbling / Bubble phase)
- Событие идёт снизу вверх:
target→ ... →body→html→document - По умолчанию большинство обработчиков срабатывают на этой фазе
Визуализация:
Захват (вниз): document → html → body → div → button
Целевая фаза: [button]
Всплытие (вверх): document ← html ← body ← div ← button
2. Пример работы фаз
<div id="outer">
<button id="inner">Кликни</button>
</div>
<script>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
// Захват (третий аргумент true)
outer.addEventListener('click', () => console.log('outer capture'), true);
// Всплытие (по умолчанию)
outer.addEventListener('click', () => console.log('outer bubble'));
// Целевая фаза
inner.addEventListener('click', () => console.log('inner target'));
// Порядок вывода при клике на button:
// 1. outer capture (фаза захвата)
// 2. inner target (целевая фаза)
// 3. outer bubble (фаза всплытия)
</script>
3. SyntheticEvent в React
SyntheticEvent — это кросс-браузерная обёртка над нативными событиями браузера. React не использует нативные события напрямую.
Преимущества SyntheticEvent:
- Кросс-браузерная совместимость: нормализует различия между браузерами (IE, Firefox, Safari, Chrome)
- Единый интерфейс:
event.target,event.currentTarget,event.preventDefault()работают одинаково везде - Пуллинг объектов: для производительности React переиспользует объекты SyntheticEvent (в React 16 и ранее)
- Делегирование: React вешает один обработчик на
document(илиrootв React 17+), а не на каждый элемент
4. Делегирование событий в React
React использует паттерн делегирования:
// React НЕ делает так:
button.addEventListener('click', handler);
// React делает так:
document.addEventListener('click', (nativeEvent) => {
// Определяет, на каком элементе произошло событие
const target = nativeEvent.target;
// Находит соответствующий React-компонент
// Создаёт SyntheticEvent
// Вызывает обработчик компонента
});
Именно поэтому для отслеживания всех кликов нужно слушать на фазе захвата — чтобы перехватить событие до того, как React его обработает.
5. Аналогия в Go
В Go аналогия — это middleware или interceptor:
// Нативное событие (аналог нативного DOM-события)
type NativeEvent struct {
Type string
Target string
Details map[string]interface{}
}
// SyntheticEvent — нормализованная обёртка
type SyntheticEvent struct {
Type string
Target string
CurrentTarget string
PreventDefault func()
StopPropagation func{}
}
// Middleware для обработки событий (аналог React event delegation)
func EventMiddleware(handler func(SyntheticEvent)) func(NativeEvent) {
return func(native NativeEvent) {
// Нормализация события
synthetic := SyntheticEvent{
Type: native.Type,
Target: native.Target,
PreventDefault: func() {
// Кросс-браузерная реализация
},
}
handler(synthetic)
}
}
6. Ключевые моменты
- Фаза захвата — событие идёт сверху вниз, используется редко
- Целевая фаза — событие на целевом элементе
- Фаза всплытия — событие идёт снизу вверх, используется по умолчанию
- SyntheticEvent — нормализованная обёртка для кросс-браузерной совместимости
- Делегирование — React вешает один обработчик на корневой элемент
Вопрос 15. Что такое SyntheticEvent в React? Зачас нужны обёртки над нативными событиями?
Таймкод: 00:41:39
Ответ собеседника: Неполный. Кандидат знает, что в React есть SyntheticEvent — обёртки над нативными событиями, которые предоставляют дополнительные возможности. Однако не смог конкретно назвать, какие именно дополнительные возможности предоставляет SyntheticEvent (кросс-браузерная совместимость, нормализация свойств, пулинг событий).
Правильный ответ:
Это повторение предыдущего вопроса для закрепления. Ответ уже был подробно раскрыт в вопросе 14. Краткое резюме:
SyntheticEvent — это кросс-браузерная обёртка над нативными DOM-событиями, которую React использует вместо прямой работы с нативными событиями.
Основные преимущества:
1. Кросс-браузерная совместимость
// Без SyntheticEvent (нативный код):
const handleClick = (e) => {
// В разных браузерах свойства могут отличаться
const target = e.target || e.srcElement; // IE
e.preventDefault(); // Работает везде, но способ вызова может отличаться
};
// С SyntheticEvent (React):
const handleClick = (e) => {
// Работает одинаково во всех браузерах
const target = e.target;
e.preventDefault();
};
2. Нормализация свойств
const handleClick = (e) => {
// Свойства одинаковы во всех браузерах
e.target // Элемент, на котором произошло событие
e.currentTarget // Элемент, на котором висит обработчик
e.type // Тип события
e.preventDefault()
e.stopPropagation()
e.nativeEvent // Доступ к оригинальному нативному событию
};
3. Пулинг объектов (React 16 и ранее)
Для производительности React переиспользует объекты SyntheticEvent:
// React 16 и ранее:
const handleClick = (e) => {
// НЕ правильно — объект будет переиспользован
setTimeout(() => {
console.log(e.target); // Может быть null
}, 0);
// Правильно — сохраняем нужное свойство
const target = e.target;
setTimeout(() => {
console.log(target); // Работает
}, 0);
// Или используем e.persist() (React 16)
e.persist();
};
4. Делегирование событий
React вешает один обработчик на корневой элемент, а не на каждый DOM-узел:
// React НЕ делает так (неэффективно):
button.addEventListener('click', handler);
input.addEventListener('change', handler);
div.addEventListener('mouseenter', handler);
// React делает так (эффективно):
document.addEventListener('click', delegateHandler);
document.addEventListener('change', delegateHandler);
document.addEventListener('mouseenter', delegateHandler);
Зачем это нужно:
- Производительность: меньше обработчиков в памяти
- Удобство: не нужно вручную добавлять/удалять обработчики при изменении DOM
- Совместимость: код работает одинаково во всех браузерах
Доступ к нативному событию:
const handleClick = (e) => {
console.log(e); // SyntheticEvent
console.log(e.nativeEvent); // Нативное событие браузера
};
Вопрос 16. Реализовать автофокус на input при монтировании. Чем отличается useRef от useState? Зачем массив зависимостей в useEffect?
Таймкод: 00:42:37
Ответ собеседника: Правильный. Кандидат успешно реализовал автофокус через useRef и useEffect. Верно объяснил разницу между useRef и useState: изменение ref не вызывает ре-рендер. Правильно описал назначение массива зависимостей в useEffect: пустой массив — только при первом рендере, без массива — при каждом рендере.
Правильный ответ:
Ответ собеседника полностью корректный. Подробное объяснение для закрепления материала:
1. Реализация автофокуса
import React, { useRef, useEffect } from 'react';
const AutoFocusInput = () => {
const inputRef = useRef(null);
useEffect(() => {
// Фокус при монтировании
inputRef.current.focus();
}, []); // Пустой массив — выполнится только при монтировании
return <input ref={inputRef} placeholder="Автофокус" />;
};
2. Различия между useRef и useState
| Аспект | useState | useRef |
|---|---|---|
| Хранение | Состояние компонента | Мутабельная ссылка |
| Ре-рендер | Вызывает при изменении | Не вызывает при изменении |
| Доступ | Через переменную состояния | Через .current |
| Инициализация | Начальное значение состояния | Начальное значение .current |
| Когда использовать | UI зависит от значения | Нужно сохранить значение без ре-рендера |
Пример:
const Example = () => {
const [count, setCount] = useState(0); // Ре-рендер при изменении
const renderCount = useRef(0); // НЕ вызывает ре-рендер
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
<p>Count: {count}</p>
<p>Рендеров: {renderCount.current}</p>
<button onClick={() => setCount(c => c + 1)}>
Увеличить
</button>
</div>
);
};
3. Массив зависимостей в useEffect
// Выполняется при каждом рендере
useEffect(() => {
console.log('Каждый рендер');
});
// Выполняется только при монтировании
useEffect(() => {
console.log('Только монтирование');
}, []);
// Выполняется при монтировании и при изменении зависимостей
useEffect(() => {
console.log(`Изменился userId: ${userId}`);
}, [userId]);
// Cleanup при размонтировании или изменении зависимостей
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => clearInterval(timer); // Cleanup
}, [userId]);
4. Аналогия в Go
В Go аналогия — это разница между изменением переменной и обновлением UI:
package main
import "fmt"
// useState — изменение вызывает "ре-рендер"
type State struct {
value int
onUpdate func(int)
}
func (s *State) Set(newValue int) {
s.value = newValue
s.onUpdate(newValue) // Уведомление об изменении
}
// useRef — изменение НЕ вызывает "ре-рендер"
type Ref struct {
Current int
}
func main() {
// useState аналог
state := State{
onUpdate: func(v int) {
fmt.Printf("State изменился: %d, обновляем UI\n", v)
},
}
state.Set(1) // Вызовет onUpdate
// useRef аналог
ref := Ref{Current: 0}
ref.Current = 1 // Ничего не происходит, просто изменение
fmt.Printf("Ref изменился: %d, UI не обновился\n", ref.Current)
}
Собеседник продемонстрировал хорошее понимание хуков React и их различий.
Вопрос 17. Для чего нужен второй аргумент (массив зависимостей) в useEffect? Что будет при пустом массиве и при его отсутствии?
Таймкод: 00:45:26
Ответ собеседника: Правильный. Кандидат верно объяснил: пустой массив зависимостей означает, что useEffect сработает только при первом рендере. Без массива зависимостей эффект перезапускается при каждом рендере. Массив зависимостей позволяет перезапускать эффект только при изменении указанных значений.
Правильный ответ:
Ответ собеседника полностью корректный. Это закрепление предыдущего вопроса. Краткое резюме:
Массив зависимостей в useEffect определяет, когда эффект должен перезапускаться:
1. Пустой массив [] — только при монтировании
useEffect(() => {
console.log('Компонент смонтирован');
fetchData();
}, []);
2. Без массива — при каждом рендере
useEffect(() => {
console.log('Любой ре-рендер');
// Выполняется после каждого рендера
});
3. С зависимостями [a, b] — при изменении зависимостей
useEffect(() => {
console.log(`userId или filter изменился: ${userId}, ${filter}`);
fetchUser(userId, filter);
}, [userId, filter]);
4. Важные нюансы
- React сравнивает зависимости по ссылке (
===) - Объекты и массивы создаются заново при каждом рендере — это может вызвать бесконечный цикл
- Для объектов используйте
useMemoдля стабилизации ссылки
// Проблема: объект создаётся заново при каждом рендере
useEffect(() => {
fetchData(options); // Бесконечный цикл!
}, [options]); // options — новый объект при каждом рендере
// Решение: useMemo
const options = useMemo(() => ({
pageSize: 10,
sortBy: 'name'
}), []);
useEffect(() => {
fetchData(options); // Работает корректно
}, [options]);
Собеседник стабильно демонстрирует понимание работы useEffect и массива зависимостей.
Вопрос 18. Чем useRef отличается от useState? Приведёт ли изменение ref не приводит к ре-рендеру компонента?
Таймкод: 00:50:21
Ответ собеседника: Правильный. Кандидат верно ответил, что useRef позволяет сохранять данные между рендерами, но в отличие от useState, изменение ref не приводит к ре-рендеру компонента.
Правильный ответ:
Ответ собеседника корректный. Это повторение ранее пройденного вопроса. Краткое резюме:
useRef vs useState:
| Характеристика | useRef | useState |
|---|---|---|
Изменение .current | Не вызывает ре-рендер | Вызывает ре-рендер |
| Доступ к DOM | Да, через ref prop | Нет |
| Сохранение между рендерами | Да | Да |
| Использование | DOM-узлы, таймеры, предыдущие значения | Состояние, от которого зависит UI |
Пример:
const Example = () => {
const [count, setCount] = useState(0); // Ре-рендер при изменении
const prevCount = useRef(0); // НЕ вызывает ре-рендер
useEffect(() => {
prevCount.current = count; // Сохраняем предыдущее значение
});
return (
<div>
<p>Текущее: {count}</p>
<p>Предыдущее: {prevCount.current}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
};
Собеседник демонстрирует стабильное понимание различий между хуками React.
Вопрос 19. Можно ли в React создать директиву (аналог директив в Vue), чтобы добавлять автофокус через атрибут без написания логики в каждом компоненте?
Таймкод: 00:51:43
Ответ собеседателя: Неправильный. Кандидат не знает, как в React реализовать пользовательские директивы. React не поддерживает директивы нативно, но можно использовать хуки, компоненты высшего порядка или render props для достижения похожего результата.
Правильный ответ:
Вопрос проверяет понимание различий между React и Vue, а также знание паттернов для переиспользования логики в React.
1. Директивы в Vue vs React
В Vue директивы — это специальные атрибуты, которые добавляют поведение элементам:
<!-- Vue: директива v-focus -->
<input v-focus />
<!-- Реализация директивы в Vue -->
Vue.directive('focus', {
inserted: function (el) {
el.focus();
}
});
React не имеет встроенной системы директив, но есть альтернативы.
2. Альтернативы директивам в React
А. Пользовательский хук (рекомендуемый способ)
// useAutoFocus.js
const useAutoFocus = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.focus();
}
}, []);
return ref;
};
// Использование
const MyComponent = () => {
const inputRef = useAutoFocus();
return <input ref={inputRef} />;
};
Б. Компонент-обёртка
// AutoFocus.jsx
const AutoFocus = ({ children }) => {
const childRef = useRef(null);
useEffect(() => {
if (childRef.current) {
childRef.current.focus();
}
}, []);
return React.cloneElement(children, { ref: childRef });
};
// Использование
<AutoFocus>
<input />
</AutoFocus>
В. Компонент высшего порядка (HOC)
// withAutoFocus.jsx
const withAutoFocus = (WrappedComponent) => {
return function WithAutoFocusComponent(props) {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.focus();
}
}, []);
return <WrappedComponent {...props} ref={ref} />;
};
};
// Использование
const AutoFocusInput = withAutoFocus(Input);
3. Можно ли создать аналог директивы?
Честный ответ: нет, в React нет и не будет директив в стиле Vue. Это фундаментальное архитектурное решение:
- Vue работает с шаблонами (template-based)
- React работает с JSX (JavaScript-based)
В JSX вы пишете JavaScript, а не HTML-атрибуты, поэтому директивы не нужны — вы можете напрямую использовать хуки и компоненты.
4. Если очень хочется синтаксис директив
Можно создать «псевдодирективу» через ref callback:
// Не рекомендуется, но возможно
const autoFocusRef = (el) => {
if (el) el.focus();
};
// Использование (выглядит как директива, но это ref callback)
<input ref={autoFocusRef} />
5. Аналогия в Go
В Go аналогия — это декораторы или middleware:
// Вместо директив — функции-обёртки
func WithAutoFocus(handler func(*Element)) func(*Element) {
return func(el *Element) {
el.Focus() // "директива" добавляет фокус
handler(el)
}
}
// Использование
inputHandler := WithAutoFocus(func(el *Element) {
// Основная логика
})
Вывод:
React не поддерживает директивы, и это осознанное решение. Вместо директив используются хуки — более мощный и гибкий механизм для переиспользования логики. Хук useAutoFocus — это идиоматичный React-способ решить задачу с автофокусом.
Вопрос 20. Реализовать сброс счётчика через подъём состояния к общему родителю. Как решить проблему пропс-дриллинга при большой вложенности?
Таймкод: 00:54:42
Ответ собеседника: Неполный. Кандидат успешно реализовал задачу через подъём состояния. Верно предложил Context API для решения пропс-дриллинга, но не смог назвать фундаментальных различий между Context и Redux (производительность, devtools, middleware).
Правильный ответ:
Вопрос проверяет понимание управления состоянием в React и знание паттернов для решения пропс-дриллинга.
1. Подъём состояния (Lifting State Up)
// Родительский компонент
const Parent = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);
const reset = () => setCount(0);
return (
<div>
<Display count={count} />
<IncrementButton onIncrement={increment} />
<ResetButton onReset={reset} />
</div>
);
};
// Дочерние компоненты
const Display = ({ count }) => <p>Счётчик: {count}</p>;
const IncrementButton = ({ onIncrement }) => (
<button onClick={onIncrement}>+1</button>
);
const ResetButton = ({ onReset }) => (
<button onClick={onReset}>Сбросить</button>
);
2. Проблема пропс-дриллинга
Когда компоненты глубоко вложены, передача пропсов через каждый уровень становится проблемой:
// Проблема: пропсы проходят через компоненты, которые их не используют
<A>
<B count={count} reset={reset}>
<C count={count} reset={reset}>
<D count={count} reset={reset}>
<E count={count} reset={reset} /> {/* Только E использует */}
</D>
</C>
</B>
</A>
3. Решения пропс-дриллинга
А. Context API (встроенное решение React)
// Создаём контекст
const CounterContext = createContext(null);
// Провайдер
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
const value = {
count,
increment: () => setCount(c => c + 1),
reset: () => setCount(0),
};
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
);
};
// Потребитель (любой уровень вложенности)
const DeeplyNestedComponent = () => {
const { count, reset } = useContext(CounterContext);
return (
<div>
<p>Счётчик: {count}</p>
<button onClick={reset}>Сбросить</button>
</div>
);
};
// Использование
const App = () => (
<CounterProvider>
<A>
<B>
<C>
<DeeplyNestedComponent />
</C>
</B>
</A>
</CounterProvider>
);
Б. Redux / Zustand / Jotai (внешние библиотеки)
// С Redux Toolkit
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => { state.count += 1; },
reset: (state) => { state.count = 0; },
},
});
const store = configureStore({ reducer: counterSlice.reducer });
// Компонент
const Counter = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(counterSlice.actions.reset())}>
Сбросить
</button>
</div>
);
};
4. Сравнение Context и Redux
| Аспект | Context API | Redux |
|---|---|---|
| Встроенность | Встроен в React | Внешняя библиотека |
| DevTools | Нет | Да (Redux DevTools) |
| Middleware | Нет | Да (redux-thunk, redux-saga) |
| Производительность | Все потребители ре-рендерятся при изменении | Только подписчики изменённых данных |
| Отладка | Сложнее | Проще (actions, state snapshot) |
| Кривая обучения | Низкая | Выше |
| Когда использовать | Простое состояние, редкие обновления | Сложное состояние, частые обновления |
5. Проблема производительности Context
// Проблема: при изменении ЛЮБОГО значения в контексте ре-рендерятся ВСЕ потребители
const AppContext = createContext();
const AppProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
// При изменении count ре-рендерятся потребители user и theme
const value = { user, setUser, theme, setTheme, count, setCount };
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
};
// Решение: разделять контексты
const UserContext = createContext();
const ThemeContext = createContext();
const CounterContext = createContext();
6. Аналогия в Go
В Go аналогия — это передача зависимостей:
// Пропс-дриллинг (аналог)
func A(ctx context.Context) {
count := ctx.Value("count").(int)
B(ctx, count) // Передаём через все уровни
}
func B(ctx context.Context, count int) {
C(ctx, count)
}
// Context (аналог React Context)
func WithCounter(ctx context.Context, count int) context.Context {
return context.WithValue(ctx, "count", count)
}
func Handler(ctx context.Context) {
count := ctx.Value("count").(int) // Доступ из любого уровня
}
Вывод:
Для решения пропс-дриллинга используется Context API для простых случаев и Redux/Zustand для сложных приложений. Важно помнить о производительности Context и разделять контексты по доменам.
Вопрос 21. Что делать, если вложенность компонентов очень большая и нужно прокидывать пропсы через множество уровней? В чём разница между Context API и внешними state manager'ами (Redux и т.д.)?
Таймкод: 00:58:43
Ответ собеседника: Неполный. Кандидат верно предложил использовать Context API или внешний менеджер состояния для решения проблемы пропс-дриллинга. Однако не смог назвать фундаментальных различий между Context и Redux (производительность, devtools, middleware, структура хранения данных).
Правильный ответ:
Это повторение предыдущего вопроса для закрепления. Ответ уже был подробно раскрыт в вопросе 20. Краткое резюме:
1. Решения пропс-дриллинга:
- Context API — встроенное решение React
- Redux / Zustand / Jotai — внешние библиотеки
- Composition — переструктуризация компонентов
2. Ключевые различия Context и Redux:
| Аспект | Context API | Redux |
|---|---|---|
| Ре-рендер | Все потребители при любом изменении | Только подписчики изменённых данных |
| DevTools | Нет | Да (Redux DevTools с time-travel) |
| Middleware | Нет | Да (thunk, saga, observable) |
| Структура | Произвольный объект | Единый store с редьюсерами |
| Отладка | Сложнее | Проще (actions, state snapshot) |
| Размер | 0 байт (встроен) | ~10-15 KB |
| Когда использовать | Простое состояние | Сложное состояние, частые обновления |
3. Проблема производительности Context:
// Плохо: один контекст для всего
const AppContext = createContext();
// Хорошо: разделение по доменам
const UserContext = createContext();
const ThemeContext = createContext();
const CounterContext = createContext();
4. Современные альтернативы:
- Zustand — минималистичный, хорошая производительность
- Jotai — атомарный подход, ре-рендер только при изменении атома
- Recoil — от Facebook, атомарный подход
- Valtio — прокси-based, автоматическое отслеживание зависимостей
Выбор зависит от сложности приложения: для простых случаев достаточно Context, для сложных — Redux или Zustand.
Вопрос 22. В чём разница между использованием внешнего state manager'а (например, Redux) и React Context? В каких случаях что предпочтительнее?
Таймкод: 01:01:05
Ответ собеседника: Неполный. Кандидат понимает, что Context — встроенное решение React, не требующее сторонних библиотек, а state manager'ы могут нести дополнительные уязвимости. Однако не смог назвать фундаментальных различий между ними (производительность, devtools, middleware, структура хранения данных).
Правильный ответ:
Это третье повторение того же вопроса. Ответ полностью раскрыт в вопросах 20 и 21. Финальное резюме:
Context API — когда использовать:
- Простое состояние (тема, язык, авторизация)
- Редкие обновления
- Небольшое приложение
- Нет необходимости в отладке состояния
Redux / Zustand — когда использовать:
- Сложное состояние с частыми обновлениями
- Нужны DevTools и time-travel debugging
- Нужен middleware для побочных эффектов
- Большое приложение с множеством команд
- Важна предсказуемость и тестируемость
Ключевое различие в производительности:
Context вызывает ре-рендер ВСЕХ потребителей при изменении ЛЮБОГО значения. Redux вызывает ре-рендер ТОЛЬКО подписчиков изменённых данных.
Собеседник правильно понимает базовые различия, но ему стоит углубить знания в аспекты производительности и инструменты разработки (DevTools, middleware).
Вопрос 23. Описать механизм работы Redux (Flux-архитектура) и асинхронные операции. Реализовать таймер с использованием setInterval в useEffect.
Таймкод: 01:05:33
Ответ собеседника: Неполный. Кандидат описал базовый поток Redux (dispatch → reducer → state → re-render) и упомянул Redux Thunk для асинхронности. При реализации таймера столкнулся с бесконечным циклом ре-рендеров из-за отсутствия очистки интервала. Задача не завершена из-за нехватки времени.
Правильный ответ:
Вопрос охватывает два важных темы: архитектуру Redux и работу с интервалами в React. Вот полное объяснение:
1. Flux-архитектура и Redux
Redux реализует паттерн Flux — однонаправленный поток данных:
Action → Dispatch → Reducer → Store → View → Action
Компоненты Redux:
- Action — объект, описывающий что произошло
- Reducer — чистая функция, которая вычисляет новое state
- Store — единый источник правды
- Dispatch — метод для отправки action в store
2. Redux Thunk для асинхронных операций
Redux Thunk — middleware, который позволяет диспатчить функции вместо объектов:
// Синхронный action
const increment = () => ({ type: 'INCREMENT' });
// Асинхронный action с thunk
const fetchUser = (userId) => {
return async (dispatch) => {
dispatch({ type: 'FETCH_USER_START' });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_ERROR', payload: error.message });
}
};
};
// Использование
dispatch(fetchUser(123));
3. Реализация таймера в React
import React, { useState, useEffect, useRef } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}
// Cleanup — обязательно!
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isRunning]);
const handleStart = () => setIsRunning(true);
const handleStop = () => setIsRunning(false);
const handleReset = () => {
setIsRunning(false);
setSeconds(0);
};
return (
<div>
<p>Время: {seconds} сек</p>
<button onClick={handleStart}>Старт</button>
<button onClick={handleStop}>Стоп</button>
<button onClick={handleReset}>Сброс</button>
</div>
);
};
4. Типичные ошибки с setInterval в React
Ошибка 1: Замыкание на старом значении
// Неправильно: seconds всегда 0 в замыкании
useEffect(() => {
const id = setInterval(() => {
console.log(seconds); // Всегда начальное значение
}, 1000);
return () => clearInterval(id);
}, []); // Пустой массив зависимостей
// Правильно: используем функциональный обновитель
useEffect(() => {
const id = setInterval(() => {
setSeconds(s => s + 1); // Получаем актуальное значение
}, 1000);
return () => clearInterval(id);
}, []);
Ошибка 2: Утечка интервала
// Неправильно: нет cleanup
useEffect(() => {
setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// При каждом ре-рендере создаётся новый интервал!
}, [isRunning]);
// Правильно: с cleanup
useEffect(() => {
const id = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(id); // Очищаем при изменении зависимости или размонтировании
}, [isRunning]);
5. Аналогия в Go
В Go аналогия — это работа с горутинами и таймерами:
package main
import (
"fmt"
"sync"
"time"
)
type Timer struct {
seconds int
running bool
stopChan chan struct{}
mu sync.Mutex
}
func NewTimer() *Timer {
return &Timer{
stopChan: make(chan struct{}),
}
}
func (t *Timer) Start() {
t.mu.Lock()
if t.running {
t.mu.Unlock()
return
}
t.running = true
t.mu.Unlock()
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
t.mu.Lock()
t.seconds++
fmt.Printf("Секунд: %d\n", t.seconds)
t.mu.Unlock()
case <-t.stopChan:
return
}
}
}()
}
func (t *Timer) Stop() {
t.mu.Lock()
defer t.mu.Unlock()
if t.running {
t.stopChan <- struct{}{}
t.running = false
}
}
func (t *Timer) Reset() {
t.Stop()
t.mu.Lock()
t.seconds = 0
t.mu.Unlock()
}
func main() {
timer := NewTimer()
timer.Start()
time.Sleep(3 * time.Second)
timer.Stop()
timer.Reset()
}
6. Ключевые моменты
- Redux: однонаправленный поток данных, предсказуемость, отладка
- Thunk: для асинхронных операций, позволяет диспатчить функции
- setInterval в React: всегда используйте cleanup в useEffect
- Замыкания: используйте функциональный обновитель (
setSeconds(s => s + 1)) - Утечки: неочищенные интервалы накапливаются и вызывают проблемы с производительностью
Вопрос 24. Реализовать компонент-таймер, который считает время (в секунды) с момента его монтирования
Таймкод: 01:10:38
Ответ собеседника: Неполный. Кандидат начал реализовать таймер через setInterval в useEffect, но столкнулся с ошибкой — бесконечный цикл ре-рендеров, так как интервал создавался при каждом рендере без очистки. Задача не была завершена из-за нехватки времени, но кандидат понимал концепцию.
Правильный ответ:
Это повторение предыдущего вопроса. Полное решение уже было приведено в вопросе 23. Вот компактная версия:
Решение:
import React, { useState, useEffect } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(s => s + 1); // Функциональное обновление
}, 1000);
// Cleanup — обязательно!
return () => clearInterval(intervalId);
}, []); // Пустой массив — запуск только при монтировании
return <p>Прошло: {seconds} сек</p>;
};
Ключевые моменты:
setIntervalвнутриuseEffectс пустым массивом зависимостейsetSeconds(s => s + 1)— функциональное обновление, чтобы не замыкаться на старом значенииreturn () => clearInterval(intervalId)— cleanup при размонтировании
Типичная ошибка (бесконечный цикл):
// Неправильно!
useEffect(() => {
setInterval(() => {
setSeconds(seconds + 1); // Замыкание на старом значении
}, 1000);
// Нет cleanup → утечка интервалов
}); // Нет массива зависимостей → создаётся при каждом рендере
Собеседник понимает концепцию, но ему нужно больше практики с очисткой побочных эффектов в React.
Вопрос 25. Бонусный вопрос на повышение: определить порядок вывода console.log в сложном примере с микрозадачами и макрозадачами (Promise, setTimeout, queueMicrotask)
Таймкод: 01:17:43
Ответ собеседния: Неполный. Кандидат попытался определить порядок вывода, но допустил ошибки. Верно понял, что микрозадачи выполняются раньше макрозадач. После подсказки убрал один resolve и смог объяснить поведение. Не до конца разобрался в порядке выполнения нескольких queueMicrotask и setTimeout.
Правильный ответ:
Это классический вопрос на понимание event loop в JavaScript. Вот подробное объяснение:
1. Типы задач в Event Loop
Микрозадачи (Microtask Queue):
Promise.then/catch/finallyqueueMicrotask()MutationObserverprocess.nextTick(Node.js)
Макрозадачи (Macrotask Queue):
setTimeoutsetIntervalsetImmediate(Node.js)requestAnimationFrame- I/O операции
2. Правила выполнения
- Выполнить весь синхронный код
- Выполнить ВСЕ микрозадачи (пока очередь не опустеет)
- Выполнить ОДНУ макрозадачу
- Повторить шаги 2-3
3. Пример для разбора
console.log('1');
setTimeout(() => console.log('2'), 0);
queueMicrotask(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
console.log('5');
Порядок выполнения:
Синхронный код: 1 → 5
Микрозадачи: 3 → 4
Макрозадачи: 2
Вывод: 1, 5, 3, 4, 2
4. Более сложный пример
console.log('1');
setTimeout(() => {
console.log('2');
queueMicrotask(() => console.log('3'));
}, 0);
queueMicrotask(() => {
console.log('4');
Promise.resolve().then(() => console.log('5'));
});
Promise.resolve()
.then(() => console.log('6'))
.then(() => console.log('7'));
console.log('8');
Пошаговый разбор:
Синхронный код:
→ console.log('1')
→ setTimeout добавляет в макрозадачи
→ queueMicrotask добавляет в микрозадачи
→ Promise.then добавляет в микрозадачи
→ console.log('8')
Микрозадачи (выполняем все):
→ console.log('4') — первая микрозадача
→ Promise.resolve().then(() => console.log('5')) — добавляет новую микрозадачу
→ console.log('6') — вторая микрозадача
→ .then(() => console.log('7')) — добавляет новую микрозадачу
→ console.log('5') — микрозадача, добавлена во время выполнения
→ console.log('7') — микрозадача, добавлена во время выполнения
Макрозадачи (выполняем одну):
→ console.log('2')
→ queueMicrotask(() => console.log('3')) — добавляет микрозадачу
Микрозадачи (после макрозадачи):
→ console.log('3')
Вывод: 1, 8, 4, 6, 5, 7, 2, 3
5. Аналогия в Go
В Go аналогия — это приоритеты в планировщике:
package main
import (
"fmt"
"time"
)
func main() {
// Синхронный код (аналог синхронного JS)
fmt.Println("1")
// Макрозадачи (аналог setTimeout)
time.AfterFunc(0, func() {
fmt.Println("2")
})
// Микрозадачи (аналог Promise)
// В Go нет прямого аналога, но можно использовать каналы
done := make(chan struct{})
go func() {
fmt.Println("3")
close(done)
}()
fmt.Println("4")
<-done // Ждём завершения "микрозадачи"
}
6. Ключевые правила для запоминания
- Синхронный код выполняется первым
- Микрозадачи выполняются все до одной макрозадачи
- Макрозадачи выполняются по одной
- Новые микрозадачи, добавленные во время выполнения микрозадач, выполняются в том же цикле
- Новые микрозадачи, добавленные из макрозадачи, выполняются после следующей макрозадачи
7. Шпаргалка
Порядок приоритета:
1. Синхронный код
2. process.nextTick (только Node.js)
3. Микрозадачи (Promise, queueMicrotask)
4. Макрозадачи (setTimeout, setInterval)
Понимание event loop критически важно для отладки асинхронного кода и предсказания порядка выполнения операций.
Вопрос 26. Какие оптимизации можно применить для таблицы с 10 000 строк? Знаком ли с React Profiler и инструментами профилирования?
Таймкод: 01:34:21
Ответ собеседния: Неполный. Кандидат предложил виртуализацию списка — загрузку только видимых строк с подгрузкой при скролле и выгрузкой невидимых. Знает про React Profiler как инструмент для анализа ре-рендеров, но не использовал его на практике. Не смог детально описать его возможности.
Правильный ответ:
Вопрос проверяет знание оптимизации производительности React-приложений и инструментов профилирования.
1. Оптимизации для таблицы с 10 000 строк
А. Виртуализация (Virtualization)
Самая важная оптимизация — рендерить только видимые строки:
// Библиотека react-window (рекомендуется)
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>
Строка {index}: {data[index].name}
</div>
);
const VirtualizedTable = ({ data }) => (
<List
height={600}
itemCount={data.length}
itemSize={35}
width="100%"
>
{Row}
</List>
);
// Альтернатива: react-virtualized
import { List, AutoSizer } from 'react-virtualized';
Б. Мемоизация компонентов строк
const TableRow = React.memo(({ row, onEdit }) => {
return (
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
<td>
<button onClick={() => onEdit(row.id)}>Редактировать</button>
</td>
</tr>
);
}, (prevProps, nextProps) => {
// Кастомное сравнение для предотвращения лишних ре-рендеров
return prevProps.row.id === nextProps.row.id &&
prevProps.row.name === nextProps.row.name;
});
В. Мемоизация вычислений
const Table = ({ data, filter }) => {
// Мемоизация отфильтрованных данных
const filteredData = useMemo(() => {
return data.filter(row =>
row.name.toLowerCase().includes(filter.toLowerCase())
);
}, [data, filter]);
// Мемоизация callback-функций
const handleEdit = useCallback((id) => {
// Логика редактирования
}, []);
return (
<table>
{filteredData.map(row => (
<TableRow
key={row.id}
row={row}
onEdit={handleEdit}
/>
))}
</table>
);
};
Г. Пагинация
const PaginatedTable = ({ data, pageSize = 100 }) => {
const [page, setPage] = useState(0);
const pageData = useMemo(() => {
const start = page * pageSize;
return data.slice(start, start + pageSize);
}, [data, page, pageSize]);
return (
<div>
<table>
{pageData.map(row => (
<TableRow key={row.id} row={row} />
))}
</table>
<Pagination
current={page}
total={data.length}
pageSize={pageSize}
onChange={setPage}
/>
</div>
);
};
Д. Ленивая загрузка данных
const LazyTable = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const loadMore = useCallback(async () => {
if (loading) return;
setLoading(true);
const newData = await fetchNextPage();
setData(prev => [...prev, ...newData]);
setLoading(false);
}, [loading]);
return (
<InfiniteScroll
dataLength={data.length}
next={loadMore}
hasMore={true}
loader={<LoadingSpinner />}
>
<VirtualizedTable data={data} />
</InfiniteScroll>
);
};
2. React Profiler
React Profiler — встроенный инструмент для измерения производительности рендеринга.
А. React DevTools Profiler
1. Установите React DevTools (расширение для браузера)
2. Откройте вкладку "Profiler"
3. Нажмите "Record"
4. Взаимодействуйте с приложением
5. Нажмите "Stop"
6. Анализируйте результаты
Б. Программное использование Profiler API
import { Profiler } from 'react';
const onRenderCallback = (
id, // id компонента
phase, // "mount" | "update" | "nested-update"
actualDuration, // время рендеринга в мс
baseDuration, // время рендеринга без оптимизаций
startTime, // время начала рендеринга
commitTime, // время завершения коммита
interactions // множество взаимодействий
) => {
console.log({
id,
phase,
actualDuration,
baseDuration,
});
};
const App = () => (
<Profiler id="Table" onRender={onRenderCallback}>
<Table data={data} />
</Profiler>
);
3. Инструменты профилирования
А. Chrome DevTools
- Performance tab — запись и анализ производительности
- Memory tab — анализ утечек памяти
- Rendering tab — визуализация repaints, layer borders
Б. React DevTools
- Components tab — иерархия компонентов, пропсы, хуки
- Profiler tab — время рендеринга, flamegraph, ranked chart
В. Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function reportWebVitals(metric) {
console.log(metric);
}
getCLS(reportWebVitals); // Cumulative Layout Shift
getFID(reportWebVitals); // First Input Delay
getLCP(reportWebVitals); // Largest Contentful Paint
4. Аналогия в Go
В Go аналогия — это профилирование с помощью pprof:
package main
import (
"net/http"
_ "net/http/pprof"
"runtime"
)
func main() {
// Включаем профилирование
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Или программно
runtime.SetCPUProfileRate(500)
runtime.MemProfileRate = 512 * 1024
// Приложение...
}
5. Чек-лист оптимизаций
- Виртуализация списка (react-window)
- React.memo для компонентов строк
- useMemo для вычислений
- useCallback для callback-функций
- Пагинация или infinite scroll
- Ленивая загрузка данных
- Оптимизация селекторов (reselect для Redux)
- Code splitting (React.lazy)
- Оптимизация изображений (lazy loading, WebP)
- Минификация и сжатие бандла
Вывод:
Виртуализация — это ключевая оптимизация для больших таблиц. React Profiler и DevTools — необходимые инструменты для поиска узких мест. Мемоизация (React.memo, useMemo, useCallback) помогает предотвратить лишние ре-рендеры.
