摘要:概述在之前,在中的異步編程都是采用回調(diào)函數(shù)和事件的方式,但是這種編程方式在處理復雜業(yè)務的情況下,很容易出現(xiàn)回調(diào)地獄,使得代碼很難被理解和維護。如果不設(shè)置回調(diào)函數(shù),內(nèi)部的錯誤不會反應到外部。
本文是基于對阮一峰的Promise文章的學習整理筆記,整理了文章的順序、增加了更多的例子,使其更好理解。
1. 概述在Promise之前,在js中的異步編程都是采用回調(diào)函數(shù)和事件的方式,但是這種編程方式在處理復雜業(yè)務的情況下,很容易出現(xiàn)callback hell(回調(diào)地獄),使得代碼很難被理解和維護。
Promise就是改善這種情形的異步編程的解決方案,它由社區(qū)最早提出和實現(xiàn),es6將其寫進了語言標準,統(tǒng)一了用法,并且提供了一個原生的對象Promise。
2. 理解Promise我們通過一個簡單例子先來感受一下Promise。
var p = new Promise(function (resolve, reject) { // ... if(/* 異步操作成功 */){ resolve(ret); } else { reject(error); } }); p.then(function (value) { // 完成態(tài) }, function (error) { // 失敗態(tài) });
我們需要關(guān)注的是
Promise的構(gòu)造函數(shù)
resolve() , reject()
then()
2.1 Promise構(gòu)造函數(shù)我們在通過Promise構(gòu)造函數(shù)實例化一個對象時,會傳遞一個函數(shù)作為參數(shù),那么這個函數(shù)有什么特點?
答案就是在新建一個Promise后,這個函數(shù)會立即執(zhí)行。
let promise = new Promise(function (reslove, reject) { console.log("Promise"); }); console.log("end");
執(zhí)行結(jié)果如下:
可以看到是先輸出了Promise,再輸出了end。
2.2 resolve/reject在Promise中,對一個異步操作做出了抽象的定義,Promise操作只會處在3種狀態(tài)的一種,他們之間的轉(zhuǎn)化如圖所示
注意,這種狀態(tài)的改變只會出現(xiàn)從未完成態(tài)向完成態(tài)或失敗態(tài)轉(zhuǎn)化,不能逆反。完成態(tài)和失敗態(tài)不能互相轉(zhuǎn)化,而且,狀態(tài)一旦轉(zhuǎn)化,將不能更改。
只有異步操作的結(jié)果可以決定當前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài)。這也是Promise這個名字的由來,它的英語意思是承諾,表示其他手段無法改變。
在聲明一個Promise對象實例時,我們傳入的匿名函數(shù)參數(shù)中:
resolve就對應著完成態(tài)之后的操作
reject對應著失敗態(tài)之后的操作
2.3 then()那么問題來了,then()方法有什么作用?resolve和reject又是從哪里傳遞過來的?
其實這兩個問題是一個問題,在實例化一個Promise對象之后,我們調(diào)用該對象實例的then()方法傳遞的兩個參數(shù)中:
第一個參數(shù)(函數(shù))對應著完成態(tài)的操作,也就是resolve
第二個參數(shù)(函數(shù))對應著失敗態(tài)的操作,也就是reject
那就是說,在Promise中是通過then()方法來指定處理異步操作結(jié)果的方法。
2.4 實際案例到這里我們明白了Promise的語法,也了解了Promise中函數(shù)是如何執(zhí)行的,結(jié)合一個實際的案例,來加深對Promise的理解。
我們來實現(xiàn)一個異步加載圖片的函數(shù)
function loadImageAsync(url) { return new Promise(function (reslove, reject) { var img = new Image(); img.onload = function () { reslove(); } img.onerror = function () { reject(); } console.log("loading image"); img.src = url; }); } var loadImage1 = loadImageAsync("https://www.google.co.jp/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"); loadImage1.then(function success() { console.log("success"); }, function fail() { console.log("fail"); }); var loadImage2 = loadImageAsync("1.png"); loadImage2.then(function success() { console.log("success"); }, function fail() { console.log("fail"); });
我們在chrome中執(zhí)行,先是傳遞一個有效的url,再傳遞一個無效的url,執(zhí)行的效果為:
3. Promise進階 3.1 resolve/reject的參數(shù)reject函數(shù)的參數(shù)一般來說是Error對象的實例,而resolve函數(shù)的參數(shù)除了正常的值外,還可能是另一個Promise實例,表示異步操作的結(jié)果有可能是一個值,也有可能是另一個異步操作。
var p1 = new Promise( function(resolve, reject) { // ... }); var p2 = new Promise( function(resolve, reject) { // ... resolve(p1); });
代碼分析:p1和p2都是Promise的實例,p2中的resolve方法將p1作為參數(shù),即一個異步操作的結(jié)果是返回另一個異步操作。
注意,這時p1的狀態(tài)就會傳遞給p2,也就是說,p1的狀態(tài)決定了p2的狀態(tài),他們之間的關(guān)系是
舉個例子
console.time("Promise example start") var p1 = new Promise( (resolve, reject) => { setTimeout(() => resolve("hi"), 3000); }); var p2 = new Promise( (resolve, reject) => { setTimeout(() => resolve(p1), 10); }); p2.then( ret => { console.log(ret); console.timeEnd("Promise example end") });
我們在node環(huán)境下運行以上代碼,執(zhí)行結(jié)果為:
從執(zhí)行時間可以看到,p2會等待p1的執(zhí)行結(jié)果,然后再執(zhí)行,從輸出hi可以看到p1完成狀態(tài)轉(zhuǎn)變之后,傳遞給resolve(或者reject)的結(jié)果會傳遞給p2中的resolve。
3.2 then()從上面的例子,我們可以了解到then()方法是Promise實例的方法,即Promise.prototype上的,它的作用是為Promise實例添加狀態(tài)改變時的回調(diào)函數(shù),這個方法的第一個參數(shù)是resolved狀態(tài)的回調(diào)函數(shù),第二個參數(shù)(可選)是rejected狀態(tài)的回調(diào)函數(shù)。
那么then()方法的返回值是什么?then方法會返回一個新的Promise實例(注意,不是原來那個Promise,原來那個Promise已經(jīng)承諾過,此時繼續(xù)then就需要新的承諾~~),這樣的設(shè)計的好處就是可以使用鏈式寫法。
還有一個點,就是鏈式中的then方法(第二個開始),它們的resolve中的參數(shù)是什么?答案就是前一個then()中resolve的return語句的返回值。
來一個示例:
var p1 = new Promise( (resolve, reject) => { setTimeout(() => resolve("p1"), 10); }); p1.then( ret => { console.log(ret); return "then1"; }).then( ret => { console.log(ret); return "then2"; }).then( ret => { console.log(ret); });
在node環(huán)境下執(zhí)行,執(zhí)行結(jié)果為:
3.3 catch()錯誤處理catch()方法是Promise實例的方法,即Promise.prototype上的屬性,它其實是.then(null, rejection)的簡寫,用于指定發(fā)生錯誤時的回調(diào)。
這個方法其實很簡單,在這里并不想討論它的使用,而是想討論的是Promise中的錯誤的捕抓和處理。
3.3.1 Error對象的傳遞性Promise對象的Error對象具有冒泡性質(zhì),會一直向后傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲,示例代碼如下:
var p = new Promise( (resolve, reject) => { setTimeout(() => resolve("p1"), 10); }); p.then( ret => { console.log(ret); throw new Error("then1"); return "then1"; }).then( ret => { console.log(ret); throw new Error("then2"); return "then2"; }).catch( err => { // 可以捕抓到前面的出現(xiàn)的錯誤。 console.log(err.toString()); });
執(zhí)行結(jié)果如下
在第一個then中拋出了一個錯誤,在最后一個Promise對象中可以catch到這個錯誤。
因為有這種方便的錯誤處理機制,所以一般來說不要在then方法里面定義reject狀態(tài)的回調(diào)函數(shù), 而是使用catch方法
3.3.2 vs try/catch跟傳統(tǒng)的try/catch不同的是,如果沒有使用catch方法指定錯誤處理回調(diào)函數(shù),則Promise對象拋出的錯誤不會傳遞到外層代碼(在chrome會報錯)
Node.js有一個unhandledRejection事件,專門監(jiān)聽未捕獲的reject錯誤。以下代碼就是在node環(huán)境下運行。
var p = new Promise((resolve, reject) => { resolve(x + 2); }); p.then( () => { console.log("nothing"); });3.3.3 catch()的返回值
沒錯,既然catch()是.then(null, rejection)的別名,那么catch()就會返回一個Promise對象,因此在后面還可以接著調(diào)用then方法,示例代碼如下:
var p = new Promise((resolve, reject) => { resolve(x + 2); }); p.then( () => { console.log("nothing"); }).catch( err => { console.log(err.toString()); return "catch"; }).then( ret => { console.log(ret); });
當出錯時,catch會先處理之前的錯誤,然后通過return語句,將值繼續(xù)傳遞給后一個then方法中。
如果沒有報錯,則跳過catch,示例如下:
var p = new Promise((resolve, reject) => { resolve("p"); }); p.then( ret => { console.log(ret); return "then1"; }).catch( err => { console.log(err.toString()); return "catch"; }).then( ret => { console.log(ret); });4. Promise對象方法 4.1 Promise.all()
Promise.all()方法用于將多個Promise實例,包裝成一個新的Promise實例,例如
var p = Promise.all([p1, p2, p3]);
新的Promise實例p的狀態(tài)由p1, p2, p3決定:
當p1, p2, p3的狀態(tài)都為完成態(tài)時,p為完成態(tài)。
p1, p2, p3中任一一個狀態(tài)為失敗態(tài),則p為失敗態(tài)。
4.2 Promise.race()Promise.race方法同樣是將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.race([p1, p2, p3]);
不同的是,只要p1, p2, p3中任意一個實例率先改變狀態(tài),則p的狀態(tài)就跟著改變,而且狀態(tài)由率先改變的實例決定。
var p = Promise.race([ new Promise(resolve => { setTimeout(() => resolve("p1"), 10000); }), new Promise((resolve, reject) => { setTimeout(() => reject(new Error("time out")), 10); }) ]); p.then( ret => console.log(ret)) .catch( err => console.log(err.toString()));4.3 Promise.resolve()
Promise.resolve()可以將現(xiàn)有的對象轉(zhuǎn)為Promise對象。
var p = Promise.resolve("p"); // 相當于 var p = new Promise(resolve => resolve("p"));
比較有意思的是Promise.resolve()會根據(jù)參數(shù)類型進行相應的處理,分幾種情況討論。
4.3.1 Promise實例參數(shù)是一個Promise實例,那么Promise.resolve將不做任何處理,直接返回這個實例。
4.3.2 thenable對象參數(shù)是一個thenable對象,也就是說對象是具有then方法的對象,但不是一個Promise實例(就跟類數(shù)組和數(shù)組的關(guān)系一樣),例如
let thenable = { then : function (resolve, reject) { resolve(42); } }; let p = Promise.resolve(thenable); p.then( ret => console.log(ret)); // 42
Promise.resolve方法會將這個對象轉(zhuǎn)為Promise對象,然后立即執(zhí)行thenable對象中的then方法,因為例子中的thenable對象的then方法中執(zhí)行了resolve,因此會輸出結(jié)果42。
4.3.3 其他參數(shù)如果參數(shù)是一個原始值,或者不具有then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態(tài)為resolve,然后直接將該參數(shù)傳遞給resolve方法。
var p = Promise.resolve("p"); p.then( ret => console.log(ret)); // p4.3.4 不帶任何參數(shù)
Promise.resolve方法不帶參數(shù)時,會直接返回一個resolve狀態(tài)的Promise對象。
需要注意的立即resolve的Promise對象,是在本輪事件循環(huán)的結(jié)束時,而不是下一輪事件循環(huán)的開始執(zhí)行。示例代碼:
setTimeout(() => console.log("3"), 0); var p = Promise.resolve(); p.then(() => console.log("2")); console.log("1");
輸出結(jié)果為:
4.4 Promise.reject()Promise.reject()返回一個新的Promise實例,該實例的狀態(tài)為rejected,對于傳入的參數(shù)的處理跟Promise.resolve類似,就是狀態(tài)都為rejected。
5. 兩個實用的方法 5.1 done()Promise對象的回調(diào)鏈,不管以then方法或者catch方法結(jié)尾,要是最后一個方法拋出錯誤,都有可能無法捕捉到,因為Promise內(nèi)部的錯誤不會冒泡到全局,因此,我們可以提供一個done方法,總是處理回調(diào)鏈的尾端,保證拋出任何可能出現(xiàn)的錯誤。
這個代碼的實現(xiàn)非常簡單
Promise.prototype.done = function (resolve, reject) { this.then(resolve, reject) .catch( function (reason) { // 拋出一個全局錯誤 setTimeout( () => { throw reason }, 0); }); } // 使用示例 var p = new Promise( (resolve, reject) => { resolve("p"); }); p.then(ret => { console.log(ret); return "then1"; }).catch( err => { console.log(err.toString()); }).then( ret => { console.log(ret); return "then2"; }).then( ret => { console.log(ret); x + 2; }).done();
這里為什么可以在全局拋出一個錯誤?原因就是setTimeout中的回調(diào)函數(shù)是在全局作用域中執(zhí)行的,因此拋出的錯誤就是在全局作用域上。
5.2 finally()finally方法用于指定不管Promise對象最后的狀態(tài)如何,都會執(zhí)行的操作,它與done方法最大的區(qū)別就是,它接受一個普通函數(shù)作為參數(shù),該函數(shù)不管怎么樣都必須執(zhí)行。
Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( ret => P.resolve(callback()).then( () => ret), err => P.resolve(callback()).then( () => {throw reason }) ); };5. Promise的優(yōu)劣勢
從上面幾個小節(jié)綜合來看,可以看到Promise其實就是做了一件事情,那就是對異步操作進行了封裝,然后可以將異步操作以同步的流程表達出來,避免了層層嵌套的回調(diào)函數(shù),同時提供統(tǒng)一的接口,使得控制異步操作更加容易。
但是,Promise也有一些缺點:
無法取消Promise,一旦新建它就會立即執(zhí)行,無法中途取消。
如果不設(shè)置回調(diào)函數(shù),Promise內(nèi)部的錯誤不會反應到外部。
當處于未完成態(tài)時,無法得知目前進展到哪一個階段。
6. Promise與generator的結(jié)合使用Generator函數(shù)來管理流程,遇到異步操作的時候,通常返回一個Promise對象。
function getFoo() { return new Promise( resolve => resolve("foo")); } var g = function * () { try { var foo = yield getFoo(); console.log(foo); } catch(e){} } function run(generator) { var it = generator(); function go(result) { if(result.done) return result.value; // 默認value是一個Promise,其實這里應該做判斷的 if(!(result.value instanceof Promise)){ throw Error("yield must follow an instanceof Promise"); } return result.value.then( ret => go(it.next(ret)) ).catch(err => go(it.throw(err))); } go(it.next()); } run(g);
上面代碼的Generator函數(shù)g之中,有一個異步操作getFoo,它返回的就是一個Promise對象。函數(shù)run用來處理這個Promise對象,并調(diào)用下一個next方法。
7. 來源個人博客
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/91221.html
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經(jīng)典面試題從輸入到頁面加載發(fā)生了什么這是一篇開發(fā)的科普類文章,涉及到優(yōu)化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結(jié)思考,循序漸進的理解 TypeScript。 網(wǎng)絡基礎(chǔ)知識之 HTTP 協(xié)議 詳細介紹 HTT...
摘要:的翻譯文檔由的維護很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。寫一個符合規(guī)范并可配合使用的寫一個符合規(guī)范并可配合使用的理解的工作原理采用回調(diào)函數(shù)來處理異步編程。 JavaScript怎么使用循環(huán)代替(異步)遞歸 問題描述 在開發(fā)過程中,遇到一個需求:在系統(tǒng)初始化時通過http獲取一個第三方服務器端的列表,第三方服務器提供了一個接口,可通過...
摘要:有兩個陌生的關(guān)鍵字,同時函數(shù)執(zhí)行結(jié)果似乎返回了一個對象。用來表示函數(shù)是異步的,定義的函數(shù)會返回一個對象,可以使用方法添加回調(diào)函數(shù)。如果的是對象會造成異步函數(shù)停止執(zhí)行并且等待的解決如果等的是正常的表達式則立即執(zhí)行。 視頻講解 關(guān)于異步處理,ES5的回調(diào)使我們陷入地獄,ES6的Promise使我們脫離魔障,終于、ES7的async-await帶我們走向光明。今天就來學習一下 async-a...
摘要:忍者級別的函數(shù)操作對于什么是匿名函數(shù),這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數(shù)是一個很重要且具有邏輯性的特性。通常,匿名函數(shù)的使用情況是創(chuàng)建一個供以后使用的函數(shù)。 JS 中的遞歸 遞歸, 遞歸基礎(chǔ), 斐波那契數(shù)列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執(zhí)行機制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機制,如果...
閱讀 1109·2021-11-16 11:45
閱讀 3131·2021-10-13 09:40
閱讀 721·2019-08-26 13:45
閱讀 1204·2019-08-26 13:32
閱讀 2175·2019-08-26 13:23
閱讀 916·2019-08-26 12:16
閱讀 2828·2019-08-26 11:37
閱讀 1754·2019-08-26 10:32