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

Поделиться с друзьями
-->

Комментарии (3)


  1. RubaXa
    21.12.2016 13:38
    +2

    Ember тут не причем, типичная же проблема непонимания прототипов. Хотя они могли бы решить проблему на уровне реализации метода .extend, но это скользкая дорожка.


    1. justboris
      21.12.2016 16:39

      Эту проблему пытаются решить будущие class properties.
      Там инициализаторы свойств срабатывают для каждого объекта заново, и состояние не утекает


      1. RubaXa
        21.12.2016 16:45
        +1

        Повторю ещё раз, нет никакой проблемы, есть непонимание некоторых программистов, как работает наследование на прототипах. Сlass properties, как и сами классы, это просто сахар, который вместо определения свойств в прототипе, будет инициализировать их в конструкторе за вас.