Как я говорил в прошлом уроке, наша схема работы с состоянием имеет один существенный недостаток - за вызов отрисовки отвечают обработчики. Ниже приведен, пример демонстрирующий вызов render.

input.addEventListener('change', () => {
  const { registrationProcess } = state;
  if (input.value === '') {
    registrationProcess.validationState = 'valid';
    registrationProcess.errors = [];
  } else if (!input.value.match(/^\d+$/)) {
    registrationProcess.validationState = 'invalid';
    registrationProcess.errors = ['Bad format'];
  } else {
    registrationProcess.validationState = 'valid';
    registrationProcess.errors = [];
  }

  render(state);
});

Какие проблемы могут возникнуть при таком подходе?

Здесь стоит сказать, что на бекенде такой подход, как раз, оправдан. Бекенд работает в рамках другой парадигмы, а именно клиент-серверной архитектуры. Обработчик на бекенде, по своей сути, это функция которая либо меняет состояние (что не приводит ни к каким перерисовкам, так как выполняется редирект), либо извлекает данные из базы для формирования ответа, например, в виде HTML. Во фронтенде изменение данных тут же влияет на экран.

Пример который мы видим выше очень упрощен, в нем вызывается только одна функция render принимающая на вход все состояние. Теперь представьте что у нас в приложении десятки обработчиков (что немного) и большое состояние (что типично). В такой ситуации перерисовывать все на каждое изменение довольно затратная операция. С другой стороны, можно вставить проверку внутри render на каждый кусок стейта и отслеживать изменился ли он. Такой подход очень быстро станет проблемой сам по себе. Можно легко забыть что-то проверить, можно ошибиться в проверке, можно, просто, забыть поправить проверку после изменения структуры состояния.

Существует другой способ выполнить эту задачу. Он основан на такой концепции (говорят шаблон проектирования), как Наблюдатель (Observer). Его идея очень проста, одна часть системы наблюдает за изменением другой части системы. Если наблюдаемый изменился, то наблюдатель может сделать что-то полезное.

В JS подобный механизм можно реализовать через Proxy, но это довольно муторно. Более простым решением будет использование готовой библиотеки Watch.js.

import { watch } from 'melanke-watchjs';

const app = () => {
  const state = {
    value: 'hello',
  };

  watch(state, 'value', () => alert('value changed!'));

  // После изменения атрибута возникнет алерт
  const el = document.querySelector('<selector>');
  el.addEventListener('change', () => {
    state.value = 'other value';
  });
}

// Где-то в другом файле (обычно в index.js)
app();

Watch.js позволяет "слушать" нужные части состояния и вызывать функции рендеринга при их изменении. То, какие части конкретно слушать и сколько вешать "вотчеров" зависит от задачи. В примитивных ситуациях достаточно одного вотчера на весь стейт (скорее это учебный проект), в реальных же ситуациях вотчеры делают так, чтобы было удобно (в каждой ситуации по разному).

See the Pen js_dom_mvc_watch by Hexlet (@hexlet) on CodePen.

Теперь, обработчики ничего не знают про рендеринг и отвечают только за взаимодействие с состоянием. В свою очередь рендеринг следит за состоянием и меняет дом только там где нужно и так как нужно. Этот способ организации приложения считается уже классическим и носит имя MVC (Model View Controller). Каждое слово обозначает слой приложения со своей зоной ответственности. Model - состояние приложения и бизнес-логика, View - слой отвечающий за взаимодействие с DOM, Controller - обработчики.

Обратите внимание на то что Model, Controller или View это не файлы, не классы, ни что-либо еще конкретное. Это логические слои, которые выполняют свою задачу и определенным образом взаимодействуют друг с другом.

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

MVC

Самое важное на этой картинке – стрелки между слоями. Они определяют барьеры абстракции. Кто с кем и как может взаимодействовать, а кто нет. Например, на этой диаграмме нет стрелки из контроллера во вью. Это обозначает, что контроллер не может (не может!) менять вью минуя модель. То что отражено на экране это отображение состояния приложения и никак иначе. Такой код считается нарушением:

// Предположим что у на странце есть одна форма
// с полем для ввода задачи и кнопка для ее добавления

const submit = document.querySelector('form');
const input = document.querySelector('form input');
form.addEventListener('submit', () => {
  state.registrationProcess.state = 'processing';
  // Что то делаем с данными, например добавляем в стейт
  input.value = ''; // Очистка инпута напрямую! Нарушение MVC!
});

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

watch(state, 'registrationProcess.state', () => {
  // Обновляется стейт! Нарушение MVC!
  state.registrationProcess.alert = 'Sending data...';
});

И конечно вью не может притворяться контроллером и выполнять например HTTP запросы:

watch(state, 'registrationProcess.state', () => {
  // Обновляется стейт! Нарушение MVC!
  if (state.registrationProcess.state === 'sending') {
    axios.post(endpoint, registrationProcess.data);
  }
});

Итого: Контроллер что-то делает с данными, на изменение данных срабатывает слой вью и изменяет DOM.


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

  1. Backbone MVC
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →