В JavaScript часто возникает подобная задача. Необходимо создать авто обновление части страницы в ответ на определенные события, используя данные, которые они предоставляют. Скажем, например, пользовательский ввод, который вы затем проецируете на один или несколько компонентов. Это приводит к большому количеству push-and-pull в коде, чтобы все синхронизировать.
Это самый очевидный пример того, где может помочь шаблон проектирования observer (наблюдатель). Этот шаблон позволяет связывать данные «один ко многим» между элементами. Эта односторонняя привязка данных может управляться событиями.
В этой статье я хотел бы рассмотреть шаблон проектирования observer. Он поможет вам решить частую проблему, создания связи «один ко многим», «односторонняя и управляемая событиями». Это проблема, которая часто возникает, когда у вас много элементов, которые должны быть синхронизированы.
Я буду использовать ECMAScript 6 для создания примеров иллюстрации шаблона. Еще будут классы, стрелочные функции и константы. Не стесняйтесь изучить эти темы самостоятельно, если вы еще с ними не знакомы. Так же я буду следовать Test-Driven-Development (TDD) походу. Таким образом, у нас будет способ узнать, насколько полезен каждый компонент.
Итак, начнем.
Общий вид шаблона выглядит следующим образом:
EventObserver │ ├── subscribe: добавляет новые наблюдаемые события │ ├── unsubscribe: удаляет наблюдаемые события | └── broadcast: выполняет все события со связанными данными
После того, как я создан шаблон наблюдателя, я добавлю пример его использования с компонентом подсчетом количество слов.
Создадим и инициализируем EventObserver:
class EventObserver { constructor() { this.observers = []; } }
Начнем с пустого списка наблюдаемых событий observers для каждого нового экземпляра. Далее добавим еще методов в EventObserver в соответствие с шаблоном проектирования.
Добавим новый метод subscribe:
subscribe(fn) { this.observers.push(fn); }
Размещаем новый элемент в массив наблюдаемых событий. Список событий представляет собой список функций обратного вызова.
Один из способов проверить этот метод:
// Arrange const observer = new EventObserver(); const fn = () => {}; // Act observer.subscribe(fn); // Assert assert.strictEqual(observer.observers.length, 1);
Тут я использую функцию Node assert для тестирования этого компонента. Точно такие же assertions существуют и в Chai assertions.
Обратите внимание, что список наблюдаемых событий состоит из обратных вызовов. Затем мы проверяем длину списка и проверяем, что обратный вызов находится в списке.
Сделаем следующий метод для удаления события:
unsubscribe(fn) { this.observers = this.observers.filter((subscriber) => subscriber !== fn); }
Отфильтруем из списка все, что соответствует функции обратного вызова. Если совпадений нет, обратный вызов остается в списке. Фильтр возвращает новый список и переназначает список наблюдателей.
Тестирование метода:
// Arrange const observer = new EventObserver(); const fn = () => {}; observer.subscribe(fn); // Act observer.unsubscribe(fn); // Assert assert.strictEqual(observer.observers.length, 0);
Обратный вызов должен соответствовать той же функции, что и в списке. В случае совпадения метод unsubscribe удаляет его из списка.
Следующий метод для вызова всех событий:
broadcast(data) { this.observers.forEach((subscriber) => subscriber(data)); }
Здесь мы перебираем список наблюдаемых событий и выполняет все обратные вызовы. Благодаря этому вы получаете необходимое отношение «один ко многим» к подписанным событиям. Так же здесь передается параметр data, который связывает данные обратного вызова.
ES6 делает код более эффективным с помощью стрелочной функции. Обратите внимание на функцию (subscriber) => subscriber(data), которая выполняет большую часть работы.
Проверим метод broadcast:
// Arrange const observer = new EventObserver(); let subscriberHasBeenCalled = false; const fn = (data) => subscriberHasBeenCalled = data; observer.subscribe(fn); // Act observer.broadcast(true); // Assert assert(subscriberHasBeenCalled);
Этот тест позволяет мне убедиться, в том что наблюдатель работает так, как я ожидаю.
Таким образом мы создали EventObserver. Вопрос в том, что нам с ним делать дальше?
Для демонстрации, мы будем автоматически подсчитывать слова в тексте поста. Каждое нажатие клавиши, которое вы вводите в качестве ввода, будет вызывать подсчет слов в помощью шаблона EventObserver.
Чтобы получить количество слов из свободного ввода текста, можно сделать сделующее:
const getWordCount = (text) => text ? text.trim().split(/\s+/).length : 0;
В этой, казалось бы, простой функции происходит много всего. Далее напишем модульный тест.
// Arrange const blogPost = 'This is a blog \n\n post with a word count. '; // Act const count = getWordCount(blogPost); // Assert assert.strictEqual(count, 9);
Обратите внимание на несколько странную строку ввода внутри blogPost. Я намерен, чтобы эта функция охватывала как можно больше крайних случаев. Пока это дает мне правильное количество слов, значит мы движемся в правильном направлении.
Время подключить эти компоненты к DOM. Это та часть, где мы можете использовать простой JavaScript и встроить его прямо в браузер.
Добавим следующий HTML-код на страницу:
<textarea id="blogPost" placeholder="Enter your blog post..." class="blogPost"> </textarea>
Вслед за ним JavaScript:
const wordCountElement = document.createElement('p'); wordCountElement.className = 'wordCount'; wordCountElement.innerHTML = 'Word Count: <strong id="blogWordCount">0</strong>'; document.body.appendChild(wordCountElement); const blogObserver = new EventObserver(); blogObserver.subscribe((text) => { const blogCount = document.getElementById('blogWordCount'); blogCount.textContent = getWordCount(text); }); const blogPost = document.getElementById('blogPost'); blogPost.addEventListener('keyup', () => blogObserver.broadcast(blogPost.value));
Этот код позволит отслеживать изменения в textarea и подсчитывать количество слов прямо под ней. Я использую body.appendChild() в DOM API, чтобы добавить новый элемент. Затем event listeners, чтобы воплотить идею в жизнь.
Обратите внимание, что с помощью стрелочных функций можно связать события одной строкой. () => BlogObserver.broadcast() выполняет большую часть работы. Он передает последние изменения в текстовую область прямо в функцию обратного вызова.
Ни одна демонстрация не будет полной без той, которую вы можете потрогать и настроить, поэтому ниже приведена ссылка на CodePen:
Я бы не назвал этот пример завершенным. Это всего лишь отправная точка для изучения шаблона Наблюдатель. На мой взгляд, вопрос, как далеко вы готовы пойти?
Это зависит от вас, чтобы развить эту идею еще дальше. Есть много способов использовать шаблон наблюдателя для создания новых функций.
Вы можете добавим в демо:
Это всего лишь несколько идей, которые вы можете сделать, чтобы глубже погрузиться в это.
Шаблон проектирования Наблюдатель может помочь вам решить реальные проблемы в JavaScript. Он решает проблему синхронизации множества элементов с одними и теми же данными. Как часто бывает, когда браузер запускает определенные события. Я уверен, что большинство из вас уже столкнулись с такой проблемой и столкнулись с инструментами и сторонними зависимостями.
Этот шаблон позволяет вам идти так далеко, как того хочет ваше воображение. В программировании вы абстрагируете решение в шаблон и создаете многократно используемый код. Нет предела тому, как далеко это вас унесет.
Эта статья была рецензирована Giulio Mainardi.
Оригинальная статья: Camilo Reyes — JavaScript Design Patterns: The Observer Pattern
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…