В 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
. Результат будет тем же: вы можете в конечном итоге разделить состояние между всеми объектами, которые используют примесь.
ссылка на оригинал статьи https://habrahabr.ru/post/318096/