Статьи

Как управлять состоянием приложения JavaScript с MobX

Управление полетом наблюдает за ракетой на другую планету

Эта статья была рецензирована Мишелем Вестстрате и Аароном Бойером. Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Если вы когда-нибудь писали нечто большее, чем очень простое приложение с jQuery, вы, вероятно, столкнулись с проблемой синхронизации различных частей пользовательского интерфейса. Часто изменения в данных должны отражаться в нескольких местах, и по мере роста приложения вы можете оказаться связанными узлами. Чтобы приручить безумие, обычно используют события, чтобы сообщить различным частям приложения, когда что-то изменилось.

Итак, как вы управляете состоянием вашего приложения сегодня? Я собираюсь выйти на конечность и сказать, что вы слишком подписались на изменения. Это верно. Я даже не знаю тебя, и я собираюсь вызвать тебя. Если вы не слишком подписаны, то я уверен, что вы работаете слишком усердно.

Если, конечно, вы не используете MobX …

Что такое «государство»?

Вот человек. Эй, это я! У меня есть имя, firstNamelastName
Кроме того, функция age

 fullName()

Как бы вы сообщили своим различным выходным данным (просмотр, сервер, журнал отладки) об изменениях этого человека? Когда бы вы вызвали эти уведомления? До MobX я использовал сеттеры, которые запускали бы пользовательские события jQuery или js-сигналы . Эти варианты хорошо мне помогли, однако их использование было далеко не гранулированным. Я бы запустил одно «измененное» событие, если какая-либо часть объекта var person = {
firstName: 'Matt',
lastName: 'Ruby',
age: 37,
fullName: function () {
this.firstName + ' ' + this.lastName;
}
};

Допустим, у меня есть фрагмент кода, который показывает мое имя. Если бы я изменил свой возраст, это представление обновилось бы, поскольку оно было связано с personperson

 changed

Как мы могли бы затянуть этот чрезмерный огонь? Легко. Просто установите сеттер для каждого поля и отдельные события для каждого изменения. Подождите — с этим вы можете начать перестрелку, если хотите сразу изменить и person.events = {};

person.setData = function (data) {
$.extend(person, data);
$(person.events).trigger('changed');
};

$(person.events).on('changed', function () {
console.log('first name: ' + person.firstName);
});

person.setData({age: 38});
Вам нужно будет создать способ отложить ваши события до тех пор, пока оба изменения не будут завершены. Это звучит как работа, и я ленивый …

MobX на помощь

MobX — это простая, целенаправленная, производительная и ненавязчивая библиотека управления состоянием, разработанная Мишелем Вестстрате .

Из документов MobX:

Просто сделайте что-нибудь с государством, и MobX позаботится о том, чтобы ваше приложение учитывало изменения.

 age

Заметили разницу? firstName
Давайте снова посмотрим на этот пример var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 37,
fullName: function () {
this.firstName + ' ' + this.lastName;
}
});

 mobx.observable

Используя console.log

Если вы думаете, что это было аккуратно, проверьте это:

 mobx.autorun(function () {
  console.log('first name: ' + person.firstName);
});

person.age = 38; // prints nothing
person.lastName = 'RUBY!'; // still nothing
person.firstName = 'Matthew!'; // that one fired

Заинтригованный? Я знаю, вы.

Основные понятия MobX

наблюдаемый

 autorun

Запустить на CodePen

Наблюдаемые объекты MobX — это просто объекты. Я не наблюдаю ничего в этом примере. В этом примере показано, как вы можете начать работать с MobX в существующей кодовой базе. Просто используйте mobx.autorun(function () {
console.log('Full name: ' + person.fullName);
});

person.age = 38; // print's nothing
person.lastName = 'RUBY!'; // Fires
person.firstName = 'Matthew!'; // Also fires
var log = function(data) {
$('#output').append('<pre>' +data+ '</pre>');
}

var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 34
});

log(person.firstName);

person.firstName = 'Mike';
log(person.firstName);

person.firstName = 'Lissy';
log(person.firstName);

автозапуск

 mobx.observable()

Запустить на CodePen

Вы хотите что-то сделать, когда ваши наблюдаемые значения изменятся, верно? Позвольте мне ввести mobx.extendObservable() Обратите внимание на приведенный выше пример, как var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 0
});

mobx.autorun(function () {
log(person.firstName + ' ' + person.age);
});

// this will print Matt NN 10 times
_.times(10, function () {
person.age = _.random(40);
});

// this will print nothing
_.times(10, function () {
person.lastName = _.random(40);
});
autorun()

вычисленное

 autorun()

Запустить на CodePen

Посмотрите, что функция agevar person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 0,
get fullName () {
return this.firstName + ' ' + this.lastName;
}
});
log(person.fullName);

person.firstName = 'Mike';
log(person.fullName);

person.firstName = 'Lissy';
log(person.fullName);
MobX автоматически создаст вычисленное значение для вас. Это одна из моих любимых функций MobX. Заметьте что-нибудь странное о fullName Посмотри снова. Это функция, и вы видите результаты, не вызывая ее! Обычно вы вызываете getperson.fullName Вы только что встретили своего первого добытчика JS .

Веселье на этом не заканчивается! MobX будет отслеживать зависимости вашего вычисленного значения на предмет изменений и запускаться только после их изменения. Если ничего не изменилось, будет возвращено кэшированное значение. Смотрите случай ниже:

 person.fullName()

Запустить на CodePen

Здесь вы можете видеть, что я person.fullNamevar person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 0,
get fullName () {
// Note how this computed value is cached.
// We only hit this function 3 times.
log('-- hit fullName --');
return this.firstName + ' ' + this.lastName;
}
});

mobx.autorun(function () {
log(person.fullName + ' ' + person.age);
});

// this will print Matt Ruby NN 10 times
_.times(10, function () {
person.age = _.random(40);
});

person.firstName = 'Mike';
person.firstName = 'Lissy';
person.fullNamefirstName Это один из способов, с помощью которых MobX может значительно ускорить ваше приложение.

БОЛЬШЕ!

Я не собираюсь больше переписывать потрясающую документацию MobX . Посмотрите документы, чтобы узнать больше способов работы и создания заметных объектов.

Положить MobX на работу

Прежде чем я надену тебя слишком много, давайте что-нибудь построим

Вот простой пример не-MobX человека, который будет печатать его полное имя всякий раз, когда он меняется.

Обратите внимание, как имя отображается 10 раз, хотя мы никогда не меняли имя или фамилию. Вы можете оптимизировать это со многими событиями или проверкой каких-либо измененных полезных данных. Это слишком много работы.

Вот тот же пример, созданный с использованием MobX:

Обратите внимание, что нет lastNameeventstrigger С MobX вы имеете дело с последней ценностью и фактом, что она изменилась. Заметьте, как он рендерился один раз? Это потому, что я не изменил ничего, что смотрел on

Давайте создадим что-то чуть менее тривиальное:

 autorun

Запустить на CodePen

Здесь мы можем редактировать весь объект person и автоматически просматривать вывод данных. Теперь в этом примере есть несколько уязвимых мест, в частности, что входные значения не синхронизированы с объектом person. Давайте исправим это:

 // observable person
var person = mobx.observable({
  firstName: 'Matt',
  lastName: 'Ruby',
  age: 37
});

// reduce the person to simple html
var printObject = function(objectToPrint) {
  return _.reduce(objectToPrint, function(result, value, key) {
    result += key + ': ' + value + '<br/>';
    return result;
  }, '');
};

// print out the person anytime there's a change
mobx.autorun(function(){
  $('#person').html(printObject(person));
});

// watch all the input for changes and update the person
// object accordingly.
$('input').on('keyup', function(event) {
  person[event.target.name] = $(this).val();
});

Запустить на CodePen

Я знаю, у тебя есть еще одно замечание: «Рубин, ты закончил рендеринг!» Ты прав. Здесь вы видите, почему многие люди решили использовать React. React позволяет вам легко разбить вывод на небольшие компоненты, которые могут отображаться по отдельности.

Для полноты, вот пример jQuery, который я оптимизировал .

Буду ли я делать что-то подобное в реальном приложении? Возможно нет. Я бы использовал React в любой день, если бы мне был нужен этот уровень детализации. Когда я использовал MobX и jQuery в реальных приложениях, я использую функции mobx.autorun(function(){
$('#person').html(printObject(person));
// update the input values
_.forIn(person, function(value, key) {
$('input[name="'+key+'"]').val(value);
});
});
autorun()

Вы сделали это так далеко, так что вот тот же пример, построенный с React и MobX

Давайте создадим слайд-шоу

Как бы вы представили состояние слайд-шоу?
Начнем с отдельной фабрики слайдов:

 var slideModelFactory = function (text, active) {
  // id is not observable
  var slide = {
    id: _.uniqueId('slide_')
  };

  return mobx.extendObservable(slide, {
    // observable fields
    active: active || false,
    imageText: text,
    // computed
    get imageMain() {
      return 'https://placeholdit.imgix.net/~text?txtsize=33&txt=' + slide.imageText + '&w=350&h=150';
    },
    get imageThumb() {
      return 'https://placeholdit.imgix.net/~text?txtsize=22&txt=' + slide.imageText + '&w=400&h=50';
    }
  });
};

У нас должно быть что-то, что объединит все наши слайды. Давайте построим это сейчас:

 var slideShowModelFactory = function (slides) {
  return mobx.observable({
    // observable
    slides: _.map(slides, function (slide) {
      return slideModelFactory(slide.text, slide.active);
    }),
    // computed
    get activeSlide() {
      return _.find(this.slides, {
        active: true
      });
    }
  });
};

Слайд-шоу живет! Это более интересно, потому что у нас есть наблюдаемый массив слайдов, который позволит нам добавлять и удалять слайды из коллекции и соответственно обновлять наш интерфейс. Затем мы добавляем вычисленное значение activeSlide

Давайте сделаем наше слайд-шоу. Мы еще не готовы к выводу HTML, поэтому будем просто печатать на консоль.

 var slideShowModel = slideShowModelFactory([
  {
    text: 'Heloo!',
    active: true
  }, {
    text: 'Cool!'
  }, {
    text: 'MobX!'
  }
]);

// this will output our data to the console
mobx.autorun(function () {
  _.forEach(slideShowModel.slides, function(slide) {
    console.log(slide.imageText + ' active: ' + slide.active);
  });
});

// Console outputs:
// Heloo! active: true
// Cool! active: false
// MobX! active: false

Круто, у нас есть несколько слайдов, и autorun Давайте изменим слайд или два:

 slideShowModel.slides[1].imageText = 'Super cool!';
// Console outputs:
// Heloo! active: true
// Super cool! active: false
// MobX! active: false

Похоже, наш autorun Если вы измените что-нибудь, что смотрит autorun Давайте изменим наш выходной вывод из консоли на HTML:

 var $slideShowContainer = $('#slideShow');
mobx.autorun(function () {
  var html = '<div class="mainImage"><img src="' 
           + slideShowModel.activeSlide.imageMain 
           + '"/></div>';

  html += '<div id="slides">';
  _.forEach(slideShowModel.slides, function (slide) {
    html += '<div class="slide ' + (slide.active ? ' active' : '') 
         + '" data-slide-id="' + slide.id + '">';
    html += '<img src="' + slide.imageThumb + '"/>'
    html += '</div>';
  });
  html += '</div>';
  $slideShowContainer.html(html);
});

Теперь у нас есть основы этого слайд-шоу, однако интерактивности пока нет. Вы не можете нажать на миниатюру и изменить основное изображение. Но вы можете легко изменить текст изображения и добавить слайды с помощью консоли:

 // add a new slide
slideShowModel.slides.push(slideModelFactory('TEST'));
// change an existing slide's text
slideShowModel.slides[1].imageText = 'Super cool!';

Давайте создадим наше первое и единственное действие, чтобы установить выбранный слайд. Мы должны будем изменить slideShowModelFactory

 // action
setActiveSlide: mobx.action('set active slide', function (slideId) {
  // deactivate the current slide
  this.activeSlide.active = false;
  // set the next slide as active
  _.find(this.slides, {id: slideId}).active = true;
})

Зачем использовать action Отличный вопрос! Действия MobX не требуются, как я показал в других моих примерах по изменению наблюдаемых значений.

Действия помогут вам несколькими способами. Во-первых, все действия MobX выполняются в транзакциях. Это означает, что наш autorun Подумайте об этом на секунду. Что случилось бы, если бы я попытался деактивировать активный слайд и активировать следующий за пределами транзакции? Наш autorun Первый запуск был бы довольно неловким, так как не было бы активного слайда для отображения.

В дополнение к своей транзакционной природе действия MobX, как правило, упрощают отладку. Первым необязательным параметром, который я передал в свой mobx.action'set active slide' Эта строка может быть выведена с помощью API отладки MobX .

Итак, у нас есть действие, давайте подключим его использование с помощью jQuery:

 $slideShowContainer.on('click', '.slide', function () {
  slideShowModel.setActiveSlide($(this).data('slideId'));
});

Вот и все. Теперь вы можете нажимать на миниатюры, и активное состояние распространяется так, как вы ожидаете. Вот рабочий пример слайд-шоу:

Вот пример React того же слайд-шоу .

Заметьте, как я вообще не менял модель? Что касается MobX, React — это просто еще один источник ваших данных, например, jQuery или консоль.

Предостережения к примеру со слайд-шоу jQuery

Пожалуйста, обратите внимание, я не оптимизировал пример jQuery в любом случае. Мы закрываем весь DOM слайд-шоу при каждом изменении. Под клочками я подразумеваю, что мы заменяем весь HTML-код слайд-шоу при каждом клике. Если бы вы собирали надежное слайд-шоу на основе jQuery, вы, вероятно, настроили бы DOM после первоначального рендеринга, установив и удалив активный класс и изменив атрибут srcmainImage<img>

Хотите узнать больше?

Если у меня возникнет аппетит, чтобы узнать больше о MobX, ознакомьтесь с некоторыми другими полезными ресурсами ниже:

Если у вас есть какие-либо вопросы, пожалуйста, напишите мне в комментариях ниже или найдите меня на канале MobX Gitter .