写在前面
好长时间没有写博客了,昨天花了些时间又整理了下之前发布过的《Ember.js之computed Property》文章,并创建了一个测试代码库,花了些时间,希望能使用测试代码的方式,来演示如何使用Ember.js同时能避免升级Ember版本后,一些功能上的变化所带来的隐含Bug。
如果大家对Ember.js有兴趣想一起研究的话,欢迎大家一起维护测试代码库 :)
本文主要是针对Ember.Array中的[]和@each,数组方法进行详细分析。
文章索引
一、如何使用[] & @each
Ember中二者均可用于计算属性的绑定,使用十分方便,可以像如下方式定义依赖关系:
1 totalArea: Ember.computed(‘sizeArray.[]‘, function () { 2 return this.get(‘sizeArray‘).reduce(function (prev, cur) { 3 return prev + Ember.get(cur, ‘height‘) * Ember.get(cur, ‘width‘); 4 }, 0); 5 }),
或者:
1 totalArea: Ember.computed(‘[email protected]‘, function () { 2 return this.get(‘sizeArray‘).reduce(function (prev, cur) { 3 return prev + Ember.get(cur, ‘height‘) * Ember.get(cur, ‘width‘); 4 }, 0); 5 }),
这样就定义了一个依赖于数组sizeArray的计算属性,建立一个绑定关系,只要sizeArray发生变化,totalArea就会被重新计算,使用十分简单,只要在function前面罗列出依赖的属性即可。虽然使用简单,但是在使用上还是有些细节的,下一节将用测试代码来讲解使用上的细节。
注:Ember计算属性有多种写法,这里给出了Ember推荐写法,更多具体细节请参考文章《Ember.js之computed Property-1. What is CP》
二、Array.[] & [email protected]特性总结
(Ember v1.13.7)
1. [email protected] 特性
当CP属性依赖于.property(‘[email protected]‘)时:
- columns里面任何一个元素的isLoaded属性发生变化。
- columns增加元素或者删除子元素。
- columns数组本身被重新赋值。
- 不能依赖@[email protected] 。
1 test(‘computed property depend on @each.height‘, function (assert) { 2 var Size = Ember.Object.extend({height: 0, width: 0}); 3 var rectangle = Ember.Object.extend({ 4 totalArea: Ember.computed(‘[email protected]‘, function () { 5 return this.get(‘sizeArray‘).reduce(function (prev, cur) { 6 return prev + Ember.get(cur, ‘height‘) * Ember.get(cur, ‘width‘); 7 }, 0); 8 }), 9 sizeArray: [ 10 Size.create({height: 10, width: 10}), 11 Size.create({height: 10, width: 10}) 12 ] 13 }).create(); 14 var sizeArray = rectangle.get(‘sizeArray‘); 15 16 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should be 200"); 17 18 sizeArray.pushObject(Size.create({height: 10, width: 10})); 19 assert.equal(rectangle.get(‘totalArea‘), 300, "the total area should be 300 after added"); 20 21 sizeArray.removeAt(0); 22 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should be 200 after removed"); 23 24 sizeArray[0].set(‘height‘, 20); 25 assert.equal(rectangle.get(‘totalArea‘), 300, "the total area should be 300 after ‘height‘ changed"); 26 27 sizeArray[0].set(‘width‘, 20); 28 assert.equal(rectangle.get(‘totalArea‘), 300, "the total area should not be changed after ‘width‘ changed"); 29 30 sizeArray.clear(); 31 assert.equal(rectangle.get(‘totalArea‘), 0, "the total area should be 0 after reset rectangle.sizeArray"); 32 });
2. [email protected]特性
当CP属性依赖于.property(‘[email protected]‘)时:
- columns增加或删除元素。
- columns自身被替换或重新赋值。
1 test(‘computed property depend on @each‘, function (assert) { 2 var Size = Ember.Object.extend({height: 0, width: 0}); 3 var rectangle = Ember.Object.extend({ 4 totalArea: Ember.computed(‘[email protected]‘, function () { 5 return this.get(‘sizeArray‘).reduce(function (prev, cur) { 6 return prev + Ember.get(cur, ‘height‘) * Ember.get(cur, ‘width‘); 7 }, 0); 8 }), 9 sizeArray: [ 10 Size.create({height: 10, width: 10}), 11 Size.create({height: 10, width: 10}) 12 ] 13 }).create(); 14 var sizeArray = rectangle.get(‘sizeArray‘); 15 16 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should be 200"); 17 18 sizeArray.pushObject(Size.create({height: 10, width: 10})); 19 assert.equal(rectangle.get(‘totalArea‘), 300, "the total area should be 300 after added"); 20 21 sizeArray.removeAt(0); 22 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should be 200 after removed"); 23 24 sizeArray[0].set(‘height‘, 20); 25 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should not be changed"); 26 27 sizeArray.clear(); 28 assert.equal(rectangle.get(‘totalArea‘), 0, "the total area should be 0 after reset rectangle.sizeArray"); 29 });
3. Array.[]特性
当CP属性依赖于.property(‘columns.[]‘)时:
- 与绑定.property(‘[email protected]‘) 行为相同。
1 test(‘computed property depend on []‘, function (assert) { 2 var Size = Ember.Object.extend({height: 0, width: 0}); 3 var rectangle = Ember.Object.extend({ 4 totalArea: Ember.computed(‘sizeArray.[]‘, function () { 5 return this.get(‘sizeArray‘).reduce(function (prev, cur) { 6 return prev + Ember.get(cur, ‘height‘) * Ember.get(cur, ‘width‘); 7 }, 0); 8 }), 9 sizeArray: [ 10 Size.create({height: 10, width: 10}), 11 Size.create({height: 10, width: 10}) 12 ] 13 }).create(); 14 var sizeArray = rectangle.get(‘sizeArray‘); 15 16 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should be 200"); 17 18 sizeArray.pushObject(Size.create({height: 10, width: 10})); 19 assert.equal(rectangle.get(‘totalArea‘), 300, "the total area should be 300 after added"); 20 21 sizeArray.removeAt(0); 22 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should be 200 after removed"); 23 24 sizeArray[0].set(‘height‘, 20); 25 assert.equal(rectangle.get(‘totalArea‘), 200, "the total area should not be changed"); 26 27 sizeArray.clear(); 28 assert.equal(rectangle.get(‘totalArea‘), 0, "the total area should be 0 after reset rectangle.sizeArray"); 29 });
(Ember v2.0.0)
2015-08-17 除建议用[]代替@each,暂未发现其他变化。
[参考代码](https://github.com/emberjs/ember.js/blob/v2.0.0/packages/ember-metal/lib/computed.js#L224)
注: 更多关于计算属性问题,请参考文章《Ember.js之computed Property-3.CP重要原则》
三、源码分析Array.[] & [email protected]
[email protected]返回的是一个特殊类型,这个类型便于实现绑定。
1 ‘@each‘: computed(function() { 2 if (!this.__each) { 3 // ES6TODO: GRRRRR 4 var EachProxy = requireModule(‘ember-runtime/system/each_proxy‘)[‘EachProxy‘]; 5 6 this.__each = new EachProxy(this); 7 } 8 9 return this.__each; 10 })
[参考这里](https://github.com/emberjs/ember.js/blob/stable-1-13/packages/ember-runtime/lib/mixins/array.js#L516)
Array.[]继承自Ember.Enumerable.[]并且返回this。
1 ‘[]‘: computed({ 2 get(key) { 3 return this; 4 }, 5 set(key, value) { 6 this.replace(0, get(this, ‘length‘), value); 7 return this; 8 } 9 }),
[参考这里](https://github.com/emberjs/ember.js/blob/stable-1-13/packages/ember-runtime/lib/mixins/array.js#L164)
二者之间唯一的区别是Array.[]返回的是一对象自身(普通对象组成的数组), 而[email protected]返回的是EachProxy对象, 针对普通对象组成的数组而言,仅仅能检测到数组长度的变化和对象本身的变化,对数组内容发生变化则不得而知,而EachProxy对每一个元素增加observerListener监听器,当数组内容发生变化时,通知数组发生变更,实现了数组元素这一级别的监听。
1 var EachProxy = EmberObject.extend({ 2 3 init(content) { 4 this._super(...arguments); 5 this._content = content; 6 content.addArrayObserver(this); 7 8 // in case someone is already observing some keys make sure they are 9 // added 10 forEach(watchedEvents(this), function(eventName) { 11 this.didAddListener(eventName); 12 }, this); 13 }, 14 15 ...
四、相关引用
[[email protected]](http://emberjs.com/api/classes/Ember.Array.html#property__each)
[Emer-EachProxy](https://github.com/emberjs/ember.js/blob/stable-1-13/packages/ember-runtime/lib/system/each_proxy.js)
[Ember-study](https://github.com/Cuiyansong/ember-table-learnning)