[Перевод] Ember — Лучшие практики: Избегайте утечки состояния внутрь фабрики
В DockYard, мы много времени уделяем Ember, от построения web приложений, создания и поддержки аддонов, до вклада в экосистему Ember. Мы надеемся поделиться некоторым опытом, который мы приобрели, через серию постов, которые будут посвящены лучшим практикам Ember, паттернам, антипаттернам и распространённым ошибкам. Это первый пост из данной серии, так что давайте начнём, вернувшись к основам Ember.Object
.
Ember.Object
это одна из первых вещей, которую мы узнаём, как разработчики Ember, и неудивительно. Практически каждый объект, с которым мы работаем в Ember, будь то маршрут (Route), компонент (Component), модель (Model), или сервис (Service), наследуется от Ember.Object. Но время от времени, я вижу как его неправильно используют:
export default Ember.Component.extend({
items: [],
actions: {
addItem(item) {
this.get('items').pushObject(item);
}
}
});
Для тех, кто сталкивался с этим раньше, проблема очевидна.
Ember.Object
Если вы посмотрите API и отмените выбор всех Inherited (Наследуемых), Protected (Защищённых), и Private (Приватных) опций, вы увидите, что Ember.Object
не имеет собственных методов и свойств. Исходный код не может быть короче. Это буквального расширение Ember CoreObject
, с примесью Observable
:
var EmberObject = CoreObject.extend(Observable);
CoreObject
обеспечивает чистый интерфейс для определения фабрик или классов. Это, по существу, абстракция вокруг того, как вы обычно создаёте функцию конструктор, определяя методы и свойства на прототипе, а затем создавая новые объекты с помощью вызова new SomeConstructor()
. За возможность вызывать методы суперкласса, используя this._super()
, или объединять набор свойств в класс через примеси, вы должны благодарить CoreObject
. Все методы, которые часто приходиться использовать с Ember objects, такие как init
, create
, extend
, или reopen
, определяются там же.
Observable
это примесь (Mixin), которая позволяет наблюдать за изменениями свойств объекта, а также в момент вызова get
и set
.
При разработке Ember приложений, вам никогда не приходиться использовать CoreObject
. Вместо этого вы наследуете Ember.Object
. В конце концов, в Ember самое важное реакция на изменения, так что вам нужны методы с Observable
для обнаружения изменения значений свойств.
Объявление нового класса
Вы можете определить новый тип наблюдаемого объекта путем расширения Ember.Object
:
const Post = Ember.Object.extend({
title: 'Untitled',
author: 'Anonymous',
header: computed('title', 'author', function() {
const title = this.get('title');
const author = this.get('author');
return `"${title}" by ${author}`;
})
});
Новые объекты типа Post
теперь могут быть созданы путём вызова Post.create()
. Для каждой записи будут наследоваться свойства и методы, объявленные в классе Post
:
const post = Post.create();
post.get('title'); // => 'Untitled'
post.get('author'); // => 'Anonymous'
post.get('header'); // => 'Untitled by Anonymous'
post instanceof Post; // => true
Вы можете изменить значения свойств и дать посту название и имя автора. Эти значения будут установлены на экземпляре, а не на классе, поэтому не повлияют на посты, которые будут созданы.
post.set('title', 'Heads? Or Tails?');
post.set('author', 'R & R Lutece');
post.get('header'); // => '"Heads? Or Tails?" by R & R Lutece'
const anotherPost = Post.create();
anotherPost.get('title'); // => 'Untitled'
anotherPost.get('author'); // => 'Anonymous'
anotherPost.get('header'); // => 'Untitled by Anonymous'
Поскольку обновление свойств таким образом не влияет на другие инстансы, легко подумать, что все операции, выполненные в примере безопасны. Но остановимся на этом этом немного больше.
Утечка состояния внутрь класса
Пост может иметь дополнительный список тегов, так что мы можем создать свойство с именем tags
и по умолчанию это пустой массив. Новые теги могут быть добавлены при помощи вызова метода addTag()
.
const Post = Ember.Object.extend({
tags: [],
addTag(tag) {
this.get('tags').pushObject(tag);
}
});
const post = Post.create();
post.get('tags'); // => []
post.addTag('constants');
post.addTag('variables');
post.get('tags'); // => ['constants', 'variables']
Похоже, что это работает! Но проверим, что происходит, после создания второго поста:
const anotherPost = Post.create();
anotherPost.get('tags'); // => ['constants', 'variables']
Даже если цель состояла в том, чтобы создать новый пост с пустыми тегами (предполагаемый по умолчанию), пост был создан с тегами из предыдущего поста. Поскольку новое значение для свойства tags
не было установлено, а просто мутировало основной массив. Так мы эффективно прокинули состояние в класс Post
, которое затем используется на всех экземплярах.
post.get('tags'); // => ['constants', 'variables']
anotherPost.get('tags'); // => ['constants', 'variables']
anotherPost.addTag('infinity'); // => ['constants', 'variables', 'infinity']
post.get('tags'); // => ['constants', 'variables', 'infinity']
Это не единственный сценарий, при котором вы можете спутать состояние экземпляра и состояние класса, но это, конечно, тот, который встречается чаще. В следующем примере, вы можете установить значение по умолчанию для createdDate
для текущей даты и времени, передав new Date()
. Но new Date()
вычисляется один раз, когда определяется класс. Поэтому независимо от того, когда вы создаете новые экземпляры этого класса, все они будут иметь одно и то же значение createdDate
:
const Post = Ember.Object.extend({
createdDate: new Date()
});
const postA = Post.create();
postA.get('createdDate'); // => Fri Sep 18 2015 13:47:02 GMT-0400 (EDT)
// Sometime in the future...
const postB = Post.create();
postB.get('createdDate'); // => Fri Sep 18 2015 13:47:02 GMT-0400 (EDT)
Как держать ситуацию под контролем?
Для того, чтобы избежать совместного использования меток между постами, свойство tags
необходимо будет установить, во время инициализации объекта:
const Post = Ember.Object.extend({
init() {
this._super(...arguments);
this.tags = [];
}
});
Так как init
вызывается всякий раз во время вызова Post.create()
, каждый пост экземпляра всегда получит свой собственный массив tags
. Кроме того, можно сделать tags
вычисляемым свойством (computed property):
const Post = Ember.Object.extend({
tags: computed({
return [];
})
});
Вывод
Теперь очевидно, почему вы не должны писать таких компонентов, как в примере из начала этого поста. Даже если компонент появляется только один раз на странице, когда вы выходите из маршрута, только экземпляр компонента уничтожается, а не фабрика. Так что когда вы вернётесь, новый экземпляр компонента будет иметь следы предыдущего посещения страницы.
Эта ошибка может встречаться при использовании примесей. Несмотря на то, что Ember.Mixin
это не Ember.Object
, объявленные в нём свойства и методы, примешиваюся к Ember.Object
. Результат будет тем же: вы можете в конечном итоге разделить состояние между всеми объектами, которые используют примесь.