我一直覺得我對Promise比較瞭解,相關的方法已經很是熟悉了,直到我看到這篇文章,裏面提出了這樣一個問題:
Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四種 promises 的區別是什麼前端
//1 doSomething().then(function(){ return doSomethingElse(); }).then(finalHandler); //2 doSomething().then(function(){ doSomethingElse(); }).then(finalHandler); //3 doSomething().then(doSomethingElse()) .then(finalHandler); //4 doSomething().then(doSomethingElse) .then(finalHandler);
我當時看了是吃驚的,由於我想,這都什麼玩意兒!!!因此我把Promise的方法複習了一遍,而且仔細讀了上面提到的那篇文章,因而就有了這篇文章。es6
在前端開發的學習中,新工具層出不窮,理解當前的基礎是要理解過去,而後瞭解未來。就異步調用而言,ES6中引入Promise簡化異步操做,主要針對的問題就是回調函數的層層嵌套(金字塔問題),除了閱讀不方便以外,只能在當前回調函數函數內部處理異常,這個很難作。Promise經過then和catch方法實現鏈式調用,每一次調用都返回一個Promise對象,擺脫了回調函數層層嵌套的問題和異步代碼「非線性執行」的問題;另外,全部回調函數的報錯均可以經過Promise統一處理,catch能夠捕獲先前調用中全部的異常(冒泡特性)。可是Promise僅僅是對回調作了簡化處理,ES7中的async函數更厲害,結合Promise,徹底不用回調函數,以近似同步的寫法實現異步操做,所須要的僅僅是一個async和await關鍵字而已。本文僅介紹Promise對象,以及ES6中Promise對象具備的一些操做方法。json
ES6中原生實現了Promise對象,經過狀態傳遞異步操做的消息。Promise對象有三種狀態:pending(進行中)、resoleved(fulfilled,已完成)、rejected(已失敗),根據異步操做的結果決定處於何種狀態。一旦狀態改變就不可再變,狀態改變僅有兩種pending=>rejected、pending=>resolved。
優勢:避免了層層嵌套的回調函數,並提供了統一的接口,使異步操做更加容易。
缺點:沒法取消Promise;若是不設置回調函數,內部錯誤沒法反映到外部。數組
Promise構造函數接收兩個參數:resolve和reject,這是兩個由JavaScript引擎自動提供的函數,不用本身部署。resolve函數在異步操做成功時調用,做用是將Promise對象的狀態由pending變爲resolved,並將異步操做的結果傳遞出去。reject函數在異步操做失敗時調用,做用是將Promise對象的狀態由pending變爲reject,將異步操做報錯傳遞出去。
then方法能夠接受兩個回調函數做爲參數,第一個是Pormise對象的狀態變爲resolved時調用,另外一個是當Promise對象的狀態變爲rejected時調用,這兩個回調函數都接受Promise對象實例建立過程當中resolve函數和reject函數傳出的值做爲參數。第二個參數可選,事實上通常經過Promise.prototype.catch()調用發生錯誤時的回調函數,經過then調用異步操做成功時的回調函數。
實例1:promise
//返回Promise對象,setTimeout中傳遞的resolve參數爲’done’ function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100).then((value) => { console.log(value); }); //done
實例2:Promise執行流異步
//建立Promise實例 let promise = new Promise(function(resolve, reject) { console.log('Promise');//當即執行 resolve(); }); //resolved狀態調用在當前腳本全部同步任務執行完纔會執行 promise.then(function() { console.log('Resolved.'); }); //當即執行 console.log('Hi!');
對以上代碼,Promise新建後當即執行,因此首先輸出的是「Promise」。而後,then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行,因此「Resolved」最後輸出。async
reject函數在異步操做失敗時調用,所以參數經常是一個錯誤對象(Error Object);resolve函數在操做成功時調用,參數經常是正常值或者另外一個Promise實例,這表徵了異步嵌套,異步嵌套的外層Promise要等待內層Promise的狀態決定下一步狀態。函數
var p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) }) var p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2.then(result => console.log(result)) //p1 is rejected, p2 is the same as p1 .catch(error => console.log(error)) // Error: fail
因爲p2的resolve方法將p1做爲參數,p1的狀態決定了p2的狀態,若是p1的狀態是pending,p2的回調函數會等待p1的狀態改變;若是p1的狀態是resolved或rejected,p2的回調函數當即執行。p2的狀態在1秒以後改變,resolve方法返回的是p1。此時,因爲p2返回的是另外一個Promise,因此後面的then語句都變成針對後者(p1)。又過了2秒,p1變爲rejected,致使觸發catch方法指定的回調函數。工具
then方法爲Promise實例添加狀態改變時的回調函數,返回一個新的Promise實例,能夠採用鏈式寫法,前一個then方法的返回值做爲後一個then方法的參數:oop
getJSON("/posts.json").then(function(json) { // json comes from 「/posts.json」 return json.post; }).then(function(post) { //post comes from json.post // ... });
若是第一個then方法內的回調函數返回一個Promise對象,後續的then方法會根據這個新的Promise對象的狀態執行回調函數。
getJSON("/post/1.json").then(function(post) { return getJSON(post.commentURL); }).then(function funcA(comments) { console.log("Resolved: ", comments); }, function funcB(err){ console.log("Rejected: ", err); });
第一個then方法指定的回調函數,返回的是另外一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。若是變爲Resolved,就調用funcA,若是狀態變爲Rejected,就調用funcB。回調函數通常是匿名函數,上述僅僅是爲了便於理解寫成命名函數。
catch(rejection)方法是then(null,rejection)的別稱,僅僅當發生錯誤時執行,catch的存在是將錯誤回調函數從then()方法中剝離出來。
getJSON('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 處理 getJSON 和 前一個回調函數運行時發生的錯誤 console.log('發生錯誤!', error); });
Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。
getJSON('/post/1.json').then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { // some code }).catch(function(error) { // 處理前面三個Promise產生的錯誤 });
catch方法返回的仍是一個 Promise 對象,所以後面還能夠接着調用then方法。要是後續then方法裏面報錯,就與前面的catch無關了。若是最後一個catch方法內部拋出錯誤,是沒法捕獲的。爲了不潛在錯誤,最好是在最後用一個catch方法兜底。
用於將多個Promise實例包裝成一個新的Promise實例。若是內部參數不是Promise實例,就調用Promise.resolve()將參數轉換爲Promise實例。
var p = Promise.all([p1, p2, p3]);//接受一個Promise數組做爲參數
p的狀態由p一、p二、p3決定,呈現&關係,fulfilled對應1,rejected對應0,分紅兩種狀況:
(1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值(rejected的順序有沒有相似與操做的順序?),會傳遞給p的回調函數。
const databasePromise = connectDatabase(); const booksPromise = databasePromise .then(findAllBooks); const userPromise = databasePromise .then(getCurrentUser); Promise.all([ booksPromise, userPromise ]) .then(([books, user]) => pickTopRecommentations(books, user));
上面代碼中,booksPromise和userPromise是兩個並行執行的異步操做,只有等到它們的結果都返回了,纔會觸發pickTopRecommentations這個回調函數。
Promise.race()和Promise.all()一樣是將多個Promise實例包裝成一個新的Promise實例,可是隻要實例數組中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給新實例的回調函數。
var p = Promise.race([p1, p2, p3]);
Promise.race方法的參數與Promise.all方法同樣,若是不是 Promise 實例,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。
const p = Promise.race([ fetch('/resource-that-may-take-a-while'), new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('request timeout')), 5000) }) ]); p.then(response => console.log(response)); p.catch(error => console.log(error));
上面代碼中,若是5秒以內fetch方法沒法返回結果,變量p的狀態就會變爲rejected,從而觸發catch方法指定的回調函數。
將現有對象轉化爲Promise對象,根據參數不一樣有不一樣結果:
(1) 參數是一個Promise實例,Promise.resolve()將原對象返回;
(2) 參數是具備then方法的對象,Promise.resolve方法會將這個對象轉爲Promise對象,而後就當即執行thenable對象的then方法。
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
thenable對象的then方法執行後,對象p1的狀態就變爲resolved,從而當即執行最後那個then方法指定的回調函數,輸出42。
(3) 若是參數是一個原始值(基本類型值),或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態爲Resolved。
var p = Promise.resolve('Hello'); p.then(function (s){ console.log(s) }); // Hello
上面代碼生成一個新的Promise對象的實例p。因爲字符串Hello不屬於異步操做(判斷方法是它不是具備then方法的對象),返回Promise實例的狀態從一輩子成就是Resolved,因此回調函數會當即執行。Promise.resolve方法的參數,會同時傳給回調函數。
(4) Promise.resolve方法容許調用時不帶參數,直接返回一個Resolved狀態的Promise對象。因此,若是但願獲得一個Promise對象,比較方便的方法就是直接調用Promise.resolve方法。
var p = Promise.resolve(); p.then(function () { // ... });
當即resolve的Promise對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時,這個很好理解,經過resolve產生的Promise對象而後調用then函數和先產生Promise對象,對象轉換成resolved後再執行then函數是同樣的,都是在本輪事件輪詢的末尾執行。
//下一輪事件輪詢開始 setTimeout(function () { console.log('three'); }, 0); //本輪事件輪詢末尾 Promise.resolve().then(function () { console.log('two'); }); //當即執行 console.log('one'); // one two three
上面代碼中,setTimeout(fn, 0)在下一輪「事件循環」開始時執行,Promise.resolve()在本輪「事件循環」結束時執行,console.log(’one‘)則是當即執行,所以最早輸出。
Promise.reject()返回一個Promise實例,實例狀態爲rejected。Promise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。
實例1:
var p = Promise.reject('出錯了'); // 等同於var p = new Promise((resolve, reject) => reject('出錯了')) //參數就是’出錯了’ p.then(null, function (s) { console.log(s) });// 出錯了
實例2:
const thenable = { then(resolve, reject) { reject('出錯了'); } }; //參數就是thenable Promise.reject(thenable) .catch(e => { console.log(e === thenable) }) // true
上面代碼中,Promise.reject方法的參數是一個thenable對象,執行之後,後面catch方法的參數不是reject拋出的「出錯了」這個字符串,而是thenable對象。
Promises 給予咱們的就是在咱們使用異步Callback時丟失的最重要的語言基石: return, throw 以及堆棧。可是想要 promises 可以提供這些便利給你的前提是你知道如何正確的使用它們。
任何有可能 throw 同步異常的代碼都是一個後續會致使幾乎沒法調試異常的潛在因素。可是若是你將全部代碼都使用Promise.resolve() 封裝,那麼你老是能夠在以後使用 catch() 來捕獲它。所以方法2要優於方法1。
方法1:
new Promise(function(resolve,reject){ resolve(someSynchronousValue); }).then(/*-------------*/);
方法2:
function somePromiseAPI() { return Promise.resolve().then(function(){ doSomethinThatMayThrow(); return ‘foo’; }).then(/*------------*/); }
如下代碼等價:
somePromise().catch(function (err)){ //handle error }); ////////////////////////////////////// somePromise().then(null, function(err)) { //handle error }
可是如下代碼不等價:
somePromise().then(function(){ return someOtherPromise(); }).catch(function(err){ //error }); /////////////////////////////// somePromise().then(function(){ return someOtherPromise(); },function(err){ //error });
所以,當你使用 then(resolveHandler, rejectHandler) 這種形式時,rejectHandler 並不會捕獲由 resolveHandler 引起的異常。最好不使用then()的第二個參數,而是老是使用catch(),惟一例外是寫一些異步的Mocha測試時,使用then()的第二個參數,但願拋出用例的異常。
當咱們但願執行一個個的執行一個 promises 序列,即相似 Promise.all() 可是並不是並行的執行全部 promises。你可能天真的寫下這樣的代碼:
function executeSequentially(promise){ var result = Promise.resolve(); promises.forEach(function (promise)){ result = result.then(promise); }); return result; }
不幸的是,這份代碼不會按照你的指望去執行,你傳入 executeSequentially() 的 promises 依然會並行執行。其根源在於你所但願的,實際上根本不是去執行一個 promises 序列。依照 promises 規範,一旦一個 promise 被建立,它就被執行了。所以你實際上須要的是一個 promise factories 數組。
我知道你在想什麼:「這是哪一個見鬼的 Java 程序猿,他爲啥在說 factories?」 。實際上,一個 promises factory 是十分簡單的,它僅僅是一個能夠返回 promise 的函數:
function executeSequentially(promiseFactories){ var result = Promise.resolve(); promiseFactories,forEach(function (promiseFactory){ result = result.then(promiseFactory) }); return result; } function promiseFactory(){ return somethingThatCreatesAPromise(); }
爲什麼這樣就能夠了?這是由於一個 promise factory 在被執行以前並不會建立 promise。它就像一個 then 函數同樣,而實際上,它們就是徹底同樣的東西。若是你查看上面的 executeSequentially() 函數,而後想象 myPromiseFactory 被包裹在 result.then(...) 之中,也許你腦中的小燈泡就會亮起。在此時此刻,對於 promise 你就算是悟道了。
Promise.resolve(‘foo’).then(Promise.resolve(‘bar’)).then(function(result){ console.log(result); });
執行結果並不是是bar,而是foo,這是由於當then()接受非函數的參數時,會解釋爲then(null),這就致使前一個Promise的結果穿透到下面一個Promise。正確的寫法是在then()方法內部包含函數:
Promise.resolve(‘foo’).then(function(){ return Promise.resolve(‘bar’); }).then(function(result){ console.log(result); });
Promise.all()以一個Promise數組做爲輸入,返回一個新的Promise,特色在於它會並行執行數組中的每一個Promise,而且每一個Promise都返回後才返回結果數組,這就數組的異步版map/forEach方法。可是若是須要返回兩個不相關的結果,使用Promise.all()能夠產生兩個不相關的數組結果;可是若是後一結果要依靠前一個結果產生,此時在Promise裏使用嵌套也就能夠的:
getUserByName(‘bill’).then(function(user){ return getUserAccountById(user.id); }).then(function (userAccount){ /*-----------------*/ });
或者在內部使用嵌套:
getUserByName(‘bill’).then(function(user){ return getUserAccountById(user.id).then(function(userAccount){ /*--------------------*/ }); });
忘記使用catch:沒人能夠保證不出錯,因此仍是在最後加一個catch吧!
Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四種 promises 的區別是什麼
//1 doSomething().then(function(){ return doSomethingElse(); }).then(finalHandler); // doSomething()返回一個Promise實例,可是後續的then方法裏是一個匿名函數,該函數產生一個新的Promise實例並返回這個實例,所以finalHandler的參數就是這個實例的resolve返回值。 doSomething /-----------------/ doSomethingElse(undefined) /----------------------------------/ finalHandler(reulstOfDoSomethingElse) /--------------------------------------------------/ //2 doSomething().then(function(){ doSomethingElse(); }).then(finalHandler); //doSomething()返回一個Promise實例,可是後續的then方法裏是一個匿名函數,該函數產生一個新的Promise實例,可是因爲這個函數沒有返回值,所以finalHandler函數沒有參數。 doSomething /-----------------/ doSomethingElse(undefined) /----------------------------------/ finalHandler(undefined) /----------------------------------/ //3 doSomething().then(doSomethingElse()) .then(finalHandler); // doSomething()返回一個Promise實例,可是後續的doSomethingElse()是一個當即執行函數,不接受上一Promise實例的resolve參數,因此參數是undefined,這時doSomething()的Promise穿透到finalHandler,finalHandler的參數就是該Promise的resolve參數。 doSomething /-----------------/ doSomethingElse(undefined) /----------------------------------/ finalHandler(reulstOfDoSomething) /--------------------------------------------------/ //4 doSomething().then(doSomethingElse) .then(finalHandler); //doSomething()返回一個Promise實例,隨後調用doSomethingElse以上一實例的resolve參數產生第二個Promise,最後是finalHandler以上一實例的resolve參數產生第三個Promise。 doSomething /-----------------/ doSomethingElse(resultOfDoSomething) /----------------------------------/ finalHandler(reulstOfDoSomethingElse) /--------------------------------------------------/