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.1 CP:Computed Property。
2.2 CP属性:上文例子中的‘fullName’。
2.3 CP所依赖的源属性:上文例子中的‘firstName’、‘lastName’。
2.4 CP的回调方法:上文例子中的function(){......}方法。
2.5 CP属性的Setter/Getter:
3.1 只有当获取或设置CP属性时,才可能会触发CP的回调方法,也就是说CP属性是属于‘懒加载’的方式(使用时加载的行为)。
3.2 当CP属性依赖于.property('person{name,age}')时,仅当person.name或person.age发生改变时改变,其余方式均不可以更新CP属性。
3.3 当CP属性依赖于.property('[email protected]')时, 以下四种情况会发生变化:
@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')时,其行为会发生如下变化:
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属性时)。
总原则: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.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.1 当计算属性(CP)设置为getter和setter时,其CP回调函数触发的场景:
6.2 Ember.Computed(function(){}).property('xxxx') 与 function(){}.property('xxxx')区别:
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回调方法,或者不能。
/** * * * * * 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); });
Ember更新日志:http://emberjs.com/deprecations/v1.x/#toc_deprecate-using-the-same-function-as-getter-and-setter-in-computed-properties