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