write by yinmingjun,引用请注明。
ember.js作为一个全功能的javascript的MVC框架,需要维护大量的代码,基于write less use more的原则,ember.js不可避免的抽象了大量的基础服务,以便最大程度的复用代码。而对于希望读懂ember.js代码的开发人员,这部分代码可能会成为理解ember.js的设计意图的障碍。因此,笔者在开始描述ember.js的特性的细节之前,选择对ember.js中的基础代码先做一个梳理,并相信理解这部分代码,对用好ember.js会有很大的帮助。
在这部分代码中,也包含ember.js对javascript语言潜力的挖掘,会看到很多javascript擅长的表述方式,对那些喜欢极致编程的开发人员,将会是一个很好的体验过程。
下面我们按种类来介绍ember.js的基础服务。
一、对调试的支持
在开发的过程中,一般有限会考虑调试支持相关的API,这是解决生产效率的关键要素。
在ember.js中,有下面的调试API:
> Ember.assert 方法
定义
Ember.assert = function(desc, test) {
if (!test) throw new Error("assertion failed: "+desc);
};
描述
在代码中设置断言。如果断言不是true,就抛出异常,中止程序的运行。
Ember.assert在开发版的ember.js中,是断言方法,在release版中,会被替换成空方法(与编译系统中的assert类似),也就是说,Ember.assert是for开发的诊断工具。
参数
desc string类型,是传递给Ember.assert的诊断描述
test bool类型,如果为false,会引发断言的异常
> Ember.warn方法
定义
Ember.warn = function(message, test) {
if (!test) {
Ember.Logger.warn("WARNING: "+message);
if ('trace' in Ember.Logger) Ember.Logger.trace();
}
};
描述
与Ember.assert类似,不过不像Ember.assert会抛出异常并中止程序的运行,Ember.warn仅产生LOG。Ember.warn也是for开发的支持工具,在release版的ember.js中会被替换成空方法。
参数
message string类型,是需要输出的警告信息
test bool类型,如果为false,会引发输出警告的信息
> Ember.debug方法
定义
Ember.debug = function(message) {
Ember.Logger.debug("DEBUG: "+message);
};
描述
在代码中输出调试信息。在release版中,会被替换成空方法。
参数
message string类型,是需要输出的调试信息
> Ember.deprecate 方法
定义
Ember.deprecate = function(message, test) {
//code .......
};
描述
与Ember.warn类似,将message会输出到logger的warn级别,Ember.deprecate与Ember.warn差别是Ember.deprecate还会提供调用栈的信息。在release版中,会被替换成空方法。
参数
message string类型,是传递给Ember.deprecate的诊断信息描述
test bool类型,如果为false,会输出警告信息到logger
> Ember.deprecateFunc方法
定义
Ember.deprecateFunc = function(message, func) {
return function() {
Ember.deprecate(message);
return func.apply(this, arguments);
};
};
描述
返回一个包装函数,用来包装对参数中func的调用。
Ember.deprecateFunc用来产生API的废弃的调用说明,并将对废弃的API重定向到新的API。
例如,下面的Ember.none方法被Ember.isNone替代了,如果继续调用Ember.none会产生警告信息:
Ember.none = Ember.deprecateFunc("Ember.none is deprecated. Please use Ember.isNone instead.", Ember.isNone);
在release版中,Ember.deprecateFunc的警告信息被移除,只保留API的重定向功能。
参数
message string类型,是需要输出的警告信息
func function类型,是重定向的目标函数
二、LOG支持
ember.js通过Ember.Logger接口来提供LOG的支持,Ember.Logger的定义如下:
Ember.Logger的定义
Ember.Logger= {
log: consoleMethod('log') || Ember.K,
warn: consoleMethod('warn') || Ember.K,
error: consoleMethod('error') || Ember.K,
info: consoleMethod('info') || Ember.K,
debug: consoleMethod('debug') || consoleMethod('info') || Ember.K
};
Ember.Logger的描述
Ember.Logger的规则很简单,只是将信息输出到imports.console,在浏览器是输出到全局console函数之中。
替代Ember.Logger,只需要将其几个方法重定向就OK了,就默认实现来说,对大多数人来说以及足够用了,不过在大型系统中可能需要对Logger做集成。
三、对象复制&合并
在ember.js依赖的jquery中存在类似的服务,不过ember.js还是提供了,可能是因为对象复制的服务太基础了。在javascript的简单数据对象(POJO)的操作中,常见的操作是合并与复制,其中复制还可能需要做深度克隆。
ember.js通过Ember.merge方法支持对象的合并,通过Ember.copy支持对象的复制。
> Ember.merge方法
定义
Ember.merge = function(original, updates) {
for (var prop in updates) {
if (!updates.hasOwnProperty(prop)) { continue; }
original[prop] = updates[prop];
}
return original;
};
描述
Ember.merge将updates中的内容合并到original对象之中。如果original中已经存在同名的属性,会被updates中的内容覆盖。
参数
original object类型,是需要合并的目标
updates object类型,是合并的数据来源
> Ember.copy方法
定义
Ember.copy = function(obj, deep) {
// fast paths
if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives
if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep);
return _copy(obj, deep, deep ? [] : null, deep ? [] : null);
};
描述
Ember.copy将复制obj中的内容,并返回克隆的对象。如果存在deep参数,并且其值为true,那么会做深度的复制。
注意,如果obj不是对象,或者是null,
合并到original对象之中。如果original中已经存在同名的属性,会被updates中的内容覆盖。
参数
obj object类型,是需要复制的对象
deep bool类型,可忽略,如果为true表示是深度复制
四、代码的调度&执行
> Ember.onLoad方法
定义
Ember.onLoad= function(name, callback) {
var object;
loadHooks[name] = loadHooks[name] || Ember.A();
loadHooks[name].pushObject(callback);
if (object = loaded[name]) {
callback(object);
}
};
描述
ember.js支持对onload队列的挂接,用于支持各个生命周期中的就绪事件的底层处理。和与Ember.onLoad方法配合的是Ember.runLoadHooks方法,用于执行一个名字对应的队列中所有的callback方法。如果队列以及执行过了,传入的callback会立即被执行。
在ember.js中,有'Ember.Handlebars'、'application'、'Ember.Application'等几个内部的队列。
参考Ember.runLoadHooks方法:
Ember.runLoadHooks= function(name, object) {
loaded[name] = object;
if (loadHooks[name]) {
forEach.call(loadHooks[name], function(callback) {
callback(object);
});
}
};
参数
name string类型,是load队列的名字
callback funtion类型,是希望放到load队列中的callback方法
> Ember.run方法
定义
Ember.run= function(target, method) {
var ret;
if (Ember.onerror) {
try {
ret = backburner.run.apply(backburner, arguments);
} catch (e) {
Ember.onerror(e);
}
} else {
ret = backburner.run.apply(backburner, arguments);
}
return ret;
};
描述
Ember.run方法执行指定对象的方法。ember.js提供Ember.run方法的主要目的是可以获取到方法执行前后的事件,可以对绑定、变更等事件立即做出响应,还可以拦截代码运行过程中的异常,并将异常传递给Ember.onerror中的全局的错误的handler中。
ember.js通过backburner来封装对运行的支持。
参数
target object类型,可能为null,是method所在的对象。
methodfunction类型,target上的方法;或string类型,是target成员的名称。
args 后续的所有参数,可省略,将在调用时传递给method。
> Ember.run.join方法
定义
Ember.run.join= function(target, method) {
if (!Ember.run.currentRunLoop) {
return Ember.run.apply(Ember.run, arguments);
}
var args = slice.call(arguments);
args.unshift('actions');
Ember.run.schedule.apply(Ember.run, args);
};
描述
如果不存在Ember.run.currentRunLoop的时候与Ember.run方法相同,如果有Ember.run.currentRunLoop就会将要执行的方法放到RunLoop的'actions'队列中执行。
说明
解释一下,Ember.run方法每次执行的时候都会建立一个DeferredActionQueues的实例,作为执行的上下文,将当前的Ember.run.currentRunLoop的实例推入栈中保存起来,并在执行完毕之后恢复Ember.run.currentRunLoop的值,也就是说,Ember.run.currentRunLoop总是代表当前的Ember.run方法的执行上下文。
参数
(同Ember.run方法)
> Ember.run.begin和Ember.run.end方法
定义
Ember.run.begin= function() {
backburner.begin();
};
Ember.run.end= function() {
backburner.end();
};
描述
是手工开启/关闭ember.js的RunLoop的底层调用的API,一般使用的模式如下:
Ember.run.begin();
// code to be execute within a RunLoop
Ember.run.end();
> Ember.run.schedule方法
定义
Ember.run.schedule= function(queue, target, method) {
checkAutoRun();
backburner.schedule.apply(backburner, arguments);
};
描述
Ember.run.schedule方法将指定对象的方法传递到指定的调度队列中执行。
ember.js提供了'sync', 'actions', 'render', 'afterRender','destroy'五个队列,三个队列的执行次序与定义次序相同,sync最先,actions居中,destroy最后。ember.js的调度队列在每次Ember.run方法的end期间执行(完callback之后)。
Ember.run.schedule方法一般是在ember.js内部使用。
参数
queue string类型,是'sync', 'actions', 'render', 'afterRender','destroy'中的一个,队列的执行次序与定义次序相同,sync最先,actions居中,destroy最后。
target object类型,可以为null,是method所在的对象。
method function类型,target上的方法;或string类型,是target成员的名称。
args 后续的所有参数,可省略,将在调用时传递给method。
> Ember.run.scheduleOnce方法
定义
Ember.run.scheduleOnce= function(queue, target, method) {
checkAutoRun();
return backburner.scheduleOnce.apply(backburner, arguments);
};
描述
与Ember.run.schedule方法左右类似,差别是在语义上保证只调度执行一次(无论使用Ember.run.scheduleOnce添加调度项目几次)
注意
使用匿名函数的时候要小心,因为每次执行的时候都返回返回不同的函数,在比较上会认为调度项目是不同的方法。如:
Ember.run.scheduleOnce('actions', myContext,function(){ console.log("Closure"); });
参数
(参考Ember.run.schedule方法)
> Ember.run.once方法
定义
Ember.run.once= function(target, method) {
checkAutoRun();
var args = slice.call(arguments);
args.unshift('actions');
return backburner.scheduleOnce.apply(backburner, args);
};
描述
是Ember.run.scheduleOnce的便捷方法,将提供的对象、方法调度到'actions'队列执行。
参数
target object类型,可以为null,是method所在的对象。
method function类型,target上的方法;或string类型,是target成员的名称。
args 后续的所有参数,可省略,将在调用时传递给method。
> Ember.run.later方法
定义
Ember.run.later= function(target, method) {
return backburner.later.apply(backburner, arguments);
};
描述
延迟执行指定的方法,与setTimeout作用类似。调用的DEMO如下:
Ember.run.later(myContext, function(){
// code here will execute within a RunLoop in about 500ms with this == myContext
},500);
规定,最后一个参数是延迟的时间,单位是ms(毫秒)
参数
target object类型,可以为null,是method所在的对象。
method function类型,target上的方法;或string类型,是target成员的名称。
args 后续的所有参数,可省略,将在调用时传递给method。
delay 最后一个参数是延迟的毫秒数。
> Ember.run.next方法
定义
Ember.run.next= function() {
var args = slice.call(arguments);
args.push(1);
return backburner.later.apply(backburner, args);
};
描述
是Ember.run.later的便捷方法,表示延迟执行方法。包装的later中提供的延迟时间是1ms。
参数
target object类型,可以为null,是method所在的对象。
method function类型,target上的方法;或string类型,是target成员的名称。
args 后续的所有参数,可省略,将在调用时传递给method。
> Ember.run.cancel方法
定义
Ember.run.cancel= function(timer) {
return backburner.cancel(timer);
};
描述
取消Ember.run.later()或Ember.run.next()指定的延迟方法的执行,提供的timer参数应该是Ember.run.later()或Ember.run.next()的返回值。
参数
timer object类型,应该是Ember.run.later()或Ember.run.next()的返回值。
> Ember.run.cancelTimers方法
定义
Ember.run.cancelTimers= function () {
backburner.cancelTimers();
};
描述
取消所有的延迟方法的执行,一般用于ember.js运行环境的清理。
参数
无
五、对事件体系的支持
ember.js通过Ember.Evented这个Mixin对事件体系提供支持,而Ember.Evented又被混合到Ember.CoreView之中,也就是说所有的view都能使用Ember.Evented提供的方法。
> Ember.Evented Mixin
定义
Ember.Evented = Ember.Mixin.create({
on: function(name, target, method) {
Ember.addListener(this, name, target, method);
return this;
},
one: function(name, target, method) {
if (!method) {
method = target;
target = null;
}
Ember.addListener(this, name, target, method, true);
return this;
},
trigger: function(name) {
var args = [], i, l;
for (i = 1, l = arguments.length; i < l; i++) {
args.push(arguments[i]);
}
Ember.sendEvent(this, name, args);
},
fire: function(name) {
Ember.deprecate("Ember.Evented#fire() has been deprecated in favor of trigger() for compatibility with jQuery. It will be removed in 1.0. Please update your code to call trigger() instead.");
this.trigger.apply(this, arguments);
},
off: function(name, target, method) {
Ember.removeListener(this, name, target, method);
return this;
},
has: function(name) {
return Ember.hasListeners(this, name);
}
});
描述
简单的说一下。
on方法:
用于设置event的handler,name对应事件的名称,target和method合起来描述handler。target参数是可以省略的。
one方法:
与on方法基本一致,差别是注册的handler执行过之后就自动remove,也就是说只响应一次事件。
trigger方法:
触发事件,需要给出需要触发事件的名称,后面是传递给handler的参数。
fire方法:
已经废弃,被trigger方法取代了。
off方法:
从事件的监听者中去掉target+method对应的处理者。target参数是可以省略的。
has方法:
获取指定的事件是否存在监听者。
五、对标准对象的扩展
ember.js对javascript的运行环境做了很多补充,我们简单看一下。
> 对string的扩充
定义
Ember.String = {
fmt: function(str, formats) {
// first, replace any ORDERED replacements.
var idx = 0; // the current index for non-numerical replacements
return str.replace(/%@([0-9]+)?/g, function(s, argIndex) {
argIndex = (argIndex) ? parseInt(argIndex,0) - 1 : idx++ ;
s = formats[argIndex];
return ((s === null) ? '(null)' : (s === undefined) ? '' : s).toString();
}) ;
},
loc: function(str, formats) {
str = Ember.STRINGS[str] || str;
return Ember.String.fmt(str, formats) ;
},
w: function(str) { return str.split(/\s+/); },
decamelize: function(str) {
return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase();
},
dasherize: function(str) {
var cache = STRING_DASHERIZE_CACHE,
hit = cache.hasOwnProperty(str),
ret;
if (hit) {
return cache[str];
} else {
ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-');
cache[str] = ret;
}
return ret;
},
camelize: function(str) {
return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) {
return chr ? chr.toUpperCase() : '';
}).replace(/^([A-Z])/, function(match, separator, chr) {
return match.toLowerCase();
});
},
classify: function(str) {
var parts = str.split("."),
out = [];
for (var i=0, l=parts.length; i<l; i++) {
var camelized = Ember.String.camelize(parts[i]);
out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1));
}
return out.join(".");
},
underscore: function(str) {
return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2').
replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase();
},
capitalize: function(str) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
};
String.prototype.fmt = function() {
return fmt(this, arguments);
};
String.prototype.w = function() {
return w(this);
};
String.prototype.loc = function() {
return loc(this, arguments);
};
String.prototype.camelize = function() {
return camelize(this);
};
String.prototype.decamelize = function() {
return decamelize(this);
};
String.prototype.dasherize = function() {
return dasherize(this);
};
String.prototype.underscore = function() {
return underscore(this);
};
String.prototype.classify = function() {
return classify(this);
};
String.prototype.capitalize = function() {
return capitalize(this);
};
描述
简单的说一下。
fmt方法:
提供字符串格式化的支持。fmt通过提供' %@'形式参数来提供格式化的支持,每个形式参数' %@'按照其出现的位置对应实际参数数组中对应的参数,如果后面有数字,则表示使用指定位置的实际参数。例如:
"Hello%@%@".fmt('John', 'Doe'); // "Hello John Doe"
"Hello%@2,%@1".fmt('John', 'Doe'); // "Hello Doe, John"
loc方法:
提供本地化的支持。通过在Ember.STRINGS中检索资源字符串来格式化字符串。第一个参数是Ember.STRINGS中资源的key值,第二个参数与fmt的第二个参数相同。例如:
Ember.STRINGS = {
'_Hello World': 'Bonjour le monde',
'_Hello %@ %@': 'Bonjour %@ %@'
};
Ember.String.loc("_Hello World"); // 'Bonjour le monde';
Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith";
w方法:
提供字符串根据空格分词的功能。
decamelize方法:
字符串去骆驼化,在大小写的分界线上添加'_',并将大写转换成小写,并去掉首字母的大写。例如:
'innerHTML'.decamelize(); // 'inner_html'
'action_name'.decamelize(); // 'action_name'
'css-class-name'.decamelize(); // 'css-class-name'
'my favorite items'.decamelize(); // 'my favorite items'
dasherize方法:
字符串短线化。字符串先去骆驼化,然后将空格和'_'转换成'-'。例如:
'innerHTML'.dasherize(); // 'inner-html'
'action_name'.dasherize(); // 'action-name'
'css-class-name'.dasherize(); // 'css-class-name'
'my favorite items'.dasherize(); // 'my-favorite-items'
camelize方法:
字符串小骆驼化。将'_'、空格、'-'等分隔符去掉,并将连接处后面的字符转换成大写,最后将首字母小写,形成小骆驼形字符串。例如:
'innerHTML'.camelize(); // 'innerHTML'
'action_name'.camelize(); // 'actionName'
'css-class-name'.camelize(); // 'cssClassName'
'my favorite items'.camelize(); // 'myFavoriteItems'
'My Favorite Items'.camelize(); // 'myFavoriteItems'
classify方法:
字符串大骆驼化,除了小骆驼化之外,还将首字母大写。例如:
'innerHTML'.classify(); // 'InnerHTML'
'action_name'.classify(); // 'ActionName'
'css-class-name'.classify(); // 'CssClassName'
'my favorite items'.classify(); // 'MyFavoriteItems'
underscore方法:
字符串下划线化。就是将空格、大小写分界处、'-'等替换成下划线分割,并转换成小写。例如:
'innerHTML'.underscore(); // 'inner_html'
'action_name'.underscore(); // 'action_name'
'css-class-name'.underscore(); // 'css_class_name'
'my favorite items'.underscore(); // 'my_favorite_items'
capitalize方法:
将字符串的首字母转换成大写。
> 对Function的扩充
定义
Function.prototype.property = function() {
var ret = Ember.computed(this);
return ret.property.apply(ret, arguments);
};
Function.prototype.observes = function() {
this.__ember_observes__ = a_slice.call(arguments);
return this;
};
Function.prototype.observesBefore = function() {
this.__ember_observesBefore__ = a_slice.call(arguments);
return this;
};
描述
property方法:
用来支持ember.js中的计算属性的书写。
observes方法:
用来支持ember.js中的属性变更的观察者的书写。
observesBefore方法:
用来支持ember.js中的属性变更的前事件的书写。
> 对Array的扩充
定义
// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map
vararrayMap= isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun/*,thisp*/) {
//"use strict";
if (this === void 0 || this === null) {
throw new TypeError();
}
vart= Object(this);
varlen= t.length>>> 0;
if (typeof fun !== "function") {
throw new TypeError();
}
varres= new Array(len);
varthisp= arguments[1];
for (var i = 0; i < len; i++) {
if (i in t) {
res[i] =fun.call(thisp,t[i],i,t);
}
}
returnres;
};
// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach
vararrayForEach= isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun/*,thisp*/) {
//"use strict";
if (this === void 0 || this === null) {
throw new TypeError();
}
vart= Object(this);
varlen= t.length >>> 0;
if (typeof fun !== "function") {
throw new TypeError();
}
varthisp= arguments[1];
for (var i = 0; i < len; i++) {
if (i in t) {
fun.call(thisp, t[i], i, t);
}
}
};
vararrayIndexOf= isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj,fromIndex) {
if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; }
else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); }
for (var i = fromIndex, j = this.length; i < j; i++) {
if (this[i]===obj) { return i; }
}
return -1;
};
Ember.ArrayPolyfills= {
map: arrayMap,
forEach: arrayForEach,
indexOf: arrayIndexOf
};
描述
由于可能和javascript语言的标准冲突,对Array的map、forEach和indexOf的扩充没有直接填充到Ember.ArrayPolyfills对象之中。对这几个方法简短的说明一下。
map方法:
对每个数组的元素调用回调函数来获取结果,并返回结果的数组。map的第一个参数是callback方法,第二个参数如果存在,会传递到callback作为this的值(call方法会将第一个参数作为this参数)。callback的参数依次是:当前的数组元素、数组元素的索引、数组本身,返回值作为map的结果,保存在map函数维护的返回值的数组中,在map结束后作为map的返回值返回。
forEach方法:
与map方法类似,只是不会收集callback的返回值。
indexOf方法:
获取指定对象在数组中的索引,第二个参数可选,用于指定检索的起始位置。
> Ember.Enumerable Mixin
定义
Ember.Enumerable=Ember.Mixin.create({
// compatibility
isEnumerable: true,
nextObject: Ember.required(Function),
firstObject:Ember.computed(function() {
if (get(this, 'length')===0) return undefined ;
// handle generic enumerables
varcontext=popCtx(), ret;
ret = this.nextObject(0, null, context);
pushCtx(context);
return ret ;
}).property('[]'),
lastObject:Ember.computed(function() {
var len = get(this, 'length');
if (len===0) return undefined ;
varcontext=popCtx(), idx=0, cur, last = null;
do {
last = cur;
cur = this.nextObject(idx++, last, context);
} while (cur !== undefined);
pushCtx(context);
return last;
}).property('[]'),
contains: function(obj) {
return this.find(function(item) { return item===obj; }) !== undefined;
},
forEach: function(callback,target) {
if (typeof callback !== "function") throw new TypeError() ;
var len = get(this, 'length'), last = null,context= popCtx();
if (target === undefined) target = null;
for(var idx=0;idx<len;idx++) {
var next = this.nextObject(idx, last,context) ;
callback.call(target, next, idx, this);
last = next ;
}
last = null ;
context = pushCtx(context);
return this ;
},
getEach: function(key) {
return this.mapProperty(key);
},
setEach: function(key, value) {
return this.forEach(function(item) {
set(item, key, value);
});
},
map: function(callback,target) {
varret=Ember.A([]);
this.forEach(function(x, idx, i) {
ret[idx] =callback.call(target,x,idx,i);
});
return ret ;
},
mapProperty: function(key) {
return this.map(function(next) {
return get(next, key);
});
},
filter: function(callback, target) {
var ret = Ember.A([]);
this.forEach(function(x, idx, i) {
if (callback.call(target, x, idx, i)) ret.push(x);
});
return ret ;
},
reject: function(callback, target) {
return this.filter(function() {
return !(callback.apply(target, arguments));
});
},
filterProperty: function(key, value) {
return this.filter(iter.apply(this, arguments));
},
rejectProperty: function(key, value) {
var exactValue = function(item) { return get(item, key) === value; },
hasValue = function(item) { return !!get(item, key); },
use = (arguments.length === 2 ? exactValue : hasValue);
return this.reject(use);
},
find: function(callback, target) {
var len = get(this, 'length') ;
if (target === undefined) target = null;
var last = null, next, found = false, ret ;
var context = popCtx();
for(var idx=0;idx<len && !found;idx++) {
next = this.nextObject(idx, last, context) ;
if (found = callback.call(target, next, idx, this)) ret = next ;
last = next ;
}
next = last = null ;
context = pushCtx(context);
return ret ;
},
findProperty: function(key, value) {
return this.find(iter.apply(this, arguments));
},
every: function(callback, target) {
return !this.find(function(x, idx, i) {
return !callback.call(target, x, idx, i);
});
},
everyProperty: function(key, value) {
return this.every(iter.apply(this, arguments));
},
some: function(callback, target) {
return !!this.find(function(x, idx, i) {
return !!callback.call(target, x, idx, i);
});
},
someProperty: function(key, value) {
return this.some(iter.apply(this, arguments));
},
reduce: function(callback, initialValue, reducerProperty) {
if (typeof callback !== "function") { throw new TypeError(); }
var ret = initialValue;
this.forEach(function(item, i) {
ret = callback.call(null, ret, item, i, this, reducerProperty);
}, this);
return ret;
},
invoke: function(methodName) {
var args, ret = Ember.A([]);
if (arguments.length>1) args = a_slice.call(arguments, 1);
this.forEach(function(x, idx) {
var method = x && x[methodName];
if ('function' === typeof method) {
ret[idx] = args ? method.apply(x, args) : method.call(x);
}
}, this);
return ret;
},
toArray: function() {
var ret = Ember.A([]);
this.forEach(function(o, idx) { ret[idx] = o; });
return ret ;
},
compact: function() {
return this.filter(function(value) { return value != null; });
},
without: function(value) {
if (!this.contains(value)) return this; // nothing to do
var ret = Ember.A([]);
this.forEach(function(k) {
if (k !== value) ret[ret.length] = k;
}) ;
return ret ;
},
uniq: function() {
var ret = Ember.A([]);
this.forEach(function(k){
if (a_indexOf(ret, k)<0) ret.push(k);
});
return ret;
},
'[]':Ember.computed(function(key, value) {
return this;
}),
// ..........................................................
// ENUMERABLE OBSERVERS
//
addEnumerableObserver: function(target, opts) {
var willChange = (opts && opts.willChange) || 'enumerableWillChange',
didChange = (opts && opts.didChange) || 'enumerableDidChange';
var hasObservers = get(this, 'hasEnumerableObservers');
if (!hasObservers) Ember.propertyWillChange(this,'hasEnumerableObservers');
Ember.addListener(this,'@enumerable:before', target, willChange);
Ember.addListener(this,'@enumerable:change', target, didChange);
if (!hasObservers) Ember.propertyDidChange(this,'hasEnumerableObservers');
return this;
},
removeEnumerableObserver: function(target, opts) {
var willChange = (opts && opts.willChange) || 'enumerableWillChange',
didChange = (opts && opts.didChange) || 'enumerableDidChange';
var hasObservers = get(this,'hasEnumerableObservers');
if (hasObservers) Ember.propertyWillChange(this,'hasEnumerableObservers');
Ember.removeListener(this,'@enumerable:before', target, willChange);
Ember.removeListener(this,'@enumerable:change', target, didChange);
if (hasObservers) Ember.propertyDidChange(this,'hasEnumerableObservers');
return this;
},
hasEnumerableObservers:Ember.computed(function() {
return Ember.hasListeners(this,'@enumerable:change') || Ember.hasListeners(this,'@enumerable:before');
}),
enumerableContentWillChange: function(removing, adding) {
var removeCnt, addCnt, hasDelta;
if ('number' === typeof removing) removeCnt = removing;
else if (removing) removeCnt = get(removing, 'length');
else removeCnt = removing = -1;
if ('number' === typeof adding) addCnt = adding;
else if (adding) addCnt = get(adding,'length');
else addCnt = adding = -1;
hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0;
if (removing === -1) removing = null;
if (adding === -1) adding = null;
Ember.propertyWillChange(this, '[]');
if (hasDelta) Ember.propertyWillChange(this, 'length');
Ember.sendEvent(this,, [this, removing, adding]);
return this;
},
enumerableContentDidChange: function(removing, adding) {
var removeCnt, addCnt, hasDelta;
if ('number' === typeof removing) removeCnt = removing;
else if (removing) removeCnt = get(removing, 'length');
else removeCnt = removing = -1;
if ('number' === typeof adding) addCnt = adding;
else if (adding) addCnt = get(adding, 'length');
else addCnt = adding = -1;
hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0;
if (removing === -1) removing = null;
if (adding === -1) adding = null;
Ember.sendEvent(this,'@enumerable:change', [this, removing, adding]);
if (hasDelta) Ember.propertyDidChange(this, 'length');
Ember.propertyDidChange(this, '[]');
return this ;
}
}) ;
描述
Ember.EnumerableMixin提供基础的可枚举能力,是ember.js对于可枚举特性的规划,并在其中提供了对枚举集合的观察者模式,是ember.js中很基础的一个组件。
我们简单的描述一下它的各个成员。
isEnumerable属性:
是Ember.Enumerable的存在性的标记。
nextObject方法:
是实现可枚举特性的基础方法,用于获取下一个对象,这个由实现类提供实现。对nextObject方法的调用很简单:
this.nextObject(idx, last,context) ;
idx是迭代的索引,从0开始依次累加;last是上次调用nextObject的返回值;context被枚举对象用于维护枚举状态,返回值是当前的枚举值。
firstObject计算属性:
用于返回枚举中的第一个对象,访问属性会导致枚举状态的变化,影响nextObject方法的返回值。
lastObject计算属性:
用于返回枚举中的最后一个对象,访问属性会导致枚举状态的变化,影响nextObject方法的返回值。
contains方法:
判断集合中是否包含指定的对象。
forEach方法:
用于对遍历集合中的所有元素,并对每个元素调用callback方法。第二个target参数是callback的this上下文参数的值,回调函数的描述如下:
function(item, index, enumerable);
item 是当前的枚举值;index是枚举的索引;enumerable是当前的可枚举对象;
getEach方法:
是mapProperty的别名。
setEach方法:
有key, value两个参数,用于遍历所有的枚举项,并在每个枚举项上设置指定的key属性的值为value。
map方法:
与forEach方法类似,不过会收集每个callback的返回值,并放置到数组之中,作为map的结果返回。
callback和target参数的描述参考forEach方法。
mapProperty方法:
有key一个参数,遍历集合的所有元素,并依次获取当前枚举项的key的属性值,并将结果传递到数组中返回。
filter方法:
有callback, target两个参数,target参数是callback的this上下文,callback的参数描述与forEach方法中的callback的参数描述一致,callback的返回值如果是true,当前枚举项的值会被添加到返回的数组之中。
reject方法:
是filter方法的反集,只返回callback为false的枚举项。
filterProperty方法:
有key, value两个参数,对所有的集合元素做过滤,只返回其属性key的值与value相等的元素。
rejectProperty方法:
有key, value两个参数,其中value是可以省略的,表达的是不同的含义。如果提供了value,那么对所有的集合元素做过滤,只返回其属性key的值与value不相等的元素;如果没提供value,那么返回不含key属性(或key属性值的bool转换为false的)的元素。
find方法:
有callback, target两个参数,target参数是callback的this上下文,callback的参数描述与forEach方法中的callback的参数描述一致,在callback中对每个元素做比较,如果是需要的元素,返回true,当前元素会被作为find的结果返回。没找到返回undefined。
findProperty方法:
有key, value两个参数,用于查找集合元素中属性key的值与value相等的元素。
every方法:
有callback, target两个参数,target参数是callback的this上下文,callback的参数描述与forEach方法中的callback的参数描述一致。如果集合中的每个元素的callback的结果都是true,那么every方法的返回值就是true,否则返回false。
everyProperty方法:
有key, value两个参数,如果集合中的每个元素的key属性的值都是value,那么返回值就是true,否则返回false。
some方法:
有callback, target两个参数,target参数是callback的this上下文,callback的参数描述与forEach方法中的callback的参数描述一致。如果对集合中的任何一个元素的callback的结果是true,返回值就是true,否则返回false。
someProperty方法:
有key, value两个参数,如果集合中的有任何一个元素的key属性的值是value,那么返回值就是true,否则返回false。
reduce方法:
有callback, initialValue, reducerProperty三个参数,其中initialValue是reduce最初的工作集,reduce方法对集合中的所有元素,依次调用callback方法,callback的描述如下:
function(previousValue, item, index, enumerable, reducerProperty);
最初传递给callback的previousValue的值是initialValue参数的值,而callback的返回值会作为下一次callback调用的previousValue参数的值,最后一次callback调用的返回值会作为reduce方法的返回值返回。
invoke方法:
携带任意多的参数,第一个参数是methodName,其余的参数会传递给调用的方法。对集合中的每个元素,调用指定的方法(如果集合元素是一个方法,如果不存在methodName成员,自身会被调用),传递的参数就是methodName以外的其他参数,返回值被放到数组中作为invoke方法的返回值返回。当前元素会作为method的this参数。
toArray方法:
将枚举集合转换成数组返回。
compact方法:
去掉集合中的null值,并将过滤结果作为数组返回。
without方法:
有一个value参数,返回不等于value的所有集合元素的数组。
uniq方法:
返回一个每个值都唯一的集合元素的数组。
'[]'计算属性:
没有实现,仅返回自身。
addEnumerableObserver方法:
支持枚举@enumerable观察者模式的内部方法,为枚举集合添加观察者。有target, opts两个参数,如果提供了opts参数并有willChange和didChange成员,这两个方法会作为观察者,否则会从当前类中查找enumerableWillChange方法和enumerableDidChange方法作为观察者。会触发hasEnumerableObservers计算属性的观察者。
removeEnumerableObserver方法:
支持枚举@enumerable观察者模式的内部方法,去掉枚举集合的观察者。有target, opts两个参数,如果提供了opts参数并有willChange和didChange成员,这两个方法会作为观察者,否则会从当前类中查找enumerableWillChange方法和enumerableDidChange方法作为观察者。会触发hasEnumerableObservers计算属性的观察者。
hasEnumerableObservers计算属性:
支持枚举@enumerable观察者模式的计算属性,用来判断是否存在@enumerable观察者。
enumerableContentWillChange方法:
是枚举@enumerable观察者前事件的触发方法,有removing, adding两个参数,可能会触发'[]'计算属性和'length'属性上的观察者的前事件。
enumerableContentDidChange方法:
是枚举@enumerable观察者后事件的触发方法,有removing, adding两个参数,可能会触发'[]'计算属性和'length'属性上的观察者的前事件。
> Ember.Array Mixin
定义
Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ {
length: Ember.required(),
objectAt: function(idx) {
if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ;
return get(this, idx);
},
objectsAt: function(indexes) {
var self = this;
return map(indexes, function(idx){ return self.objectAt(idx); });
},
// overrides Ember.Enumerable version
nextObject: function(idx) {
return this.objectAt(idx);
},
'[]': Ember.computed(function(key, value) {
if (value !== undefined) this.replace(0, get(this, 'length'), value) ;
return this ;
}),
firstObject: Ember.computed(function() {
return this.objectAt(0);
}),
lastObject: Ember.computed(function() {
return this.objectAt(get(this, 'length')-1);
}),
// optimized version from Enumerable
contains: function(obj){
return this.indexOf(obj) >= 0;
},
// Add any extra methods to Ember.Array that are native to the built-in Array.
slice: function(beginIndex, endIndex) {
var ret = Ember.A([]);
var length = get(this, 'length') ;
if (isNone(beginIndex)) beginIndex = 0 ;
if (isNone(endIndex) || (endIndex > length)) endIndex = length ;
if (beginIndex < 0) beginIndex = length + beginIndex;
if (endIndex < 0) endIndex = length + endIndex;
while(beginIndex < endIndex) {
ret[ret.length] = this.objectAt(beginIndex++) ;
}
return ret ;
},
indexOf: function(object, startAt) {
var idx, len = get(this, 'length');
if (startAt === undefined) startAt = 0;
if (startAt < 0) startAt += len;
for(idx=startAt;idx<len;idx++) {
if (this.objectAt(idx, true) === object) return idx ;
}
return -1;
},
lastIndexOf: function(object, startAt) {
var idx, len = get(this, 'length');
if (startAt === undefined || startAt >= len) startAt = len-1;
if (startAt < 0) startAt += len;
for(idx=startAt;idx>=0;idx--) {
if (this.objectAt(idx) === object) return idx ;
}
return -1;
},
addArrayObserver: function(target, opts) {
var willChange = (opts && opts.willChange) || 'arrayWillChange',
didChange = (opts && opts.didChange) || 'arrayDidChange';
var hasObservers = get(this, 'hasArrayObservers');
if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers');
Ember.addListener(this,'@array:before', target, willChange);
Ember.addListener(this,'@array:change', target, didChange);
if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers');
return this;
},
removeArrayObserver: function(target, opts) {
var willChange = (opts && opts.willChange) || 'arrayWillChange',
didChange = (opts && opts.didChange) || 'arrayDidChange';
var hasObservers = get(this, 'hasArrayObservers');
if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers');
Ember.removeListener(this, '@array:before', target, willChange);
Ember.removeListener(this, '@array:change', target, didChange);
if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers');
return this;
},
hasArrayObservers: Ember.computed(function() {
return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before');
}),
arrayContentWillChange: function(startIdx, removeAmt, addAmt) {
// if no args are passed assume everything changes
if (startIdx===undefined) {
startIdx = 0;
removeAmt = addAmt = -1;
} else {
if (removeAmt === undefined) removeAmt=-1;
if (addAmt === undefined) addAmt=-1;
}
// Make sure the @each proxy is set up if anyone is observing @each
if (Ember.isWatching(this, '@each')) { get(this, '@each'); }
Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]);
var removing, lim;
if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) {
removing = [];
lim = startIdx+removeAmt;
for(var idx=startIdx;idx<lim;idx++) removing.push(this.objectAt(idx));
} else {
removing = removeAmt;
}
this.enumerableContentWillChange(removing, addAmt);
return this;
},
arrayContentDidChange: function(startIdx, removeAmt, addAmt) {
// if no args are passed assume everything changes
if (startIdx===undefined) {
startIdx = 0;
removeAmt = addAmt = -1;
} else {
if (removeAmt === undefined) removeAmt=-1;
if (addAmt === undefined) addAmt=-1;
}
var adding, lim;
if (startIdx>=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) {
adding = [];
lim = startIdx+addAmt;
for(var idx=startIdx;idx<lim;idx++) adding.push(this.objectAt(idx));
} else {
adding = addAmt;
}
this.enumerableContentDidChange(removeAmt, adding);
Ember.sendEvent(this, '@array:change', [this, startIdx, removeAmt, addAmt]);
var length = get(this, 'length'),
cachedFirst = cacheFor(this, 'firstObject'),
cachedLast = cacheFor(this, 'lastObject');
if (this.objectAt(0) !== cachedFirst) {
Ember.propertyWillChange(this, 'firstObject');
Ember.propertyDidChange(this, 'firstObject');
}
if (this.objectAt(length-1) !== cachedLast) {
Ember.propertyWillChange(this, 'lastObject');
Ember.propertyDidChange(this, 'lastObject');
}
return this;
},
// ..........................................................
// ENUMERATED PROPERTIES
//
'@each': Ember.computed(function() {
if (!this.__each) this.__each = new Ember.EachProxy(this);
return this.__each;
})
}) ;
描述
Ember.Array是ember.js对Array的类型的扩展定义,封装在一个Mixin之中。
也就是说需要混合Ember.Array到其他的类型中才能发挥作用。Ember.Array从Ember.Enumerable扩展而来,继续向Ember.MutableArray扩展,最终合并到Ember.ArrayProxy之中。而Ember.ArrayProxy又是Ember.ArrayController的基类。ember.js在Ember.Array上提供了许多关键的特性,用来支持ember.js对array的观察者模式的支持。
接下来,对其成员做一个简短的说明。
length属性:
用于提供array的length。
objectAt方法:
有一个idx参数,提供指定位置上的数组元素。
objectsAt方法:
有一个indexes参数,是一个数组,用来获取多个位置上的数组元素。
nextObject方法:
提供可枚举的nextObject方法实现。
'[]'计算属性:
返回自身,如果有set,将value赋值到length属性上。
firstObject计算属性:
提供firstObject的优化实现。
lastObject计算属性:
提供lastObject的优化实现。
contains方法:
提供contains方法的优化实现。
slice方法:
提供数组切片的方法,有两个参数,beginIndex, endIndex,对应切片的开始和结束的位置。如:
var arr = ['red', 'green', 'blue'];
arr.slice(0); // ['red', 'green', 'blue']
arr.slice(0, 2); // ['red', 'green']
arr.slice(1, 100); // ['green', 'blue']
indexOf方法:
有两个参数,object, startAt,startAt参数可以忽略。返回object所在的索引。没找到返回-1。startAt可以为负值,表示从结尾开始计算的索引。indexOf方法检索方向从前向后。
lastIndexOf方法:
有两个参数,object, startAt,startAt参数可以忽略。没找到返回-1。startAt可以为负值,表示从结尾开始计算的索引。lastIndexOf方法检索方向从后向前。
addArrayObserver方法:
支持数组@array观察者模式的内部方法,添加数组@array的观察者。有target, opts两个参数,如果提供了opts参数并有willChange和didChange成员,这两个方法会作为观察者,否则会从当前类中查找arrayWillChange方法和arrayDidChange方法作为观察者。会触发hasArrayObservers计算属性的观察者。
removeArrayObserver方法:
支持数组@array观察者模式的内部方法,去掉数组@array的观察者。有target, opts两个参数,如果提供了opts参数并有willChange和didChange成员,这两个方法会作为观察者,否则会从当前类中查找arrayWillChange方法和arrayDidChange方法作为观察者。会触发hasEnumerableObservers计算属性的观察者。
hasArrayObservers计算属性:
支持数组@array观察者模式的计算属性,用来判断是否存在@array观察者。
arrayContentWillChange方法:
是数组@array观察者前事件的触发方法,有startIdx, removeAmt, addAmt三个参数,会调用枚举的enumerableContentWillChange方法。
arrayContentDidChange方法:
是数组@array观察者后事件的触发方法,有startIdx, removeAmt, addAmt三个参数,会调用枚举的enumerableContentDidChange方法。可能会触发firstObject、lastObject等计算属性上的观察者。
@each计算属性:
返回的是Ember.EachProxy实例的包装,细节先不展开了。
六、一些帮助函数
> Ember.isNone 方法
定义
Ember.isNone = function(obj) {
return obj === null || obj === undefined;
};
描述
用于判断是否是null或undefined。
> Ember.isEmpty 方法
定义
Ember.isEmpty = function(obj) {
return Ember.isNone(obj) || (obj.length === 0 && typeof obj !== 'function') || (typeof obj === 'object' && Ember.get(obj, 'length') === 0);
};
描述
协助判断是否是空的方法。例:
Ember.isEmpty(); // true
Ember.isEmpty(null); // true
Ember.isEmpty(undefined); // true
Ember.isEmpty(''); // true
Ember.isEmpty([]); // true
Ember.isEmpty('Adam Hawkins'); // false
Ember.isEmpty([0,1,2]); // false
> Ember.isEqual 方法
定义
Ember.isEqual = function(a, b) {
if (a && 'function'===typeof a.isEqual) return a.isEqual(b);
return a === b;
};
描述
协助判断是否相等的方法。例:
Ember.isEqual('hello', 'hello'); // true
Ember.isEqual(1, 2); // false
Ember.isEqual([4,2], [4,2]); // false
> Ember.inspect 方法
定义
Ember.inspect = function(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj + '';
}
var v, ret = [];
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
v = obj[key];
if (v === 'toString') { continue; } // ignore useless items
if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; }
ret.push(key + ": " + v);
}
}
return "{" + ret.join(", ") + "}";
};
描述
提供获取对象摘要信息的方法。
> Ember.compare 方法
定义
Ember.compare = function compare(v, w) {
if (v === w) { return 0; }
var type1 = Ember.typeOf(v);
var type2 = Ember.typeOf(w);
var Comparable = Ember.Comparable;
if (Comparable) {
if (type1==='instance' && Comparable.detect(v.constructor)) {
return v.constructor.compare(v, w);
}
if (type2 === 'instance' && Comparable.detect(w.constructor)) {
return 1-w.constructor.compare(w, v);
}
}
// If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION,
// do so now.
var mapping = Ember.ORDER_DEFINITION_MAPPING;
if (!mapping) {
var order = Ember.ORDER_DEFINITION;
mapping = Ember.ORDER_DEFINITION_MAPPING = {};
var idx, len;
for (idx = 0, len = order.length; idx < len; ++idx) {
mapping[order[idx]] = idx;
}
// We no longer need Ember.ORDER_DEFINITION.
delete Ember.ORDER_DEFINITION;
}
var type1Index = mapping[type1];
var type2Index = mapping[type2];
if (type1Index < type2Index) { return -1; }
if (type1Index > type2Index) { return 1; }
// types are equal - so we have to check values now
switch (type1) {
case 'boolean':
case 'number':
if (v < w) { return -1; }
if (v > w) { return 1; }
return 0;
case 'string':
var comp = v.localeCompare(w);
if (comp < 0) { return -1; }
if (comp > 0) { return 1; }
return 0;
case 'array':
var vLen = v.length;
var wLen = w.length;
var l = Math.min(vLen, wLen);
var r = 0;
var i = 0;
while (r === 0 && i < l) {
r = compare(v[i],w[i]);
i++;
}
if (r !== 0) { return r; }
// all elements are equal now
// shorter array should be ordered first
if (vLen < wLen) { return -1; }
if (vLen > wLen) { return 1; }
// arrays are equal now
return 0;
case 'instance':
if (Ember.Comparable && Ember.Comparable.detect(v)) {
return v.compare(v, w);
}
return 0;
case 'date':
var vNum = v.getTime();
var wNum = w.getTime();
if (vNum < wNum) { return -1; }
if (vNum > wNum) { return 1; }
return 0;
default:
return 0;
}
};
描述
提供对象比较的方法,如果v>w,返回1,如果相等,返回0,如果v<w,返回-1。如:
Ember.compare('hello', 'hello'); // 0
Ember.compare('abc', 'dfg'); // -1
Ember.compare(2, 1); // 1