React вышел на JSConf в мае 2013. AngularJS 1.0 появился в июне 2012, но не получил массовой аудитории до 2013. До того как эти фреймворки стали доминировать, команды, строящие сложные JavaScript-приложения, имели два серьёзных варианта: Backbone.js (октябрь 2010) и Knockout.js (июль 2010).
Мы использовали оба. Они решали разные задачи, имели разную философию и давали разный код.
Backbone.js: принеси свою структуру
Джереми Ашкенас построил Backbone на одной идее: JavaScript нуждался в минимальной структуре MVC без навязанных мнений фреймворка. Модели, Коллекции, Представления и Router - всё остальное решаешь сам.
// Backbone 0.9.x - модель Product
var Product = Backbone.Model.extend({
defaults: { name: '', price: 0, inStock: true },
urlRoot: '/api/products',
validate: function(attrs) {
if (!attrs.name || attrs.name.trim() === '') return 'Имя обязательно';
if (attrs.price < 0) return 'Цена не может быть отрицательной';
},
formattedPrice: function() {
return this.get('price').toFixed(2) + ' сом';
}
});
View-слой в Backbone был болезненным. Вы отвечали за:
- Рендеринг HTML
- Привязку событий
- Обновление DOM при изменении модели
- Очистку слушателей событий для предотвращения утечек памяти
var ProductView = Backbone.View.extend({
events: {
'click .btn-add-to-cart': 'onAddToCart',
},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
var template = _.template(
'<h3><%= name %></h3><p><%= formattedPrice() %></p>' +
'<% if (inStock) { %><button class="btn-add-to-cart">В корзину</button><% } %>'
);
this.$el.html(template(this.model.toJSON()));
return this;
},
// КРИТИЧНО: удаляем слушатели для предотвращения утечек памяти
remove: function() {
this.stopListening();
Backbone.View.prototype.remove.call(this);
}
});
Проблема утечки памяти была реальной. View, слушающий события модели через model.on('change', this.render, this) без последующего model.off, удерживал модель, ссылающуюся на View, а View удерживал свой DOM-элемент - даже после удаления элемента со страницы.
Knockout.js: привязка данных без MVC
Knockout взял противоположную философию - двусторонняя привязка данных через «наблюдаемые переменные»:
function ProductViewModel(data) {
var self = this;
self.name = ko.observable(data.name || '');
self.price = ko.observable(data.price || 0);
// Вычисляемое наблюдаемое - пересчитывается автоматически
self.formattedPrice = ko.computed(function() {
return self.price().toFixed(2) + ' сом';
});
self.addToCart = function() {
cart.add(self, 1);
};
}
<div data-bind="with: product">
<h3 data-bind="text: name"></h3>
<p data-bind="text: formattedPrice"></p>
<!-- ko if: inStock() -->
<button data-bind="click: addToCart">В корзину</button>
<!-- /ko -->
</div>
Когда price менялась, formattedPrice автоматически пересчитывалась, и DOM автоматически обновлялся. Никакой ручной работы с DOM. Никаких слушателей событий для управления.
Ключевые различия
| Функция | Backbone.js | Knockout.js |
|---|---|---|
| Философия | Минимальная структура | Магия привязки данных |
| Обновление DOM | Ручное, в render() |
Автоматическое через наблюдаемые |
| Отладка | Легко (обычный JS) | Сложно (граф зависимостей) |
| Лучше подходит | Сложные кастомные UI | Формы, CRUD |
Наши ошибки
Backbone: мы не соблюдали границы компонентов. View разрастался до 400 строк. Логика модели накапливалась. Гибкость, делающая Backbone простым в начале, делала его трудным в поддержке.
Knockout: цепочки наблюдаемых было сложно отлаживать. Вычисляемое наблюдаемое, зависящее от 5 других наблюдаемых, каждое из которых зависит от ещё 3, создавало граф зависимостей, который никто не мог удержать в голове.
Наследие
React появился в 2013 с виртуальным DOM и однонаправленным потоком данных. Он сделал ручное управление DOM в Backbone и магию наблюдаемых Knockout похожими на обходные пути для одной и той же проблемы: мутировать DOM трудно понимать. Ответ React - не мутировать DOM, описывать его желаемый вид, позволить фреймворку вычислить diff - был правильным ответом.
К 2015 году мы всё писали на React. К 2016 переносили старые Backbone-кодовые репозитории на React. К 2018 переписали каждый значимый Knockout-проект.
Но Backbone научил нас структуре и событийно-ориентированной архитектуре. Knockout научил реактивности и декларативному UI. Концепции выжили, хотя реализации были отправлены на покой.