← JavaScript/События в браузере#87 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

События в браузере

Какую проблему решают события

Ты разрабатываешь Trello. Пользователь кликает «Добавить карточку», вводит текст, нажимает Enter — карточка появляется на доске. Эти взаимодействия — клики, нажатия, ввод — браузер оборачивает в события. JavaScript реагирует на них через обработчики.

На основе предыдущих уроков

  • «DOM» — события привязываются к DOM-элементам
  • «Функции» — обработчик события это колбэк-функция
  • «Стрелочные функции» — в обработчиках часто используют стрелки
  • addEventListener

    const addBtn = document.querySelector('#add-card-btn')
    
    addBtn.addEventListener('click', function(event) {
      console.log('Кликнули по кнопке')
      console.log(event.type)    // 'click'
      console.log(event.target)  // элемент, на котором произошло событие
    })
    
    // Стрелочная функция — короче, но this будет другим
    addBtn.addEventListener('click', (event) => {
      createCard(event.target.dataset.listId)
    })

    Объект события (Event)

    form.addEventListener('submit', (event) => {
      event.preventDefault()    // отменяет отправку формы на сервер (перезагрузку)
      event.stopPropagation()   // останавливает всплытие к родителям
    
      const formData = new FormData(event.target)
      const title = formData.get('title')
      createCard(title)
    })

    Ключевые свойства:

  • target — элемент, на котором произошло событие (где кликнули)
  • currentTarget — элемент, на котором висит обработчик
  • key — нажатая клавиша (для keydown/keyup): 'Enter', 'Escape', 'ArrowUp'
  • Всплытие (Event Bubbling)

    Событие сначала срабатывает на целевом элементе, потом всплывает к родителям:

    // Клик на <button> внутри <li> внутри <ul>:
    // button → li → ul → body → html → document

    Это поведение — основа делегирования событий.

    Делегирование событий

    Один обработчик на родителе вместо обработчика на каждом дочернем элементе:

    // Плохо: N карточек = N обработчиков = расход памяти
    document.querySelectorAll('.card').forEach(card => {
      card.addEventListener('click', handleCardClick)
    })
    
    // Хорошо: один обработчик на весь список
    const board = document.querySelector('#board')
    board.addEventListener('click', (event) => {
      // closest() ищет ближайший предок с нужным селектором
      const btn = event.target.closest('[data-action]')
      if (!btn) return  // клик не по кнопке с data-action — игнорируем
    
      const action = btn.dataset.action       // 'delete', 'edit', 'move'
      const cardId = btn.closest('.card').dataset.id
    
      handleCardAction(action, parseInt(cardId))
    })

    Преимущества делегирования:

    1. Один обработчик вместо тысячи — меньше памяти

    2. Работает для динамически добавленных элементов

    3. Легко управлять — убрать один обработчик, а не тысячу

    Популярные события

    // Живой поиск — каждый введённый символ
    searchInput.addEventListener('input', e => filterResults(e.target.value))
    
    // Горячие клавиши
    document.addEventListener('keydown', e => {
      if (e.key === 'Escape') closeModal()
      if (e.key === 'Enter' && e.ctrlKey) submitForm()
      if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
        e.preventDefault()  // не сохранять страницу!
        saveDocument()
      }
    })
    
    // Инициализация приложения
    document.addEventListener('DOMContentLoaded', () => initApp())

    Типичные ошибки

    1. Забыли preventDefault() на форме — страница перезагружается:

    // Сломано:
    form.addEventListener('submit', (e) => {
      // нет e.preventDefault() — форма отправится, страница перезагрузится!
      const data = collectFormData()  // не успеет выполниться
    })
    
    // Исправлено:
    form.addEventListener('submit', (e) => {
      e.preventDefault()   // блокируем стандартное поведение браузера
      const data = collectFormData()
      sendToServer(data)
    })

    2. Используют onclick вместо addEventListener — теряют предыдущие обработчики:

    // Сломано — второй вызов затирает первый:
    btn.onclick = showTooltip
    btn.onclick = openModal   // showTooltip больше не сработает!
    
    // Исправлено:
    btn.addEventListener('click', showTooltip)
    btn.addEventListener('click', openModal)  // оба сработают

    3. Не убирают обработчик при удалении элемента — утечка памяти:

    // Лучше — автоудаление после первого срабатывания:
    modal.addEventListener('click', closeOnBackdrop, { once: true })

    В реальных проектах

  • React: onClick, onChange, onSubmit — синтетические события поверх addEventListener
  • Infinite scroll: window.addEventListener('scroll', loadMore)
  • Drag & Drop: mousemove, mouseup, mousedown — основа Trello-подобных досок
  • Keyboard shortcuts: Figma, Notion, GitHub — все горячие клавиши через keydown
  • Примеры

    Симуляция делегирования событий: доска задач Trello-стиль

    // Симуляция системы событий без реального DOM
    // В браузере: board.addEventListener('click', e => dispatch(e.target.dataset))
    
    const board = {
      cards: [
        { id: 1, title: 'Изучить промисы',   done: false, priority: 'high' },
        { id: 2, title: 'Написать тесты',    done: false, priority: 'medium' },
        { id: 3, title: 'Сделать код-ревью', done: true,  priority: 'low' },
      ],
    }
    
    // Единый диспетчер — один обработчик вместо N кнопок на N карточках
    function dispatch(action, cardId) {
      const card = board.cards.find(c => c.id === cardId)
      if (!card) { console.log('Карточка не найдена:', cardId); return }
    
      switch (action) {
        case 'toggle':
          card.done = !card.done
          console.log(`[${card.done ? 'x' : ' '}] ${card.title}`)
          break
    
        case 'delete': {
          const idx = board.cards.indexOf(card)
          board.cards.splice(idx, 1)
          console.log(`Удалено: "${card.title}"`)
          break
        }
    
        case 'priority': {
          const levels = ['low', 'medium', 'high']
          const next = levels[(levels.indexOf(card.priority) + 1) % levels.length]
          card.priority = next
          console.log(`"${card.title}" → приоритет: ${next}`)
          break
        }
    
        default:
          console.log('Неизвестное действие:', action)
      }
    }
    
    // В браузере это была бы HTML-разметка:
    // <ul id="board">
    //   <li class="card" data-id="1">
    //     <button data-action="toggle">✓</button>
    //     <button data-action="delete">✕</button>
    //     <button data-action="priority">↑</button>
    //   </li>
    // </ul>
    // И ОДИН обработчик на всю доску
    
    console.log('=== Начальное состояние ===')
    board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))
    
    console.log('\n=== Симуляция кликов ===')
    dispatch('toggle', 1)      // отмечаем выполненной
    dispatch('priority', 2)    // повышаем приоритет
    dispatch('delete', 3)      // удаляем
    
    console.log('\n=== После изменений ===')
    board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))

    События в браузере

    Какую проблему решают события

    Ты разрабатываешь Trello. Пользователь кликает «Добавить карточку», вводит текст, нажимает Enter — карточка появляется на доске. Эти взаимодействия — клики, нажатия, ввод — браузер оборачивает в события. JavaScript реагирует на них через обработчики.

    На основе предыдущих уроков

  • «DOM» — события привязываются к DOM-элементам
  • «Функции» — обработчик события это колбэк-функция
  • «Стрелочные функции» — в обработчиках часто используют стрелки
  • addEventListener

    const addBtn = document.querySelector('#add-card-btn')
    
    addBtn.addEventListener('click', function(event) {
      console.log('Кликнули по кнопке')
      console.log(event.type)    // 'click'
      console.log(event.target)  // элемент, на котором произошло событие
    })
    
    // Стрелочная функция — короче, но this будет другим
    addBtn.addEventListener('click', (event) => {
      createCard(event.target.dataset.listId)
    })

    Объект события (Event)

    form.addEventListener('submit', (event) => {
      event.preventDefault()    // отменяет отправку формы на сервер (перезагрузку)
      event.stopPropagation()   // останавливает всплытие к родителям
    
      const formData = new FormData(event.target)
      const title = formData.get('title')
      createCard(title)
    })

    Ключевые свойства:

  • target — элемент, на котором произошло событие (где кликнули)
  • currentTarget — элемент, на котором висит обработчик
  • key — нажатая клавиша (для keydown/keyup): 'Enter', 'Escape', 'ArrowUp'
  • Всплытие (Event Bubbling)

    Событие сначала срабатывает на целевом элементе, потом всплывает к родителям:

    // Клик на <button> внутри <li> внутри <ul>:
    // button → li → ul → body → html → document

    Это поведение — основа делегирования событий.

    Делегирование событий

    Один обработчик на родителе вместо обработчика на каждом дочернем элементе:

    // Плохо: N карточек = N обработчиков = расход памяти
    document.querySelectorAll('.card').forEach(card => {
      card.addEventListener('click', handleCardClick)
    })
    
    // Хорошо: один обработчик на весь список
    const board = document.querySelector('#board')
    board.addEventListener('click', (event) => {
      // closest() ищет ближайший предок с нужным селектором
      const btn = event.target.closest('[data-action]')
      if (!btn) return  // клик не по кнопке с data-action — игнорируем
    
      const action = btn.dataset.action       // 'delete', 'edit', 'move'
      const cardId = btn.closest('.card').dataset.id
    
      handleCardAction(action, parseInt(cardId))
    })

    Преимущества делегирования:

    1. Один обработчик вместо тысячи — меньше памяти

    2. Работает для динамически добавленных элементов

    3. Легко управлять — убрать один обработчик, а не тысячу

    Популярные события

    // Живой поиск — каждый введённый символ
    searchInput.addEventListener('input', e => filterResults(e.target.value))
    
    // Горячие клавиши
    document.addEventListener('keydown', e => {
      if (e.key === 'Escape') closeModal()
      if (e.key === 'Enter' && e.ctrlKey) submitForm()
      if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
        e.preventDefault()  // не сохранять страницу!
        saveDocument()
      }
    })
    
    // Инициализация приложения
    document.addEventListener('DOMContentLoaded', () => initApp())

    Типичные ошибки

    1. Забыли preventDefault() на форме — страница перезагружается:

    // Сломано:
    form.addEventListener('submit', (e) => {
      // нет e.preventDefault() — форма отправится, страница перезагрузится!
      const data = collectFormData()  // не успеет выполниться
    })
    
    // Исправлено:
    form.addEventListener('submit', (e) => {
      e.preventDefault()   // блокируем стандартное поведение браузера
      const data = collectFormData()
      sendToServer(data)
    })

    2. Используют onclick вместо addEventListener — теряют предыдущие обработчики:

    // Сломано — второй вызов затирает первый:
    btn.onclick = showTooltip
    btn.onclick = openModal   // showTooltip больше не сработает!
    
    // Исправлено:
    btn.addEventListener('click', showTooltip)
    btn.addEventListener('click', openModal)  // оба сработают

    3. Не убирают обработчик при удалении элемента — утечка памяти:

    // Лучше — автоудаление после первого срабатывания:
    modal.addEventListener('click', closeOnBackdrop, { once: true })

    В реальных проектах

  • React: onClick, onChange, onSubmit — синтетические события поверх addEventListener
  • Infinite scroll: window.addEventListener('scroll', loadMore)
  • Drag & Drop: mousemove, mouseup, mousedown — основа Trello-подобных досок
  • Keyboard shortcuts: Figma, Notion, GitHub — все горячие клавиши через keydown
  • Примеры

    Симуляция делегирования событий: доска задач Trello-стиль

    // Симуляция системы событий без реального DOM
    // В браузере: board.addEventListener('click', e => dispatch(e.target.dataset))
    
    const board = {
      cards: [
        { id: 1, title: 'Изучить промисы',   done: false, priority: 'high' },
        { id: 2, title: 'Написать тесты',    done: false, priority: 'medium' },
        { id: 3, title: 'Сделать код-ревью', done: true,  priority: 'low' },
      ],
    }
    
    // Единый диспетчер — один обработчик вместо N кнопок на N карточках
    function dispatch(action, cardId) {
      const card = board.cards.find(c => c.id === cardId)
      if (!card) { console.log('Карточка не найдена:', cardId); return }
    
      switch (action) {
        case 'toggle':
          card.done = !card.done
          console.log(`[${card.done ? 'x' : ' '}] ${card.title}`)
          break
    
        case 'delete': {
          const idx = board.cards.indexOf(card)
          board.cards.splice(idx, 1)
          console.log(`Удалено: "${card.title}"`)
          break
        }
    
        case 'priority': {
          const levels = ['low', 'medium', 'high']
          const next = levels[(levels.indexOf(card.priority) + 1) % levels.length]
          card.priority = next
          console.log(`"${card.title}" → приоритет: ${next}`)
          break
        }
    
        default:
          console.log('Неизвестное действие:', action)
      }
    }
    
    // В браузере это была бы HTML-разметка:
    // <ul id="board">
    //   <li class="card" data-id="1">
    //     <button data-action="toggle">✓</button>
    //     <button data-action="delete">✕</button>
    //     <button data-action="priority">↑</button>
    //   </li>
    // </ul>
    // И ОДИН обработчик на всю доску
    
    console.log('=== Начальное состояние ===')
    board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))
    
    console.log('\n=== Симуляция кликов ===')
    dispatch('toggle', 1)      // отмечаем выполненной
    dispatch('priority', 2)    // повышаем приоритет
    dispatch('delete', 3)      // удаляем
    
    console.log('\n=== После изменений ===')
    board.cards.forEach(c => console.log(`[${c.done ? 'x' : ' '}] (${c.priority}) ${c.title}`))

    Задание

    Ты разрабатываешь ленту постов социальной сети (Instagram-стиль). Симулируй делегирование событий через JavaScript без DOM. Создай массив постов и функцию `handlePostAction(postId, action)`, обрабатывающую три действия: - `'like'` — увеличивает счётчик лайков, выводит новое значение - `'bookmark'` — переключает `bookmarked` (true/false), выводит статус - `'delete'` — удаляет пост из массива, выводит подтверждение Покажи, что один обработчик заменяет множество индивидуальных.

    Подсказка

    post.likes++ для лайка. post.bookmarked = !post.bookmarked для закладки. Для удаления: const idx = posts.findIndex(p => p.id === postId); posts.splice(idx, 1). Вся идея: один handlePostAction вместо обработчика на каждом посте.

    Загружаем среду выполнения...
    Загружаем AI-помощника...