В моей предыдущей статье я писал о том, как и почему неизменяемость . Я утверждал, что одна из областей, в которой неизменность действительно сияет, — это отслеживание изменений, одна из больших проблем в современных интерфейсных средах. В этой статье я приведу пример того, как неизменность можно использовать с React , библиотекой, разработанной Facebook.
Эта статья не будет обсуждать основы React. Если вам нужно введение в эту библиотеку, вы можете прочитать статью Введение в библиотеку React JavaScript .
Как использовать неизменяемость в React
В моей предыдущей статье я реализовал основную игровую логику для Minesweeper. В этой статье мы рассмотрим уровень пользовательского интерфейса. Есть несколько ошибок, которые мы должны учитывать при использовании объектов immutable.js с React. Во-первых, вы не можете передать неизменяемую карту или список непосредственно компоненту React, например так:
var data = Immutable.Map();
React.render(MyComponent(data), element);
Причина, по которой это не работает, заключается в том, что React копирует содержимое этого свойства объекта по свойству и объединяет его с существующим props
Это означает, что React не получит неизменный экземпляр. Он также не получит данные, содержащиеся на карте, потому что он не отображается как свойства объекта — у вас есть доступ к данным с помощью метода get()
Решение простое:
var data = Immutable.Map();
React.render(MyComponent({data: data}), element);
В этой статье все компоненты будут работать с неизменяемыми данными, поэтому мы создадим небольшую оболочку, чтобы избежать повторения этого снова и снова:
function createComponent(def) {
var component = React.createFactory(React.createClass(def));
return function (data) {
return component({data: data});
};
}
С этим компонентом мы можем забыть об обёртке, пока нам не понадобится доступ к ней в функции render()
this.props.data
Наши компоненты будут определять только функцию render()
function createComponent(render) {
var component = React.createFactory(React.createClass({
render: function () {
return render(this.props.data);
}
}));
return function (data) {
return component({data: data});
};
}
Благодаря этому определение и использование компонентов, которые работают с неизменяемыми данными, очень просто:
var div = React.DOM.div;
var Tile = createComponent(function (tile) {
if (tile.get('isRevealed')) {
return div({className: 'tile' + (tile.get('isMine') ? ' mine' : '')},
tile.get('threatCount') > 0 ? tile.get('threatCount') : '');
}
return div({
className: 'tile'
}, div({className: 'lid'}, ''));
});
Если плитка раскрыта, мы делаем мину, если она есть; в противном случае мы визуализируем количество близлежащих мин, за исключением случаев, когда это число равно 0. Если мина не обнаружена, мы просто визуализируем ее с «крышкой», которая в CSS будет выглядеть как кликабельная плитка.
Остальные компоненты React также просты. Есть еще одна загвоздка, о которой нужно знать. Реактивные компоненты могут принимать массив дочерних компонентов. Мы должны убедиться, что неизменяемые списки преобразованы в массивы, прежде чем передать их в React:
var Row = createComponent(function (tiles) {
return div({className: 'row'}, tiles.map(Tile).toJS());
});
Вызов map()
toJS()
Эти и другие компоненты пользовательского интерфейса можно полностью увидеть в этом коде, где вы также можете поиграть в игру.
Ускорение вещей
В моей предыдущей статье я упоминал, что отслеживание изменений может быть значительно улучшено, потому что мы можем закорачивать дорогой алгоритм сравнения в таких библиотеках, как React. Когда вы даете React новые данные, он вызывает shouldComponentUpdate()
Если эта функция возвращает false
Это потенциально экономит много работы и может привести к значительному улучшению производительности.
Давайте рассмотрим нашу игру. Когда вы открываете плитку, вся игра отображается заново. Однако, благодаря нашей неизменной модели данных, все плитки, которые не изменились, по-прежнему будут такими же точными ссылками. Их не нужно повторно отображать, потому что с неизменяемыми данными одна и та же ссылка означает отсутствие изменений. Чтобы сообщить React об этой детали, мы можем улучшить нашу оболочку компонента следующим образом:
function createComponent(render) {
var component = React.createFactory(React.createClass({
shouldComponentUpdate: function (newProps, newState) {
// Simplified, this app only uses props
return newProps.data !== this.props.data;
},
render: function() {
return render.call(this, this.props.data);
}
}));
return function (data) {
return component({data: data});
};
}
Этого простого кода достаточно, чтобы наше приложение не было значительно медленнее, чем приложение, основанное на изменяемых данных, и почти вдвое быстрее. Вот как много влияет эффективное отслеживание изменений! Эта улучшенная версия также доступна на CodePen . Если вы хотите взглянуть поближе на некоторые цифры, есть также репозиторий GitHub , который включает в себя несколько грубых тестов и несколько реализаций.
Уменьшение сложности
Возможно, самое большое преимущество неизменяемых данных заключается в том, как они уменьшают случайную сложность. Изменчивые данные по своей природе более сложны, чем неизменяемые, потому что они запутывают состояние и время. В изменчивых данных время является встроенным фактором. Фактически, доступ к значению в два разных момента времени может дать вам два разных значения. Неизменные данные, с другой стороны, не имеют этой функции. Если вы извлекаете значение неизменяемых данных в два разных момента времени, это гарантирует, что вы получите одно и то же значение. Это заставляет нас занять более осознанную позицию в отношении того, как наши данные меняются со временем.
С неизменяемыми данными некоторые виды функций, которые трудно или даже невозможно с изменяемыми данными, становятся тривиальными для реализации. Одним из примеров такой функции является отмена всего приложения. Если состояние вашего приложения может быть представлено с неизменным значением, реализация отмены — это сохранение списка версий состояния приложения и предоставление кнопки для сброса состояния приложения до предыдущей версии.
Давайте добавим функцию «отменить» в наш проект Minesweeper. Всякий раз, когда пользователь щелкает плитку, мы получаем новый игровой объект для рендеринга. Чтобы поддержать эту функцию, мы будем держать старые. Код для реализации этой функции приведен ниже:
var newGame = revealTile(game, tile);
if (newGame !== game) {
gameHistory = gameHistory.push(newGame);
game = newGame;
}
render();
Затем есть кнопка «Отменить»:
var UndoButton = createComponent(function () {
return React.DOM.button({
onClick: function () {
channel.emit('undo');
}
}, 'Undo');
});
Объект channel
Если мы не исчерпали историю, мы выскакиваем последнюю версию и восстанавливаем предпоследнюю версию в качестве текущего состояния и заново визуализируем игру. Это делается с помощью следующего кода:
channel.on('undo', function () {
if (history.size > 1) {
gameHistory = gameHistory.pop();
game = gameHistory.last();
render();
}
});
Очень просто, не правда ли? Можете ли вы даже представить, как бы вы делали нечто подобное в приложении, состояние которого состоит из изменяемых объектов? Сыграйте в Minesweeper, отменив (это в основном обман, но эй …), воспользовавшись подготовленным мною CodePen или демонстрацией ниже:
Помните, что я упоминал «структурное разделение» в предыдущей статье? Структурное совместное использование означает, что при сохранении двух почти идентичных версий состояния приложения фактически не будет храниться две копии одного и того же состояния. Это снова связано с тем, что неизменяемые данные не изменяются. Если бы вы решили проблему «отменить» с изменяемым состоянием, вы бы сохранили мутации в некотором роде, чтобы соответствовать неизменяемому решению с точки зрения использования памяти. В противном случае, вы бы в конечном итоге хранили кучу полноценных копий, которые не будут хорошо масштабироваться.
Снимки приложений
Вы можете подумать, что большинство приложений не выиграют от использования функции «отменить». Но есть и другие варианты использования, которые были бы интересны. Например, если все состояние вашего приложения хранится в одном неизменяемом значении, вы можете так же легко добавить моментальный снимок в свое приложение. Для чего это будет полезно? Ну, сохраняя текущее состояние произвольно сложного интерфейса для одного. Отладка это другое.
Представьте себе, что вместо того, чтобы получить отчет об ошибке с кучей плохо описанных шагов для воспроизведения, вы получили строку JSON, которую вы можете выгрузить в свой браузер, чтобы раскрутить приложение в том же состоянии, в котором вы его использовали? Это еще один пример того, что было бы тривиально добавить в приложение, состояние которого хранится в неизменяемом значении.
Выводы
Я загрузил исходный код в ранее упомянутую реализацию Minesweeper в GitHub, где вы можете погрузиться в детали. Вы найдете реализацию, использующую как изменяемые, так и неизменные структуры данных, а также инструкции по выполнению простых тестов производительности. Теперь приступайте к внедрению неизменности в ваших приложениях!