Эта статья была рецензирована Мишелем Вестстрате и Аароном Бойером. Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Если вы когда-нибудь писали нечто большее, чем очень простое приложение с jQuery, вы, вероятно, столкнулись с проблемой синхронизации различных частей пользовательского интерфейса. Часто изменения в данных должны отражаться в нескольких местах, и по мере роста приложения вы можете оказаться связанными узлами. Чтобы приручить безумие, обычно используют события, чтобы сообщить различным частям приложения, когда что-то изменилось.
Итак, как вы управляете состоянием вашего приложения сегодня? Я собираюсь выйти на конечность и сказать, что вы слишком подписались на изменения. Это верно. Я даже не знаю тебя, и я собираюсь вызвать тебя. Если вы не слишком подписаны, то я уверен, что вы работаете слишком усердно.
Если, конечно, вы не используете MobX …
Что такое «государство»?
Вот человек. Эй, это я! У меня есть имя, firstName
lastName
Кроме того, функция age
fullName()
Как бы вы сообщили своим различным выходным данным (просмотр, сервер, журнал отладки) об изменениях этого человека? Когда бы вы вызвали эти уведомления? До MobX я использовал сеттеры, которые запускали бы пользовательские события jQuery или js-сигналы . Эти варианты хорошо мне помогли, однако их использование было далеко не гранулированным. Я бы запустил одно «измененное» событие, если какая-либо часть объекта var person = {
firstName: 'Matt',
lastName: 'Ruby',
age: 37,
fullName: function () {
this.firstName + ' ' + this.lastName;
}
};
Допустим, у меня есть фрагмент кода, который показывает мое имя. Если бы я изменил свой возраст, это представление обновилось бы, поскольку оно было связано с person
person
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
Наблюдаемые объекты 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()
Вы хотите что-то сделать, когда ваши наблюдаемые значения изменятся, верно? Позвольте мне ввести 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()
Посмотрите, что функция age
var 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
Посмотри снова. Это функция, и вы видите результаты, не вызывая ее! Обычно вы вызываете get
person.fullName
Вы только что встретили своего первого добытчика JS .
Веселье на этом не заканчивается! MobX будет отслеживать зависимости вашего вычисленного значения на предмет изменений и запускаться только после их изменения. Если ничего не изменилось, будет возвращено кэшированное значение. Смотрите случай ниже:
person.fullName()
Здесь вы можете видеть, что я person.fullName
var 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.fullName
firstName
Это один из способов, с помощью которых MobX может значительно ускорить ваше приложение.
БОЛЬШЕ!
Я не собираюсь больше переписывать потрясающую документацию MobX . Посмотрите документы, чтобы узнать больше способов работы и создания заметных объектов.
Положить MobX на работу
Прежде чем я надену тебя слишком много, давайте что-нибудь построим
Вот простой пример не-MobX человека, который будет печатать его полное имя всякий раз, когда он меняется.
Обратите внимание, как имя отображается 10 раз, хотя мы никогда не меняли имя или фамилию. Вы можете оптимизировать это со многими событиями или проверкой каких-либо измененных полезных данных. Это слишком много работы.
Вот тот же пример, созданный с использованием MobX:
Обратите внимание, что нет lastName
events
trigger
С MobX вы имеете дело с последней ценностью и фактом, что она изменилась. Заметьте, как он рендерился один раз? Это потому, что я не изменил ничего, что смотрел on
Давайте создадим что-то чуть менее тривиальное:
autorun
Здесь мы можем редактировать весь объект 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();
});
Я знаю, у тебя есть еще одно замечание: «Рубин, ты закончил рендеринг!» Ты прав. Здесь вы видите, почему многие люди решили использовать 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 после первоначального рендеринга, установив и удалив активный класс и изменив атрибут src
mainImage
<img>
Хотите узнать больше?
Если у меня возникнет аппетит, чтобы узнать больше о MobX, ознакомьтесь с некоторыми другими полезными ресурсами ниже:
- MobX блоги, учебные пособия и видео
- Egghead.io курс: Управление сложным состоянием в приложениях React с MobX
- Практическая реакция с MobX
- Простые примеры MobX
- Справочник по MobX API
Если у вас есть какие-либо вопросы, пожалуйста, напишите мне в комментариях ниже или найдите меня на канале MobX Gitter .