Emberjs之ComputedProperty

Computed Property (简称CP)

1. What is CP?

In a nutshell, computed properties let you declare functions as properties. You create one by defining a computed property as a function, which Ember will automatically call when you ask for the property. You can then use it the same way you would any normal, static property.

App.Person = Ember.Object.extend({

  // these will be supplied by `create`

  firstName: null,

  lastName: null,



  fullName: function() {

    return this.get('firstName') + ' ' + this.get('lastName');

  }.property('firstName', 'lastName')

});



var ironMan = App.Person.create({

  firstName: "Tony",

  lastName:  "Stark"

});



ironMan.get('fullName'); // "Tony Stark"

这里fullName是一个CP,依赖于firstName,lastName.

另:CP也可以这样写,建议如下写法,避免“禁用”function的property扩展带来的问题。

[Prototype Extensions](http://emberjs.com/guides/configuring-ember/disabling-prototype-extensions/)
 fullName: Ember.computed('firstName', 'lastName', function() {

    return this.get('firstName') + ' ' + this.get('lastName');

  })

还可以这样写:

  // An array of Ember.Table.Row computed based on `content`

  bodyContent: Ember.computed(function() {

    return RowArrayController.create({

      target: this,

      parentController: this,

      container: this.get('container'),

      itemController: Row,

      content: this.get('content')

    });

  }).property('content.[]', '_reloadBody'),

Ember.js v1.12 更新带有Set和get方法的CP:

  fullName: Ember.computed("firstName", "lastName", {

    get: function() {

      return this.get("firstName") + " " + this.get("lastName");

    },

    set: function(key, newName) {

      var parts = newName.split(" ");

      this.setProperties({ firstName: parts[0], lastName: parts[1] });

      return newName;

    }

  });

 

2. CP中名词描述定义

2.1 CP:Computed Property。

2.2 CP属性:上文例子中的‘fullName’。

2.3 CP所依赖的源属性:上文例子中的‘firstName’、‘lastName’。

2.4 CP的回调方法:上文例子中的function(){......}方法。

2.5 CP属性的Setter/Getter:

 

3. CP重要原则(特性)

3.1 只有当获取或设置CP属性时,才可能会触发CP的回调方法,也就是说CP属性是属于‘懒加载’的方式(使用时加载的行为)。

3.2 当CP属性依赖于.property('person{name,age}')时,仅当person.name或person.age发生改变时改变,其余方式均不可以更新CP属性。

3.3 当CP属性依赖于.property('[email protected]')时, 以下四种情况会发生变化:

  • columns里面任何一个元素的isLoaded属性发生变化时,CP属性会重新计算(当CP属性被设置或获取时)。
  • columns增加元素或者删除子元素时,CP属性会重新计算(当CP属性被设置或获取时)。
  • columns数组本身被重新赋值,会触发CP属性重新计算(当CP属性被设置或获取时)。
  • columns数组元素中其他属性更改不会触发CP属性重新计算(当CP属性被设置或获取时)。
  • Note that @each only works one level deep. You cannot use nested forms like [email protected]or [email protected][email protected].

3.4 当CP属性依赖于.property('columns.@each')时,其行为会发生如下变化:

  • 当columns增加或删除元素时,CP属性会重新计算(当CP属性被设置或获取时)。
  • 当columns自身被替换或重新赋值时,CP属性会重新计算(当CP属性被设置或获取时)。

3.5 当CP属性依赖于.property('columns.[]')时,其行为会发生如下变化:

  与3.4 绑定.property('columns.@each') 行为相同。

3.6 当通过set方法设置CP属性时,然后调用get方法获取CP属性,则不调用CP回调,set时值被缓存。

3.7 对象在继承CP属性时,保持CP属性的独立性、互不干扰,并且重写后会改写CP依赖关系,变更依赖源。

3.8 当存在两个互相依赖的CP属性时,仅仅发生三次属性变更。

3.9 不要将CP的依赖属性附着在另一个CP属性上。

3.10 当CP属性依赖于对象列表时,例如.property('a.b.c.d')上时,节点上任意对象发生变化时,均会重新计算属性(当调用CP属性时)。

4. CP宏定义

总原则:CP依赖的属性发生改变后,当调用CP值时,会触发宏的重新计算(触发回调function)。

 

Ember.computed.empty: empty(属性名)返回bool

Ember.computed.not: not(属性名)返回bool

Ember.computed.alias:alias(属性名),双向绑定, alias不要依赖于一个CP.

Ember.computed.defaultTo: 如果CP属性为null,则读取依赖属性值一次

Ember.computed.match(属性名, 匹配字符串)

Ember.computed.gt(属性名,数字)大于返回bool

Ember.computed.gte(属性名,数字)大于或等于bool

Ember.computed.and(属性名,属性名) 并集

Ember.computed.or(属性名, 属性名) 交集

Ember.computed.collect( 数组 ) 匹配所有项,没有相则为null

Ember.computed.oneWay(属性名) 单方向从到PC属性. CP可以被设置,但不会影响到CP依赖的属性。

Ember.computed.readOnly(属性名) CP属性不允许被设置,但CP所依赖的源属性更新CP值。

 更多宏定义请参考这里:http://emberjs.com/api/classes/Ember.computed.html#method_alias

5. CP使用场景

5.1 在我们使用的对象上,希望使用一个属性值监听一个或多个属性的变更,或者CP属性强依赖于某些属性,而且还能缓存CP属性值,减少性能损耗。(CP特性请参考3.1)

5.2 CP可以依赖在一个对象的多个属性上, 特别是绑定在集合元素上甚至监听集合元素内部某一属性,但层次有限制。例如.property('person{name,age}')或.property('pencilBox.[]', [email protected]', penBox.@each)。(CP特性请参考3.2、3.3、3.4)

5.3 Ember.computed.alias作用于两个强关联对象的双向绑定,并提供缓存机制。

5.4 通过CP来组合属性,CP属性回调中不能有边界效应等循环、异步方法。

 

6. Q & A

6.1 当计算属性(CP)设置为getter和setter时,其CP回调函数触发的场景:

  •   设置(set)CP属性时,无论何时都触发CP回调方法。
  •   当获取CP属性时,若CP依赖属性未发生变化,则不执行CP回调方法。

 

6.2 Ember.Computed(function(){}).property('xxxx') 与 function(){}.property('xxxx')区别:

  • 若Ember.computed('firstName', function(){.....}).property('lastName'),改变firstName的值不会出发CP回调方法(当调用CP属性时),而相反,当lastName更新时会触发CP回调方法(当调用CP属性时)。
  • 前者可以用{get:function(){}, set: function(key, newName){}}的方式,不必在判断Argument.length,实现了get、set的语法糖。
  fullName: Ember.computed("firstName", "lastName", {

    get: function() {

      return this.get("firstName") + " " + this.get("lastName");

    },

    set: function(key, newName) {

      var parts = newName.split(" ");

      this.setProperties({ firstName: parts[0], lastName: parts[1] });

      return newName;

    }

  });

 

6.3 .property('column.@each'}行为:

请查看 特性-3.4

 

6.4 CP依赖于CP时的行为:

 CP1依赖于CP2,CP2依赖于firstName.     CP2--->CP1--->firstName

  当先读取CP2的值,然后在读取CP1的值,最后再次读取CP2的值,CP2最后一次不发生属性回调。

  当先读取CP1的值,然后读取CP2的值时,CP2发生回调。

test("Should not invoke current CP when dependency CP‘s dependency changed.", function (assert) {

  var Person = Ember.Object.extend({

    firstName: null,

    lastName: null,



    CP1: function (key, value) {

      computedCount++;

      return computedCount;

    }.property('CP2'),



    CP2: function (key, value) {

      computedCount++;

      return computedCount;

    }.property('firstName')

  });



  var person = Person.create();

  person.set('firstName', "Cui");



  assert.ok(computedCount === 0, "Should not invoke CP1's function");



  person.get('CP1');

  assert.ok(computedCount === 1, "Should invoke CP1's function at first time");



  person.get('CP1');

  assert.ok(computedCount === 1, "Should not invoke function when CP2 not changed");



  person.get('CP2');

  assert.ok(computedCount === 2, "Should invoke CP2's function when CP2 is called at first time");



  person.get('CP1');

  assert.ok(computedCount === 2, "Should not invoke CP1's function when CP2 is changed");



  /////////////////////////////////////////////////////////////////////////////////////////////



  person.set('firstName',"Wu");

  person.get('CP2');

  assert.ok(computedCount === 3, "Should invoke CP2's function when firstName changed");



  person.get('CP1');

  assert.ok(computedCount === 4, "Should invoke CP1's function when CP2 changed.");

});

 

6.5 Person.get('firstName')和Person.firstName之间的区别:

  当firstName为(非计算属性)普通属性时,行为相同。

  当firstName为CP属性时,前者能触发CP回调方法,或者不能。

 

7. 示例代码

Emberjs之ComputedProperty
/**

 *

 *

 *

 *

 *      Computed Property

 *

 *

 *

 *

 */

var computedCount = 0;

module('CP test', {

  beforeEach: function () {

    computedCount = 0;

  },



  afterEach: function () {

  }

});



test("Test dependency property to invoke CP when change dependency property", function(assert){

  var Person = Ember.Object.extend({

    name: 'Alex Matchneer',

    age: 0,

    human: Ember.computed('name', function(){

      computedCount++;

      return this.get('name');

    }).property('age')

  });



  var stephen = Person.create();

  stephen.get('human');

  console.log(computedCount);

  assert.ok(computedCount === 1);



  stephen.set('name',"CuiYansong");

  stephen.get('human');

  console.log(computedCount);

  assert.ok(computedCount === 1);



  stephen.set('age',29);

  stephen.get('human');

  console.log(computedCount);

  assert.ok(computedCount === 2);

});



test("Should invoke computed fucntion when get or set the CP property", function(assert)

{

  var Person = Ember.Object.extend({

    name: 'Alex Matchneer',

    nomen: Ember.computed('name', function(){

      computedCount++;

       return this.get('name');

    })

  });



  var alex = Person.create();

  alex.get('nomen'); // 'Alex Matchneer'

  assert.ok(computedCount === 1);



  alex.set('name', "Cuiyansong");

  assert.ok(computedCount === 1);



  alex.get('nomen');

  assert.ok(computedCount === 2);

});



test('Should change dependency property value when CP property changed and binding style is Ember.computed.alias', function(assert){

  var Person = Ember.Object.extend({

    name: 'Alex Matchneer',

    nomen: Ember.computed.alias('name')

  });



  var alex = Person.create();

  alex.get('nomen'); // 'Alex Matchneer'

  alex.get('name');  // 'Alex Matchneer'



  alex.set('nomen', '@machty');

  assert.ok(alex.get('name'), '@machty');

});



test('Should change CP property when dependency property changed changed and binding style is Ember.computed.alias', function(assert){

  var Person = Ember.Object.extend({

    name: 'Alex Matchneer',

    nomen: Ember.computed.alias('name')

  });



  var alex = Person.create();

  alex.get('nomen'); // 'Alex Matchneer'

  alex.get('name');  // 'Alex Matchneer'



  alex.set('name', '@machty');

  assert.ok(alex.get('nomen'), '@machty');

});



test('Should compute fullName when dependency property of columns inner item changed and binding style is [email protected]', function(assert) {

  var Person = Ember.Object.extend({

    columns: [

      Ember.Object.create({ isDone: true, isLoading: false }),

      Ember.Object.create({ isDone: false, isLoading: false  }),

      Ember.Object.create({ isDone: true , isLoading: false })

    ],

    fullName: Ember.computed(function(key, value) {

      computedCount++;

      return this.columns.length;

    }).property('[email protected]')

  });



  var client = Person.create();

  client.get('fullName');

  assert.ok(computedCount === 1);



  client.get('columns').objectAt(1).set('isLoading', true);

  client.get('fullName');

  assert.ok(computedCount === 1);



  client.get('columns').objectAt(1).set('isDone', true);

  client.get('fullName');

  assert.ok(computedCount === 2);



  client.get('columns').removeAt(1);

  client.get('fullName');

  assert.ok(computedCount === 3);



  client.get('columns').addObject(Ember.Object.create({ isDone: true, isLoading: false }));

  client.get('fullName');

  assert.ok(computedCount === 4);



  client.set('columns',[]);

  client.get('fullName');

  assert.ok(computedCount === 5);

});



test('Should compute fullName when dependency property of columns inner item changed and binding style is columns.@each', function(assert) {

  var Person = Ember.Object.extend({

    columns: [

      Ember.Object.create({ isDone: true, isLoading: false }),

      Ember.Object.create({ isDone: false, isLoading: false  }),

      Ember.Object.create({ isDone: true , isLoading: false })

    ],

    fullName: Ember.computed(function(key, value) {

      computedCount++;

      return this.columns.length;

    }).property('columns.@each')

  });



  var client = Person.create();

  client.get('fullName');

  console.log( "After get fullName, computedCount should be equal 1");

  assert.ok(computedCount === 1);



  client.get('columns').objectAt(1).set('isLoading', true);

  client.get('fullName');

  console.log( "After get fullName, computedCount should be equal 1");

  assert.ok(computedCount === 1);



  //client.get('columns').replace(1,0,[{}]);

  //client.get('fullName');

  //console.log("After set item of the columns, computedCount should be equal "+ computedCount);



  client.get('columns').objectAt(1).set('isDone', true);

  client.get('fullName');

  console.log( "After set isDone to true, computedCount should be equal 1");

  assert.ok(computedCount === 1);



  client.get('columns').removeAt(1);

  client.get('fullName');

  console.log( "After remove column item, computedCount should be equal 2");

  assert.ok(computedCount === 2);



  client.get('columns').addObject(Ember.Object.create({ isDone: true, isLoading: false }));

  client.get('fullName');

  console.log( "After add new object, computedCount should be equal 3");

  assert.ok(computedCount === 3);



  client.set('columns',[]);

  client.get('fullName');

  console.log( "After replace itself, computedCount should be equal 4");

  assert.ok(computedCount === 4);

});



test('Should compute fullName when dependency property of columns inner item changed and binding style is columns.[]', function(assert) {

  var Person = Ember.Object.extend({

    columns: [

      Ember.Object.create({ isDone: true, isLoading: false }),

      Ember.Object.create({ isDone: false, isLoading: false  }),

      Ember.Object.create({ isDone: true , isLoading: false })

    ],

    fullName: Ember.computed(function(key, value) {

      computedCount++;

      return this.columns.length;

    }).property('columns.[]')

  });



  var client = Person.create();

  client.get('fullName');

  console.log( "After get fullName, computedCount should be equal 1");

  assert.ok(computedCount === 1);



  client.get('columns').objectAt(1).set('isLoading', true);

  client.get('fullName');

  console.log("After set isLoading to true, computedCount should be equal 1");

  assert.ok(computedCount === 1);



  client.get('columns').objectAt(1).set('isDone', true);

  client.get('fullName');

  console.log( "After set isDone to true, computedCount should be equal 1");

  assert.ok(computedCount === 1);



  client.get('columns').removeAt(1);

  client.get('fullName');

  console.log( "After remove column item, computedCount should be equal 2");

  assert.ok(computedCount === 2);



  client.get('columns').addObject(Ember.Object.create({ isDone: true, isLoading: false }));

  client.get('fullName');

  console.log( "After add new object, computedCount should be equal 3");

  assert.ok(computedCount === 3);



  client.set('columns',[]);

  client.get('fullName');

  console.log( "After replace itself, computedCount should be equal 4");

  assert.ok(computedCount === 4);

});



/**

 *

 *

 *

 *

 *      Observer

 *

 *

 *

 *

 */

module('Observer test', {

  beforeEach: function () {

    computedCount = 0;

  },



  afterEach: function () {

  }

});



test('Should invoke observer callback when observed property changed immediately', function(assert) {

var  Person = Ember.Object.extend({

    // these will be supplied by `create`

    firstName: null,

    lastName: null,



    fullName: function() {

      var firstName = this.get('firstName');

      var lastName = this.get('lastName');



      return firstName + ' ' + lastName;

    }.property('firstName', 'lastName'),



    fullNameChanged: function() {

      // deal with the change

      computedCount++;

    }.observes('fullName')

  });



  var person = Person.create({

    firstName: 'Yehuda',

    lastName: 'Katz'

  });



  person.set('lastName', 'Yansong'); // observer will fire

  console.log(computedCount);

  assert.ok(computedCount ===1);

});



test('Should invoke once when update two property of which observer watching on', function(assert) {

  var  Person = Ember.Object.extend({

    // these will be supplied by `create`

    firstName: null,

    lastName: null,



    fullName: function() {

      var firstName = this.get('firstName');

      var lastName = this.get('lastName');



      return firstName + ' ' + lastName;

    }.property('firstName', 'lastName'),



    partOfNameChanged: function() {

      Ember.run.once(this, 'processFullName');

    }.observes('firstName', 'lastName'),



    processFullName: function() {

      // This will only fire once if you set two properties at the same time, and

      // will also happen in the next run loop once all properties are synchronized

      computedCount++;

      console.log(this.get('fullName'));

    }

  });



  var person = Person.create({

    firstName: 'Yehuda',

    lastName: 'Katz'

  });

  person.set('firstName', 'John');

  person.set('lastName', 'Smith');



  return asyncAssert(function () {

    assert.ok(computedCount ===1);

  });

});



test('Should invoke observer after init finish.', function(assert) {

  var Person = Ember.Object.extend({



    init: function() {

      //this.set('salutation', "Mr/Ms");

    },



    salutationDidChange: function() {

      // some side effect of salutation changing

      computedCount++;

    }.observes('salutation').on('init')

  });

  var person = Person.create({

  });



  assert.ok(computedCount === 1);



  person.set('salutation', "Mr");

  assert.ok(computedCount === 2);

});



test('Should invoke observer after CP changed when set CP property', function(assert) {

  var Person = Ember.Object.extend({

    firstName: null,

    lastName: null,



    fullName: function() {

      computedCount++;

      return this.get('firstName')+this.get('lastName');

    }.property('firstName', 'lastName'),



    fullNameDidChange: function() {

       console.log("I have been executed...");

    }.observes('fullName')

  });

  var person = Person.create({

  });



  person.set('firstName',"Cui");

  console.log(computedCount);

  assert.ok(computedCount === 0);



  person.get('fullName');

  console.log(computedCount);

  assert.ok(computedCount === 1);



  person.set('none', "none");

  assert.ok(computedCount === 1);

});
View Code

 

Ember更新日志:http://emberjs.com/deprecations/v1.x/#toc_deprecate-using-the-same-function-as-getter-and-setter-in-computed-properties 

 

你可能感兴趣的:(property)