Ember 1.0 RC8发布


Ember 1.0 RC8是1.0正式版前的最后一次发布,如果一切顺利本周末将发布Ember 1.0正式版。

在RC8中做了一些1.0正式版之前的重大更改,这些更改对应用代码只有很少的影响,不过带来了很大的性能提升。

这些更改都与观察器有关。如果编码的时候使用了大量的观察器,那么可能意味着代码不符合语言习惯。通常只在与其他不支持绑定机制的库桥接的时候需要使用观察器。

例如,如果编写一个包裹jQuery UI控件的组件时,可能需要使用观察器来监视组件上的改变,并将这些改变反映到控件上。

在应用代码中,应该尽可能的使用计算属性。

声明事件监听器

现在Ember提供了一种方法来用声明的方式给Ember类添加事件监听器。这比手动在init中设置监听器要简单很多。

以前的方法:

1
2
3
4
5
6
7
8
9
10
11
App.Person = DS.Model.extend({
  init: function() {
    this.on('didLoad', this, function() {
      this.finishedLoading();
    });
  },

  finishedLoading: function() {
    // do stuff
  }
});

现在只需要:

1
2
3
4
5
App.Person = DS.Model.extend({
  finishedLoading: function() {
    // do stuff
  }.on('didLoad')
});

数组计算

现在有一种符合惯例和稳定的方法来构建一个基于数组的计算属性,其将只对更新的部分进行计算。

例如,当有一组people时,并想通过一个计算属性返回他们的年龄。

当前最简单的实现方法是:

1
2
3
4
5
App.Person = Ember.Object.extend({
  childAges: function() {
    return this.get('children').mapBy('age');
  }.property('children.@each.age')
});

这样实现非常简洁,但是当数组中任何时候有一个元素被添加或者删除时,就会重新计算整个数组。对于小的数组来说,这可能不成问题。然而,如果数组非常巨大,或者这些计算属性被链式使用,或者用于完成一些繁重的工作,那么开销将会非常大。

这时可以使用数组计算属性性:

1
2
3
App.Person = Ember.Object.extend({
  childAges: Ember.computed.mapBy('children', 'age')
});

可以将数组计算属性链接在一起:

1
2
3
4
App.Person = Ember.Object.extend({
  childAges: Ember.computed.mapBy('children', 'age'),
  maxChildAge: Ember.computed.max('childAges')
});

当一个元素被添加或者删除时,计算只进行一次。在本例中,如果添加一个孩子,那他的年龄会被追加到childAges中,并且如果这个年龄大于maxChildAge,那么maxChildAge也将得到更新。

这些计算属性总是保持同步、高效,并完全由Ember来管理。

Ember扩展

经过几个月的测试,及Teddy Zeenny的辛勤付出,Ember Inspector已经准备发布到Chrome Web Store了。

最近,Teddy添加了对加载的数据的支持。已经支持Ember Data,Ember Model的支持也在开发中。

Teddy完成了对象检查器的重大改进,增加了对组属性对象的支持(如:Ember Data模型的属性、一对多关联),支持通过检查器修改对象。

通过检查器可以查看应用中所有路由的列表,这些命名可以与对象结合使用。这样更容易记住命名惯例。

另外,视图树通过应用模板关联的控制器和模型显示了应用的概况。

其他改进

  • 改进yield,确保其总是yield回调用的上下文。
  • 不使用W3C range API来改进range更新的性能
  • 完成1.0文档的审查
  • 通过<script>重复定义同名的模板时给出更加友好的错误消息
  • ApplicationController中添加currentRouteName,可以用于link-totransitionTo
  • 定义新的别名:linkTo -> link-tobindAttr -> bind-attr,来保持与html命名一致。老命名还保留不过已经软废除。

更新 TL;DR

观察器在构造过程中不触发

以前通过create传入或者在prototype上指定的属性不会触发观察器,但在init中通过set方法设置的属性会触发。

现在观察器直到init完后才会触发观察器。

如果在初始化过程中需要出发一个观察器,不能通过set来实现,需要在观察器上通过.on('init')指定其在init下也工作。

1
2
3
4
5
6
7
8
9
App.Person = Ember.Object.extend({
  init: function() {
    this.set('salutation', "Mr/Ms");
  },

  salutationDidChange: function() {
    // some side effect of salutation changing
  }.observes('salutation').on('init')
});
没有消费的计算属性不触发观察器

如果从未get一个计算属性,与它相关的观察器不会被触发,即使其依赖的键已经改变。可以想象为值从一个未知的值变成了另一个。

这基本上不会影响到应用程序代码,因为计算属性几乎总是在其取来的时候同时被观察到。例如,获取一个计算属性的值,将其放置到DOM中,(或者通过D3绘图),接着观察它,以便在其更新的时候更新DOM。

如果需要观察一个不需要立即使用的计算属性,可以在init方法中get它一下。

路由、控制器和视图的新操作哈希

为了保持一致性并提供更灵活的操作命名,通过一个统一的actions哈希来定义操作。当继承一个定义了actions的类时,会在子类将定义的actions进行合并,或在父进行实例化。另外还支持_super,因此没有散失任何的灵活性。

之前的行为依然支持,不过已经废除了。如果一个控制器代理了一个拥有actions属性的模型,那么将自动将其重命名为_actions来避免发生任何可能的冲突。

在Handlebars助手中使用引号引起来的字符串

过去,Handlebars助手没有严格限制字符串是否需要加引号。不幸的是,这意味着没有办法区分字符串值和属性路径。现在严格限制如果希望是一个字符串值的话,必须加引号。这意味着link-to的路由名必须用引号引起来。相反,如果定义一个自定义的绑定助手,并且用一个引号引起来的字符表示属性路径,这将不再工作。加引号表示字符串,不加表示路径。

init中设置属性

当前,传递一个哈希给create和在init中设置同样的属性是不一致的。

1
2
3
4
5
6
7
App.Person = Ember.Object.extend({
  firstNameDidChange: function() {
    // this observer does not fire
  }.observes('firstName')
});

App.Person.create({ firstName: "Tom", lastName: "Dale" });

本例中,由于所有属性都是通过一个传给create的hash来设置的,观察器不会被触发。

下面看一看在RC7中在init方法里完成同样的出发会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// WARNING: OLD BEHAVIOR

App.Person = Ember.Object.extend({
  init: function() {
    if (!this.get('firstName')) {
      this.set('firstName', "Tom");
    }
  },
  firstNameDidChange: function() {
    // this observer fires
  }.observes('firstName')
});

App.Person.create({ lastName: "Dale" });

在此,如果firstName没有,也会出发观察器。

新的设计对象模型只在构造后会触发观察器,这是为什么create不触发的原因。

此外,因为如果为包含数组、对象值的属性进行初始化只能在init中,这导致了不一致性:

1
2
3
4
5
6
7
8
9
10
11
12
// WARNING: OLD BEHAVIOR

App.Person = Ember.Object.extend({
  // initial property value, does not trigger an initialization observer
  salutation: "Mr.",

  init: function() {
    // also initial property value, triggers an observer on
    // initialization
    this.set('children', []);
  }
});

总之,属性在初始化过程中被设置不论是否设置到prototype,或作为哈希传给create,或在init中被设置,都不触发观察器。

如果有代码需要不论是在初始化过程还是当一个属性改变时执行,必须使用.on('init')将其标记为需要在初始化过程执行。这种情况最好是进行重构,来避免initset的负面效应。

1
2
3
4
5
App.Person = Ember.Object.extend({
  firstNameDidChange: function() {
    // some side effect that happens when first name changes
  }.observes('firstName').on('init')
});

计算属性性能改进

最新发布的Ember.js版本包含了观察器和计算属性交互的更新。这对依赖旧有行为的应用是一个破坏性的更新。

为了理解这个更新,通过一个计算属性的例子来介绍。假设尝试用Ember.js对象来为Schrödinger's famous cat 建模。

1
2
3
4
5
6
7
App.Cat = Ember.Object.extend({
  isDead: function() {
    return Math.rand() > 0.5;
  }.property()
});

var cat = App.Cat.create();

给定一个猫的对象,判断猫是死是活?这里通过一个随机数来决定。在观察猫对象之前,可以说猫既是死的又是活的,或者要死不活。

而实际上,并非猫超凡脱俗,而是取决于第一次调用。

1
2
3
cat.get('isDead');
// true
// …or false, half the time

在询问了猫对象的isDead属性后,就可以明确的说猫是死还是活。但是在此之前,这个计算属性的值并不存在。

下面看看在混合(Mix)中的观察器。如果计算属性的值还不存在,其依赖的键发生改变时是否触发观察器?

在之前版本的Ember.js中,答案是肯定的。例如:

1
2
3
4
5
6
7
8
9
10
11
App.Person = Ember.Object.extend({
  observerCount: 0,

  fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName'),

  fullNameDidChange: function() {
    this.incrementProperty('observerCount');
  }.observes('fullName')
});

依赖的任意键发生改变,都会触发观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// WARNING: OLD BEHAVIOR DO NOT RELY ON THIS

var person = App.Person.create({
  firstName: "Yehuda",
  lastName: "Katz"
});

person.get('observerCount'); // => 0

person.set('firstName', "Tomhuda");
person.get('observerCount'); // => 1

person.set('lastName', "Katzdale");
person.get('observerCount'); // => 2

然后,因为fullName属性并不"exist"直到请求它为止,触发一个观察者是否是正确的行为并不明确。

一个影响计算属性的关联问题是如果计算属性依赖键包含一个路径。(请记住依赖键只是定义一个计算属性时,传递个.property()方法的属性名。

例如,假设构造一个模型表示一篇博客,如果需要使用博客的评论,采用延迟加载的方式加载评论(例如在模板中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
App.BlogPost = Ember.Object.extend({
  comments: function() {
    var comments = [];
    var url = '/post/' + this.get('id') + '/comments.json');

    $.getJSON(url).then(function(data) {
      data.forEach(function(comment) {
        comments.pushObject(comment);
      });
    });

    return comments;
  }.property()
});

在这里跟预期的行为一样,博文的评论只会在第一次使用post.get('comments')或者在模板中使用的时候,才通过网络去加载:

1
2
3
4
5
<ul>
{{#each comments}}
  <li>{{title}}</li>
{{/each}}
</ul>

然而,现在希望添加一个计算属性,用来从加载的评论中选择第一条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
App.BlogPost = Ember.Object.extend({
  comments: function() {
    var comments = [];
    var url = '/post/' + this.get('id') + '/comments.json';

    $.getJSON(url).then(function(data) {
      data.forEach(function(comment) {
        comments.pushObject(comment);
      });
    });

    return comments;
  }.property(),

  firstComment: function() {
    return this.get('comments.firstObject');
  }.property('comments.firstObject')
});

现在有了一个问题!因为firstComment计算属性依赖comments.firstObject,为了建立一个firstObject的观察器,它将get()comments属性。

在此添加这个计算属性意味着应用中所有博文的评论都被加载,无论评论是否被使用!

为了决定如何处理,花了一些时间分析实际的Ember.js应用。发现这个行为严重的影响了性能。

  1. 触发没有物化的计算属性的观察器意味着需要在一开始的时候为所有计算属性设置监听器,而不是在第一次计算的时候。
  2. 许多计算属性因为路径依赖键,从未使用,也未被计算。

为了修正这些问题,RC8做了一下改变

  1. 观察一个计算属性的观察器只在该属性被使用过至少一次后才会被触发。
  2. 观察一个路径("foo.bar.baz"),或者使用一个路径作为一个依赖主键,将不导致路径任意部分从未计算变为计算。

大部分Ember.js应用程序不会受此影响,因为:

  1. 大部分应用程序观察计算属性,并且在对象初始化时就get()这些属性,因而触发了正确的行为。
  2. 对于计算属性依赖键,新行为正是开发者所期待的。

如果应用受这个更改的影响,修正方法非常简单,只需要在类的init方法中get()计算属性即可。

例如,为了更新上述的观察器例子,能通过"precomputing"fullName属性来保持RC8之前版本的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
App.Person = Ember.Object.extend({
  init: function() {
    this.get('fullName');
    this._super();
  },

  observerCount: 0,

  fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName'),

  fullNameDidChange: function() {
    this.incrementProperty('observerCount');
  }.observes('fullName')
});

link-to助手(之前为linkTo)现在将未加引号的参数(非数字参数)作为绑定属性路径对待,这意味着当一个传给link-to的属性改变时,链接的href将改变。这包括第一个参数(目标路由名)和接着的任意上下文参数。

下面的模板例子将在当前上下文(通常是一个控制器)查找destinationRoute,并使用其来决定链接的href和点击链接将切换至的路由。

1
{{#link-to destinationRoute}}Link Text{{/link-to}}

下面的例子将一直指向articles.show路由(因为路由名称参数加了引号),但是当article的值改变时,链接的href将更新为对应article新值的URL。

1
{{#link-to 'articles.show' article}}Read More...{{/link-to}}

如果之前写的应用没有正确的区分加引号的字符串和属性路径,这可能导致一些问题。因此当升级到RC8时,需要确定所有link-to参数的静态字符串都正确的添加了引号。

绑定助手:加引号的字符串,数字和路径

调用自定义的绑定助手(如通过Ember.Handlebars.helper定义的)时传入加引号的字符串或者原生数字会将其原值直接传入,而不是将所有的都作为绑定属性的路径,每次在属性变化的时候都重新渲染助手。

1
2
3
4
5
Pass the string 'hello' to myHelper:
{{myHelper 'hello'}}

Pass the property pointed-to by the path 'hello' to myHelper:
{{myHelper hello}}

如果之前调用绑定助手时传入加引号的字符串,并期望其是一个绑定属性路径,那么会导致应用出现一些问题。因此需要确定只有当确实需要传入一个字符串的时候才给参数加引号,而不是路径对应的值。 如果之前写的应用没有正确的区分加引号的字符串和属性路径,这可能导致一些问题。因此当升级到RC8时,需要确定所有link-to参数的静态字符串都正确的添加了引号。


博客评论基于Disqus