一、前言
最近在研究Promises/A+規范及實現,而Promise/A+規范的制定則很大程度地參考了由日本geek cho45發起的jsDeferred項目(《JavaScript框架設計》提供該資訊,再次感謝),追本溯源地了解jsDeferred是十分有必要的,并且當你看過官網(http://cho45.stfuawsc.com/jsdeferred/)的新手引導后就會有種不好好學學就太可惜的感覺了,而只看API和使用指南是無法滿足我對它的好奇心的,通過解讀源碼讀透它的設計思想才是根本。
本文部分內容將和《JS魔法堂:剖析源碼理解Promises/A》中的內容作對比來講解。
由于內容較多,特設目錄一坨
1. 基礎功能部分
2. 輔助功能部分
jsDeferred的特點:
①. 內部通過單向鏈表結果存儲 成功事件處理函數、失敗事件處理函數 和 鏈表中下一個Deferred類型對象;
②. Deferred實例內部沒有狀態標識(也就是說Deferred實例沒有自定義的生命周期);
③. 由于Deferred實例沒有狀態標識,因此不支持成功/失敗事件處理函數的晚綁定;
④. Deferred實例的成功/失敗事件是基于事件本身的觸發而被調用的;
⑤. 由于Deferred實例沒有狀態標識,因此成功/失敗事件可被多次觸發,也不存在不變值作為事件處理函數入參的說法;
Promises/A的特點:
①. 內部通過單向鏈表結果存儲 成功事件處理函數、失敗事件處理函數 和 鏈表中下一個Promise類型對象;
②. Promise實例內部有狀態標識:pending(初始狀態)、fulfilled(成功狀態)和rejected(失敗狀態),且狀態為單方向移動“pending->fulfilled","pending->rejected";(也就是Promse實例存在自定義的生命周期,而生命周期的每個階段具備不同的事件和操作)
③. 由于Promise實例含狀態標識,因此支持事件處理函數的晚綁定;
④. Promise實例的成功/失敗事件函數是基于Promise的狀態而被調用的。
核心區別
Promises調用成功/失敗事件處理函數的兩種流程:
①. 調用resolve/reject方法嘗試改變Promise實例的狀態,若成功改變其狀態,則調用Promise當前狀態相應的事件處理函數;(類似于觸發onchange事件)
②. 通過then方法進行事件綁定,若Promise實例的狀態不是pending,則調用Promise當前狀態相應的事件處理函數。
由上述可以知道Promises的成功/失敗事件處理函數均基于Promise實例的狀態而被調用,而非成功/失敗事件。
jsDeferred調用成功/失敗事件處理函數的流程:
①. 調用call/fail方法觸發成功/失敗事件,則調用相應的事件處理函數。
因此jsDeferred的是基于事件的。
下列內容均為大概介紹API接口,具體用法請參考官網。
1. 構造函數
Deferred ,可通過 new Deferred() 或 Deferred() 兩種方式創建Deferred實例。
2. 實例方法
Deferred next({Function} fn) ,綁定成功事件處理函數,返回一個新的Deferred實例。
Deferred error({Function} fn) ,綁定失敗事件處理函數,返回一個新的Deferred實例。
Deferred call(val*) ,觸發成功事件,返回一個新的Deferred實例。
Deferred fail(val*) ,觸發失敗事件,返回一個新的Deferred實例。
3. 靜態屬性
{Function} Deferred.ok ,默認的成功事件處理函數。
{Function} Deferred.ng ,默認的失敗事件處理函數。
{Array} Deferred.methods ,默認的向外暴露的靜態方法。(供 Deferred.define方法 使用)
4. 靜態方法
{Function}Deferred Deferred.define(obj, list) ,暴露list制定的靜態方法到obj上,obj默認是全局對象。
Deferred Deferred.call({Function} fn [, arg]*) ,創建一個Deferred實例并且觸發其成功事件。
Deferred Deferred.next({Function} fn) ,創建一個Deferred實例并且觸發其成功事件,其實就是無法傳入參到成功事件處理函數的 Deferred.call() 。
Deferred Deferred.wait(sec) ,創建一個Deferred實例并且等sec秒后觸發其成功事件。
Deferred Deferred.loop(n, fun) ,循環執行fun并且上一個fun,最后一個fun的返回值將作為Deferred實例的成功事件處理函數的入參。
Deferred Deferred.parallel(dl) ,將dl中非Deferred對象轉換為Deferred對象,然后并行觸發dl中的Deferred實例的成功事件,當
所有Deferred對象均調用了成功事件處理函數后,返回的Deferred實例則觸發成功事件,并且所有返回值將被封裝為數組作為Deferred實例的成功事件處理函數的入參。
Deferred Deferred.earlier(dl) ,將dl中非Deferred對象轉換為Deferred對象,然后并行觸發dl中的Deferred實例的成功事件,當
其中一個Deferred對象調用了成功事件處理函數則終止其他Deferred對象的觸發成功事件,而返回的Deferred實例則觸發成功事件,并且那個被調用的成功事件處理函數的返回值為Deferred實例的成功事件處理函數的入參。
Boolean Deferred.isDeferred(obj) ,判斷obj是否為Deferred類型。
Deferred Deferred.chain(args) ,創建一個Deferred實例一次執行args的函數
Deferred Deferred.connect(funo, options) ,將某個函數封裝為Deferred對象
Deferred Deferred.register(name, fn) ,將靜態方法附加到Deferred.prototype上
Deferred Deferred.retry(retryCount, funcDeferred, options) ,嘗試調用funcDeffered方法(返回值類型為Deferred)retryCount,直到觸發成功事件或超過嘗試次數為止。
Deferred Deferred.repeat(n, fun) ,循環執行fun方法n次,若fun的執行事件超過20毫秒則先將UI線程的控制權交出,等一會兒再執行下一輪的循環。
jsDeferred采用DSL風格的API設計,語義化我喜歡啊!
1. 基礎功能部分
function Deferred () { return (this instanceof Deferred) ? this.init() : new Deferred() } // 默認的成功事件處理函數 Deferred.ok = function (x) { return x }; // 默認的失敗事件處理函數 Deferred.ng = function (x) { throw x }; Deferred.prototype = { // 初始化函數 init : function () { this._next = null; this.callback = { ok: Deferred.ok, ng: Deferred.ng }; return this; }};
Deferred.prototype.call = function (val) { return this._fire("ok", val) }; Deferred.prototype._filre = function(okng, value){ var next = "ok"; try { // 調用當前Deferred實例的事件處理函數 value = this.callback[okng].call(this, value); } catch (e) { next = "ng"; value = e; if (Deferred.onerror) Deferred.onerror(e); } if (Deferred.isDeferred(value)) { // 若事件處理函數返回一個新Deferred實例,則將新Deferred實例的鏈表指針指向當前Deferred實例的鏈表指針指向, // 這樣新Deferred實例的事件處理函數就會先與原鏈表中其他Deferred實例的事件處理函數被調用。 value._next = this._next; } else { if (this._next) this._next._fire(next, value); } return this; };
Deferred.prototype.fail = function (err) { return this._fire("ng", err) };
Deferred.prototype.next = function (fun) { return this._post("ok", fun) }; Deferred.prototype._post = function (okng, fun) { // 創建一個新的Deferred實例,插入Deferred鏈表尾,并將事件處理函數綁定到新的Deferred上 this._next = new Deferred(); this._next.callback[okng] = fun; return this._next; };
《JS魔法堂:剖析源碼理解Promises/A》中的官網實現示例是將事件處理函數綁定到當前的Promise實例,而不是新創的Promise實例。而jsDeferred則是綁定到新創建的Deferred實例上。這是因為Promise實例默認的事件處理函數為undefined,而Deferred是含默認的事件處理函數的。
Deferred.prototype.error = function (fun) { return this._post("ng", fun) };
2. 輔助功能部分
jsDeferred的基礎功能部分都十分好理解,我認為它的精彩之處在于類方法——輔助功能部分。
Deferred.define = function (obj, list) { if (!list) list = Deferred.methods; // 以全局對象作為默認的入潛目標 // 由于帶代碼運行在sloppy模式,因此函數內的this指針指向全局對象。若運行在strict模式,則this指針值為undefined。 // 即使被以strict模式運行的程序調用,本段程序依然以sloppy模式運行使用 if (!obj) obj = (function getGlobal () { return this })(); for (var i = 0; i < list.length; i++) { var n = list[i]; obj[n] = Deferred[n]; } return Deferred; };
當我第一次看新手引導中的示例代碼
Deferred.define();
next(function(){
............
}).next(function(){
...............
});
這不是就和jdk1.5的靜態導入 import static一樣嗎?!兩者同樣是以入侵的方式將類方法附加到當前執行上下文中,這種導入的方式有人喜歡有人明令禁止(原上下文被破壞,維護性大大降低)。而我則有一個準則,就是導入的類方法足夠少(5個左右,反正能看一眼API就記得那種),團隊的小伙伴們均熟知這些API,并且僅以此方式導入一個類的方法到當前執行上下文中。其實能滿足這些要求的庫不多,還不如起個短小精干的類名作常規導入更實際。這里扯遠了,我再看看 Deferred.define方法 吧,其實它除了將類方法導入到當前執行上下文,還可以導入到一個指定的對象中(這個方法比較中用!)
var ctx = {}; Deferred.define(ctx); ctx.next(function(){ .............. }).next(function(){ ............. });
Deferred.isDeferred = function (obj) { return !!(obj && obj._id === Deferred.prototype._id); }; // 貌似是Mozilla有個插件也叫Deferred,因此不能通過instanceof來檢測。cho45于是自定義標志位來作檢測,并在github上提交fxxking Mozilla,哈哈! Deferred.prototype._id = 0xe38286e381ae;
Deferred.wait = function (n) { var d = new Deferred(), t = new Date(); var id = setTimeout(function () { // 入參為實際等待毫秒數,由于各瀏覽器的setTimeout均有一個最小精準度參數(IE9+和現代瀏覽器為4msec,IE5.5~8為15.4msec),因此實際等待的時間一定被希望的長 d.call((new Date()).getTime() - t.getTime()); }, n * 1000); d.canceller = function () { clearTimeout(id) }; return d; };
剛看到該函數時我確實有點小雞凍,我們可以將《JS魔法堂:剖析源碼理解Promises/A》的第三節“從感性領悟”下的示例,寫得于現實生活的思路更貼近了。
// 任務定義部分 var 下班 = function(){}; var 搭車 = function(){}; var 接小孩 = function(){}; var 回家 = function(){}; // 流程部分 next(下班) .wait(10*60) .next(下班) .wait(10*60) .next(搭車) .wait(10*60) .next(接小孩) .wait(20*60) .next(回家);
該函數可為是真個jsDeferred最出彩的地方了,也是后續其他方法的實現基礎,它的功能是創建一個新的Deferred對象,并且異步執行該Deferred對象的call方法來觸發成功事件。針對運行環境的不同,它提供了相應的異步調用的實現方式并作出降級處理。
Deferred.next = Deferred.next_faster_way_readystatechange || Deferred.next_faster_way_Image || Deferred.next_tick || Deferred.next_default;
由淺入深,我們先看看使用setTimeout實現異步的 Deferred.next_default方法 (存在最小時間精度的問題)
Deferred.next_default = function (fun) { var d = new Deferred(); var id = setTimeout(function () { d.call() }, 0); d.canceller = function () { clearTimeout(id) }; if (fun) d.callback.ok = fun; return d; };
然后是針對nodejs的 Deferred.next_tick方法
Deferred.next_tick = function (fun) { var d = new Deferred(); // 使用process.nextTick來實現異步調用 process.nextTick(function() { d.call() }); if (fun) d.callback.ok = fun; return d; };
然后就是針對現代瀏覽器的 Deferred.next_faster_way_Image方法
Deferred.next_faster_way_Image = function (fun) { var d = new Deferred(); var img = new Image(); var handler = function () { d.canceller(); d.call(); }; img.addEventListener("load", handler, false); img.addEventListener("error", handler, false); d.canceller = function () { img.removeEventListener("load", handler, false); img.removeEventListener("error", handler, false); }; // 請求一個無效data uri scheme導致馬上觸發load或error事件 // 注意:先綁定事件處理函數,再設置圖片的src是個良好的習慣。因為設置img.src屬性后就會馬上發起請求,假如讀的是緩存那有可能還未綁定事件處理函數,事件已經被觸發了。 img.src = "data:image/png," + Math.random(); if (fun) d.callback.ok = fun; return d; };
最后就是針對IE5.5~8的 Deferred.next_faster_way_readystatechange方法
Deferred.next_faster_way_readystatechange = ((typeof window === 'object') && (location.protocol == "http:") && !window.opera && /\bMSIE\b/.test(navigator.userAgent)) && function (fun) { var d = new Deferred(); var t = new Date().getTime(); /* 原理: 由于瀏覽器對并發請求數作出限制(IE5.5~8為2~3,IE9+和現代瀏覽器為6), 因此當并發請求數大于上限時,會讓請求的發起操作排隊執行,導致延時更嚴重了。 實現手段: 以150毫秒為一個周期,每個周期以通過setTimeout發起的異步執行作為起始, 周期內的其他異步執行操作均通過script請求實現。 (若該方法將在短時間內被頻繁調用,可以將周期頻率再設高一些,如100毫秒) */ if (t - arguments.callee._prev_timeout_called < 150) { var cancel = false; var script = document.createElement("script"); script.type = "text/javascript"; // 采用無效的data uri sheme馬上觸發readystate變化 script.src = "data:text/javascript,"; script.onreadystatechange = function () { // 由于在一次請求過程中script的readystate會變化多次,因此通過cancel標識來保證僅調用一次call方法 if (!cancel) { d.canceller(); d.call(); } }; d.canceller = function () { if (!cancel) { cancel = true; script.onreadystatechange = null; document.body.removeChild(script); } }; // 不同于img元素,script元素需要添加到dom樹中才會發起請求 document.body.appendChild(script); } else { arguments.callee._prev_timeout_called = t; var id = setTimeout(function () { d.call() }, 0); d.canceller = function () { clearTimeout(id) }; } if (fun) d.callback.ok = fun; return d; };
Deferred.call = function (fun) { var args = Array.prototype.slice.call(arguments, 1); // 核心在Deferred.next return Deferred.next(function () { return fun.apply(this, args); }); };
Deferred.loop = function (n, fun) { // 入參n類似于Python中range的效果 // 組裝循環的配置信息 var o = { begin : n.begin || 0, end : (typeof n.end == "number") ? n.end : n - 1, step : n.step || 1, last : false, prev : null }; var ret, step = o.step; return Deferred.next(function () { function _loop (i) { if (i <= o.end) { if ((i + step) > o.end) { o.last = true; o.step = o.end - i + 1; } o.prev = ret; ret = fun.call(this, i, o); if (Deferred.isDeferred(ret)) { return ret.next(function (r) { ret = r; return Deferred.call(_loop, i + step); }); } else { return Deferred.call(_loop, i + step); } } else { return ret; } } return (o.begin <= o.end) ? Deferred.call(_loop, o.begin) : null; }); };
上述代碼的理解難點在于Deferred實例A的事件處理函數若返回一個新的Deferred實例B,而實例A的Deferred鏈表中原本指向Deferred實例C,那么當調用實例A的call方法時是實例C的事件處理函數先被調用,還是實例B的事件處理函數先被調用呢?這時只需細讀 Deferred.prototype.call方法 的實現就迎刃而解了,答案是先調用實例B的事件處理函數哦!
Deferred.parallel = function (dl) { // 對入參作處理 var isArray = false; if (arguments.length > 1) { dl = Array.prototype.slice.call(arguments); isArray = true; } else if (Array.isArray && Array.isArray(dl) || typeof dl.length == "number") { isArray = true; } var ret = new Deferred(), values = {}, num = 0; for (var i in dl) if (dl.hasOwnProperty(i)) (function (d, i) { // 若d為函數類型,則封裝為Deferred實例 // 若d既不是函數類型,也不是Deferred實例則報錯哦! if (typeof d == "function") dl[i] = d = Deferred.next(d); d.next(function (v) { values[i] = v; if (--num <= 0) { // 湊夠數就觸發事件處理函數 if (isArray) { values.length = dl.length; values = Array.prototype.slice.call(values, 0); } ret.call(values); } }).error(function (e) { ret.fail(e); }); num++; })(dl[i], i); // 當dl為空時觸發Deferred實例的成功事件 if (!num) Deferred.next(function () { ret.call() }); ret.canceller = function () { for (var i in dl) if (dl.hasOwnProperty(i)) { dl[i].cancel(); } }; return ret; };
通過源碼我們可以知道parallel的入參必須為函數或Deferred實例,否則會報錯哦!
Deferred.earlier = function (dl) { // 對入參作處理 var isArray = false; if (arguments.length > 1) { dl = Array.prototype.slice.call(arguments); isArray = true; } else if (Array.isArray && Array.isArray(dl) || typeof dl.length == "number") { isArray = true; } var ret = new Deferred(), values = {}, num = 0; for (var i in dl) if (dl.hasOwnProperty(i)) (function (d, i) { // d只能是Deferred實例,否則拋異常 d.next(function (v) { values[i] = v; // 一個Deferred實例觸發成功事件則終止其他Deferred實例觸發成功事件了 if (isArray) { values.length = dl.length; values = Array.prototype.slice.call(values, 0); } ret.call(values); ret.canceller(); }).error(function (e) { ret.fail(e); }); num++; })(dl[i], i); // 當dl為空時觸發Deferred實例的成功事件 if (!num) Deferred.next(function () { ret.call() }); ret.canceller = function () { for (var i in dl) if (dl.hasOwnProperty(i)) { dl[i].cancel(); } }; return ret; };
通過源碼我們可以知道earlier的入參必須為Deferred實例,否則會報錯哦!
Deferred.chain = function () { var chain = Deferred.next(); // 生成Deferred實例鏈表,鏈表長度等于arguemtns.length for (var i = 0, len = arguments.length; i < len; i++) (function (obj) { switch (typeof obj) { case "function": var name = null; // 通過函數名決定是訂閱成功還是失敗事件 try { name = obj.toString().match(/^\s*function\s+([^\s()]+)/)[1]; } catch (e) { } if (name != "error") { chain = chain.next(obj); } else { chain = chain.error(obj); } break; case "object": // 這里的object包含形如{0:function(){}, 1: Deferred實例}、Deferred實例 chain = chain.next(function() { return Deferred.parallel(obj) }); break; default: throw "unknown type in process chains"; } })(arguments[i]); return chain; };
Deferred.connect = function (funo, options) { var target, // 目標函數所屬的對象 func, // 目標函數 obj; // 配置項 if (typeof arguments[1] == "string") { target = arguments[0]; func = target[arguments[1]]; obj = arguments[2] || {}; } else { func = arguments[0]; obj = arguments[1] || {}; target = obj.target; } // 預設定的入參 var partialArgs = obj.args ? Array.prototype.slice.call(obj.args, 0) : []; // 指出成功事件的回調處理函數位于原函數的入參索引 var callbackArgIndex = isFinite(obj.ok) ? obj.ok : obj.args ? obj.args.length : undefined; // 指出失敗事件的回調處理函數位于原函數的入參索引 var errorbackArgIndex = obj.ng; return function () { // 改造成功事件處理函數,將預設入參和實際入參作為成功事件處理函數的入參 var d = new Deferred().next(function (args) { var next = this._next.callback.ok; this._next.callback.ok = function () { return next.apply(this, args.args); }; }); // 合并預設入參和實際入參 var args = partialArgs.concat(Array.prototype.slice.call(arguments, 0)); // 打造func的成功事件處理函數,內部將觸發d的成功事件 if (!(isFinite(callbackArgIndex) && callbackArgIndex !== null)) { callbackArgIndex = args.length; } var callback = function () { d.call(new Deferred.Arguments(arguments)) }; args.splice(callbackArgIndex, 0, callback); // 打造func的失敗事件處理函數,內部將觸發d的失敗事件 if (isFinite(errorbackArgIndex) && errorbackArgIndex !== null) { var errorback = function () { d.fail(arguments) }; args.splice(errorbackArgIndex, 0, errorback); } // 相當于setTimeout(function(){ func.apply(target, args) }) Deferred.next(function () { func.apply(target, args) }); return d; }; };
如何簡化將setTimeout、setInterval、XmlHttpRequest等異步API封裝為Deferred對象(或Promise)對象的步驟是一件值思考的事情,而jsDeferred的connect類方法提供了一個很好的范本。
Deferred.register = function (name, fun) { this.prototype[name] = function () { var a = arguments; return this.next(function () { return fun.apply(this, a); }); }; }; Deferred.register("loop", Deferred.loop); Deferred.register("wait", Deferred.wait);
Deferred.retry = function (retryCount, funcDeferred, options) { if (!options) options = {}; var wait = options.wait || 0; // 嘗試的間隔時間,存在最小時間精度所導致的延時問題 var d = new Deferred(); var retry = function () { // 有funcDeferred內部觸發事件 var m = funcDeferred(retryCount); m.next(function (mes) { d.call(mes); }). error(function (e) { if (--retryCount <= 0) { d.fail(['retry failed', e]); } else { setTimeout(retry, wait * 1000); } }); }; // 異步執行retry方法 setTimeout(retry, 0); return d; };
Deferred.repeat = function (n, fun) { var i = 0, end = {}, ret = null; return Deferred.next(function () { var t = (new Date()).getTime(); // 當fun的執行耗時小于20毫秒,則馬上繼續執行下一次的fun; // 若fun的執行耗時大于20毫秒,則將UI線程控制權交出,并將異步執行下一次的fun。 // 從而降低因循環執行耗時操作使頁面卡住的風險。 do { if (i >= n) return null; ret = fun(i++); } while ((new Date()).getTime() - t < 20); return Deferred.call(arguments.callee); }); };
通過剖析jsDeferred源碼我們更深刻地理解Promises/A和Promises/A+規范,也了解到setTimeout的延時問題和通過img、script等事件縮短延時的解決辦法(當然這里并沒有詳細記錄解決辦法的細節),最重要的是吸取大牛們的經驗和了解API設計的藝術。但這里我提出一點對jsDeferred設計上的吐槽,就是Deferred實例的私有成員還是可以通過實例直接引用,而不像Promises/A官網實現示例那樣通過閉包隱藏起來。
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4141918.html ^_^肥子John
文章列表