一、前言
Promises/A是由CommonJS組織制定的異步模式編程規范,有不少庫已根據該規范及后來經改進的Promises/A+規范提供了實現
如Q, Bluebird, when, rsvp.js, mmDeferred, jQuery.Deffered()等。
雖然上述實現庫均以Promises/A+規范作為實現基準,但由于Promises/A+是對Promises/A規范的改進和增強,因此深入學習Promises/A規范也是不可缺少的。
本文內容主要根據以下內容進行學習后整理而成,若有紕漏請各位指正,謝謝。
http://wiki.commonjs.org/wiki/Promises/A
由于篇幅較長特設目錄一坨
1. 基礎功能部分(1.1. 構造函數; 1.2. then函數的實現)
2. 輔助功能部分(2.1. Promise.resolve實現; 2.2. Promise.reject實現; 2.3. Promise.all實現; 2.4. Promise.race實現)
js中最常見的異步編程方式我想應該非回調函數不可了,優點是簡單明了。但如果要實現下面的功能——非連續移動的動畫效果,那是否還那么簡單明了呢?
var left = function(cb){ el.style.left = (el.offsetLeft + 200) + 'px'; cb && cb(); }; var el = document.getElementById('test'); // 臭名遠播的“回調地獄(callback hell)” setTimeout(function(){ left(function(){ setTimeout(function(){ left(function(){ setTimeout(function(){ left(); },2000); }); }, 2000); }); }, 2000);
傻眼了吧!下面我們看一下使用Promises/A規范異步模式的編碼方式吧!
var el = document.getElementById('test'); // 定義thenable對象 var thenable = {then: function(resolve, reject){ setTimeout(function(){ el.style.left = (el.offsetLeft + 200) + 'px'; // 觸發promise對象鏈中的下一個狀態為pending的promise對象的狀態變為fulfilled resolve && resolve(); }, 2000); }}; // 將thenable對象封裝為promise對象 var promise = Promise.resolve(thenable); // 訂閱promise對象狀態變化時所觸發的事件 promise .then(function(){ return thenable; }) .then(function(){ return thenable; });
也許你有著“我不入地獄誰入地獄”的豪情,但如果現在需求改為循環10次執行非連續移動呢?20次呢?這時我想連地獄的門在哪也難以找到了,但Promises/A的方式讓我們輕松應對。
var el = document.getElementById('test'); var thenable = {then: function(resolve, reject){ setTimeout(function(){ el.style.left = (el.offsetLeft + 200) + 'px'; resolve && resolve(); }, 2000); }}; var promise = Promise.resolve(thenable); var i = 0, count = 10; while (++i < 10){ promise = promise.then(function(){ return thenable; }); }
也許通過上述代碼我們已經了解到Promise可以讓我們以不同的方式編寫異步代碼,但也僅僅停留在按形式套代碼的層面上而已,只有從感性出發理解Promise的設計思想我想才能更好地掌握。以下內容是讀張鑫旭的《es6-javascript-promise-感性認知》和《js算法與思維方式》后的所思所想。
首先舉一個生活的例子,看看實際生活的處理邏輯和通過代的處理邏輯有什么出入吧!
例子:下班搭車去接小孩回家。
直覺思維分解上述句子會得出以下任務及順序:下班->搭車->到幼兒園(小學等)接小孩->(走路)回家。可以看到這種思維方式是任務+執行順序的,絲毫沒有帶任務間的時間距離。于是同步代碼可以寫成:
var 下班 = function(){}; var 搭車 = function(){}; var 接小孩 = function(){}; var 回家 = function(){}; 下班(); 搭車(); 接小孩(); 回家();
但實際執行時各任務均有可能因某些原因出現不同程度的延時,如下班時老板突然安排一項新任務(推遲10分鐘下班),錯過班車(下一班要等10分鐘),小孩正在搞衛生(20分鐘后搞完),回家。真實生活中我們能做的就是干等或做點其他事等到點后再繼續之前的流程!但程序中尤其是像JS這樣單線程程序是“等”不起的,于是出現異步模式:
var 下班 = function(nextTask){}; var 搭車 = function(nextTask){}; var 接小孩 = function(nextTask){}; var 回家 = function(nextTask){}; 下班(function(){ setTimeout(function(){ 搭車(function(){ setTimeout(function(){ 接小孩(function(){ setTimeout(function(){ 回家(); }, 20*60*1000); }); }, 10*60*1000); }); }, 10*60*1000); });
當回頭再看上面這段代碼時,會發現整個流程被任務間的時間距離拉得很遠“下班------------搭車------------接小孩-------------------回家”。回想一下真實生活中我們即使執行每個任務時均需要等待,但整個流程的抽象也只是“下班,等,搭車,等,接小孩,等,回家”。因此回調函數的異步模式與我們的思維模式相距甚遠,那么如何做到即告訴程序任務間的時間距離,又從代碼結構上淡化這種時間距離感呢?而Promise就是其中一種方式了!
從開發者角度(第三人稱)來看Promise作為任務間的紐帶存在,流程被抽象為“下班,promise,搭車,promise,接小孩,promise,回家”,而任務間的時間距離則歸并到任務本身而已。從程序執行角度(第一人稱)來看Promise為一個待定變量,但結果僅有兩種——成功和失敗,于是僅對待定變量設定兩種結果的處理方式即可。
// 任務定義部分 var 下班 = function(){};
下班.then = function(resovle){ setTimeout(function(){ resovle(); }, 10*60*1000); }; var 搭車 = function(){}; 搭車.then = function(resovle){ setTimeout(function(){ resovle(); }, 10*60*1000); }; var 接小孩 = function(){}; 接小孩.then = function(resovle){ setTimeout(function(){ resovle(); }, 20*60*1000); }; var 回家 = function(){}; // 流程部分 var p = new Promise(function(resolve, reject){ resolve(); }); p.then(function(){ 下班(); return 下班; }) .then(function(){ 搭車(); return 搭車; }) .then(function(){ 接小孩(); return 接小孩; }) .then(function(){ 回家(); });
看代碼結構被拉平了,但代碼結構的變化是表象,最根本的是任務間的時間距離被淡化了,當我們想了解工作流程時不會被時間距離分散注意力,當我們想知道各個任務的延時時只需查看任務定義本身即可,這就是關注點分離的一種表現哦!
經過上述示例我想大家已經嘗到了甜頭,并希望掌握這一武器從而逃離回調地獄的折磨了。下面就一起了解Promise及其API規范吧!
1. 有限狀態機
Promise(中文:承諾)其實為一個有限狀態機,共有三種狀態:pending(執行中)、fulfilled(執行成功)和rejected(執行失敗)。
其中pending為初始狀態,fulfilled和rejected為結束狀態(結束狀態表示promise的生命周期已結束)。
狀態轉換關系為:pending->fulfilled,pending->rejected。
隨著狀態的轉換將觸發各種事件(如執行成功事件、執行失敗事件等)。
2. 構造函數
Promise({Function} factory/*({Function} resolve, {Function} reject)*/) ,構造函數存在一個Function類型的入參factory,作為唯一一個修改promise對象狀態的地方,其中factory函數的入參resolve的作用是將promise對象的狀態從pending轉換為fulfilled,而reject的作用是將promise對象的狀態從pending轉換為rejected。
入參 void resolve({Any} val) ,當val為非thenable對象和promise對象時則會將val作為執行成功事件處理函數的入參,若val為thenable對象時則會執行thenable.then方法,若val為Promise對象時則會將該Promise對象添加到Promise對象單向鏈表中。
入參 void reject({Any} reason) ,reason不管是哪種內容均直接作為執行失敗事件處理函數的入參。
注意:關于拋異常的做法,同步模式為 throw new Error("I'm synchronous way!") ,而Promise規范的做法是 reject(new Error("I'm asynchronous way!"));
3. 實例方法
Promise then([{Function} onFulfilled[, {Function} onRejected]]) ,用于訂閱Promise對象狀態轉換事件,入參onFulfilled為執行成功的事件處理函數,入參onRejected為執行失敗的事件處理函數。兩者的返回值均作為Promise對象單向鏈表中下一個Promise對象的狀態轉換事件處理函數的入參。而then方法的返回值是一個新的Promise對象并且已添加到Promise對象單向鏈表的末尾。
Promise catch({Function} onRejected) ,相當于 then(null, onRejected) 。
4. 類方法
Promise Promise.resolve({Any} obj) ,用于將非Promise類型的入參封裝為Promise對象,若obj為非thenable對象則返回狀態為fulfilled的Promise對象,對于非若入參為Promise對象則直接返回。
Promise Promise.reject({Any} obj) ,用于將非Promise類型的入參封裝為狀態為rejected的Promise對象。
Promise Promise.all({Array} array) ,當array中所有Promise實例的狀態均為fulfilled時,該方法返回的Promise對象的狀態也轉為fulfilled(執行成功事件處理函數的入參為array數組中所有Promise實例執行成功事件處理函數的返回值),否則轉換為rejected。
Promise Promise.race({Array} array) ,當array中有一個Promise實例的狀態出現fulfilled或rejected時,該方法返回的Promise對象的狀態也轉為fulfilled或rejected。
5. thenable對象
擁有 then方法 的對象均稱為thenable對象,并且thenable對象將作為Promise對象被處理。
單看接口API是無法掌握Promise/A的特性的,下面通過示例說明:
示例1——鏈式操作+執行最近的事件處理函數
// 創建Promise實例p1 var p1 = new Promise(function(resovle, reject){ setTimout(function(){ console.log('hello1'); // 1秒后修改promise實例的狀態為fulfilled resolve('hello1'); },1000); }); // 訂閱p1的執行成功事件處理函數,并創建Promise實例p2 // 該處理函數將立即返回結果 var p2 = p1.then(function(val){ var newVal = 'hello2'; console.log(val); console.log(newVal); return newVal; }) // 訂閱p2的執行成功事件處理函數,并創建Promise實例p3 // 該處理函數返回一個Promise實例,并1秒后該Promise實例的狀態轉換為rejected var p3 = p2.then(function(val){ console.log(val); var tmp = new Promise(function(resolve, reject){ setTimout(function(){ reject(new Error('my error!')); }, 1000); }); return tmp; }); // 訂閱p3的執行成功事件處理函數,并創建Promise實例p4 // 由于p2的處理函數所返回的Promise實例狀態為rejected,因此p3的執行成功事件處理函數將不被執行,并且p3沒有執行失敗事件處理函數,因此會將控制權往下傳遞給p4的執行失敗事件處理函數。 var p4 = p3.then(function(val){ console.log('skip'); }) // 訂閱p4的執行成功事件處理函數,并創建Promise實例p5 var p5 = p4.catch(function(reason){ console.log(reason); });、
該示例的結果為:hello1 hello1 hello2 hello2 error:my error!。
示例2——事件處理函數晚綁定,同樣可被觸發
var p1 = new Promise(function(resolve, reject){ resolve('hello'); }); // Promise實例p1狀態轉換為fulfilled一秒后才綁定事件處理函數 setTimeout(function(){ p1.then(function(val){ console.log(val); }); }, 1000);
該示例的結果為: hello
由于Promises/A規范實際僅提供接口定義,并沒有規定具體實現細節,因此我們可以先自行作實現方式的猜想。
上述的示例1表明Promise是具有鏈式操作,因此Promise的內部結構應該是一個單向鏈表結構,每個節點除了自身數據外,還有一個字段用于指向下一個Promise實例。
構造函數的具體實現可以是這樣的
var Promise = exports.Promise = function(fn){ if (!(this instanceof iPromise)) return new iPromise(fn); var _ = this._ = {}; _.state = 0; // 0:pending, 1:fulfilled, 2:rejected _.thenables = []; // 單向鏈表 fn && fn(this.resolve.bind(this), this.reject.bind(this)); };
而then函數的具體實現為
Promise.prototype.then = function(fulfilledHandler, rejectedHandler){ var _ = this._; var promise = new Promise(); // 單向鏈表的邏輯結構 var thenable = { onFulfilled: fulfilledHandler, // 執行成功的事件處理函數 onRejected: rejectedHandler, // 執行失敗的事件處理函數 promise: promise // 下一個Promise實例 }; _.thenables.push(thenable); if (_.state !== 0){ window[window.setImmediate ? 'setImmediate' : 'setTimeout'].call(window, function(){ handle(_); }, 0); } return promise; };
但官方提供的實現方式卻比上述思路晦澀得多(源碼含適配nodejs和瀏覽器端的額外代碼干擾視線,因此我提取可直接在瀏覽器上使用的主邏輯部分出來,具體代碼請瀏覽:https://github.com/fsjohnhuang/iPromise/blob/master/theory/PromisesA/promise-6.0.0-browser.js)
基礎功能部分主要分為 構造函數 和 then函數的實現 兩部分,而 then函數的實現是理解的難點
var Promise = exports.Promise = function (fn) { if (typeof this !== "object") throw new TypeError("Promises must be constructed via new"); if (typeof fn !== "function") throw new TypeError("not a function"); var state = null; // 狀態,null:pending,true:fulfilled,false:rejected var value = null; // 當前promise的狀態事件處理函數(onFulfilled或onRejected)的入參 var deferreds = []; // 當前promise的狀態事件處理函數和promise鏈表中下一個promise的狀態轉換發起函數 var self = this; // 唯一的公開方法 this.then = function(onFulfilled, onRejected) { return new self.constructor(function(resolve, reject) { handle(new Handler(onFulfilled, onRejected, resolve, reject)); }); }; // 保存和執行deferreds數組中的元素 function handle(deferred) { if (state === null) { deferreds.push(deferred); return; } // asap的作用為將入參的操作壓入event loop隊列中 asap(function() { var cb = state ? deferred.onFulfilled : deferred.onRejected; if (cb === null) { (state ? deferred.resolve : deferred.reject)(value); return; } var ret; try { // 執行當前promise的狀態轉換事件處理函數 ret = cb(value); } catch (e) { // 修改promise鏈表中下一個promise對象的狀態為rejected deferred.reject(e); return; } // 修改promise鏈表中下一個promise對象的狀態為fulfilled deferred.resolve(ret); }); } // promise的狀態轉換發起函數,觸發promise的狀態從pending->fulfilled function resolve(newValue) { try { if (newValue === self) throw new TypeError("A promise cannot be resolved with itself."); if (newValue && (typeof newValue === "object" || typeof newValue === "function")) { var then = newValue.then; if (typeof then === "function") { // 將控制權移交thenable和promise對象,由它們來設置當前pormise的狀態和狀態轉換事件處理函數的實參 doResolve(then.bind(newValue), resolve, reject); return; } } state = true; value = newValue; finale(); } catch (e) { reject(e); } } // promise的狀態轉換發起函數,觸發promise的狀態從pending->rejected function reject(newValue) { state = false; value = newValue; finale(); } // 向鏈表的下一個promise移動 function finale() { for (var i = 0, len = deferreds.length; i < len; i++) handle(deferreds[i]); deferreds = null; } // 執行構造函數的工廠方法,由工廠方法觸發promise的狀態轉換 doResolve(fn, resolve, reject); }
我們可以通過 new Promise(function(resolve, reject){ resolve('hello'); }); 來跟蹤一下執行過程,發現重點在 doResolve(fn, resolve, reject) 方法調用中,該方法定義如下:
// 對狀態轉換事件處理函數進行封裝后,再傳給執行函數 function doResolve(fn, onFulfilled, onRejected) { // done作為開關以防止fn內同時調用resolve和reject方法 var done = false; try { fn(function(value) { if (done) return; done = true; onFulfilled(value); }, function(reason) { if (done) return; done = true; onRejected(reason); }); } catch (ex) { if (done) return; done = true; onRejected(ex); } }
doResovle僅僅是對resolve和reject方法進行封裝以防止同時被調用的情況而已,這時控制權到達 resolve方法 。由于resovle的入參為字符串類型,因此直接修改當前promise的狀態和保存狀態轉換事件處理函數的實參即可(若resolve的入參為thenable對象或Promise對象,則將控制權交給該對象,由該對象來設置當前promise的狀態和狀態轉換事件處理函數的實參),然后將控制權移交 finale方法 。finale方法內部會遍歷deffereds數組并根據狀態調用對應的處理函數和修改promise鏈表中下一個promise對象的狀態。
那么deffereds數組具體是什么呢?其實它就跟我之前猜想的thenables數組功能一致,用于保存狀態轉換事件處理函數和維護promise單向鏈表(不直接存放下一個promise對象的指針,而是存放下一個promise的resovle和reject方法)的。具體數據結構如下:
// 構造promise的鏈表邏輯結構 function Handler(onFulfilled, onRejected, resolve, reject) { this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; // 當前promise的狀態轉換事件處理函數 this.onRejected = typeof onRejected === "function" ? onRejected : null; // 當前promise的狀態轉換事件處理函數 this.resolve = resolve; // 設置鏈表中下一個promise的狀態為fulfilled this.reject = reject; // 設置鏈表中下一個promise的狀態為rejected }
若當前promise有deffered實例,那么則會執行handle函數中asap函數的函數入參
function() { var cb = state ? deferred.onFulfilled : deferred.onRejected; if (cb === null) { (state ? deferred.resolve : deferred.reject)(value); return; } var ret; try { // 執行當前promise的狀態轉換事件處理函數 ret = cb(value); } catch (e) { // 修改promise鏈表中下一個promise對象的狀態為rejected deferred.reject(e); return; } // 修改promise鏈表中下一個promise對象的狀態為fulfilled deferred.resolve(ret);
}
我覺得原實現方式不夠直白,于是改成這樣:
function(){ var cb = deferred[state ? 'onFulfilled' : 'onRejected']; var deferredAction = 'resolve', ret; try{ ret = cb ? cb(value) : value; } catch (e){ ret = e; deferredAction = 'reject'; } deferred[deferredAction].call(deferred, ret); }
文字太多了,還是看圖更清楚哦!
接下來的問題就是deffereds數組的元素是從何而來呢?那就要看看then函數了。
then函數代碼結構上很簡單,但設計上卻很精妙。
this.then = function(onFulfilled, onRejected){ return new self.constructor(function(resolve, reject) { handle(new Handler(onFulfilled, onRejected, resolve, reject)); }); }
為了好看些,我修改了一下格式:
this.then = function(onFulfilled, onRejected) { // 構造新的promise實例并返回,從而形成鏈式操作 return new Promise(function(resolve, reject) { var handler = new Handler(onFulfilled, onRejected, resolve, reject); /* 注意:這里利用了閉包特性,此處的handle并不是新Promise的handle函數,而是this.then所屬promise的handle函數。 因此handler將被添加到this.then所屬promise的deffereds數組中。 而onFulfilled和onRejected自然成為了this.then所屬promise的狀態轉換事件處理函數, 而resolve和reject依舊是新promise實例的狀態轉換觸發函數。 */ handle(handler); }); };
源碼讀后感:
通過閉包特性來讓鏈表后一個對象調用前一個對象的方法和變量,從而實現私有成員方法和屬性實在是過癮。比起我猜想的實現方式通過下劃線(_)提示API調用者該屬性下的均為私有成員的做法封裝性更完整。
輔助功能部分主要就是Promise.resolve、Promise.reject、Promise.all、Promsie.race的實現,它們均由基礎功能擴展而來。
作用:將非Promise對象轉換為Promise對象,而非Promise對象則被細分為兩種:thenable對象和非thenable對象。
thenable對象的then將作為Promise構造函數的工廠方法被調用
非thenable對象(Number、DOMString、Boolean、null、undefined等)將作為pending->fulfilled的事件處理函數的入參。
由于源碼中加入性能優化的代碼,因此我提出核心邏輯以便分析:
// 將非thenable對象構造為thenable對象 // 其then方法則返回一個真正的Promise對象 function ValuePromise(value) { this.then = function(onFulfilled) { if (typeof onFulfilled !== "function") return this; return new Promise(function(resolve, reject) { asap(function() { try { resolve(onFulfilled(value)); } catch (ex) { reject(ex); } }); }); }; }
/* 也可以將非thenable對象構造為Promise對象 function ValuePromise(value){ return new Promise(function(resolve){ resolve(value); }); }*/ Promise.resolve = function(value) { if (value instanceof Promise) return value; if (typeof value === "object" || typeof value === "function") { try { var then = value.then; if (typeof then === "function") { return new Promise(then.bind(value)); } } catch (ex) { return new Promise(function(resolve, reject) { reject(ex); }); } } return new ValuePromise(value); };
作用:創建一個狀態為rejected的promise對象,且入參將作為onRejected函數的入參。
Promise.reject = function(value) { return new Promise(function(resolve, reject) { reject(value); }); };
作用:返回的一個promise實例,且該實例當且僅當Promise.all入參數組中所有Promise元素狀態均為fulfilled時該返回的promise實例的狀態轉換為fulfilled(onFulfilled事件處理函數的入參為處理結果數組),否則轉換為rejected。
Promise.all = function(arr) { var args = Array.prototype.slice.call(arr); return new Promise(function(resolve, reject) { if (args.length === 0) return resolve([]); var remaining = args.length; function res(i, val) { try { if (val && (typeof val === "object" || typeof val === "function")) { var then = val.then; if (typeof then === "function") { then.call(val, function(val) { // 對于thenable和promise對象則訂閱onFulfilled事件獲取處理結果值 res(i, val); }, reject); return; } } args[i] = val; // 檢測是否所有入參都已返回值 if (--remaining === 0) { resolve(args); } } catch (ex) { reject(ex); } } for (var i = 0; i < args.length; i++) { res(i, args[i]); } }); };
作用:返回一個promise對象,且入參數組中一旦某個promise對象狀態轉換為fulfilled,則該promise對象的狀態也轉換為fulfilled。
Promise.race = function(values) { return new Promise(function(resolve, reject) { values.forEach(function(value) { // 將數組元素轉換為promise對象 Promise.resolve(value).then(resolve, reject); }); }); };
源碼實現的方式是即使第一個數組元素的狀態已經為fulfilled,但仍然會訂閱其他元素的onFulfilled和onRejected事件,依賴resolve函數中的標識位done來保證返回的promise對象的onFulfilled函數僅執行一次。我修改為如下形式:
Promise.race = function(values){ return new Promise(function(resolve, reject){ var over = 0; for (var i = 0, len = values.length; i < len && !over; ++i){ var val = values[i]; if (val && typeof val.then === 'function'){ val.then(function(res){ !over++ && resolve(res); }, reject); } else{ !over++ && resolve(val); } } }); };
雖然通過Promises/A規范進行異步編程已經舒坦不少,但該規范仍然不夠給力,于是出現了Promises/A+規范。后面我們繼續探討Promises/A+規范吧!
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4135149.html ^_^肥仔John
http://javascript.ruanyifeng.com/advanced/asynchronous.htm
http://www.zhangxinxu.com/wordpress/2014/02/es6-javascript-promise-%E6%84%9F%E6%80%A7%E8%AE%A4%E7%9F%A5/comment-page-1/
文章列表
留言列表