MVC

Как я говорил в прошлом уроке, наша схема работы с состоянием имеет один существенный недостаток — за вызов отрисовки отвечают обработчики. Ниже приведён пример, демонстрирующий вызов 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, но это довольно сложно. Более простым решением будет использование готовой библиотеки on-change.

import onChange from 'on-change';

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

  const watchedState = onChange(state, (path, value, previousValue) => {
    alert('value changed!');
    console.log(path);
    // => 'ui.value'
    console.log(value);
    // => 'other value'
    console.log(previousValue);
    // => 'hello'
  });

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

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

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

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

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

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

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

const watchedState = onChange(state, (path, value) => {
  if (path === 'registrationProcess.state') {
    // Делаем HTTP-запрос! Нарушение MVC!
    if (value === 'sending') {
      axios.post(endpoint, watchedState.registrationProcess.data);
    }
  }
});

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


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

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

Хекслет

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