Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。(摘自<ECMAScript 6 入門>??,但是對于Promise的創建和執行流程,其實有很多人初學時,容易走入誤區。
今天就以一首歌來帶大家學習Promise。
Promise創建
Promise翻譯過來的意思就是”承諾”,其實Promise的創建本質就是一個許下承諾的過程,想到這我的腦海中不由得浮現了一首經典老歌,海鳴威 《你的承諾》。
歌中相知相戀的兩個人,奈何情深緣淺,終于還是分開,許下了“不為彼此難過, 過各自的生活”這樣的承諾。但是隨后歌曲又以”Oh baby 你答應我的我都記得 ,但是你卻忘了你的承諾 ,不是說好彼此都不再聯絡, 誰都別再犯錯”,暗示承諾的狀態發生改變。
由此不難想象以下兩種情況:
- 遵守承諾

- 違背承諾

同樣,JS中的Promise的創建也是許下代碼承諾的一種方式,它默認也包含三種狀態:
- pending (進行中)
- fulfilled(已成功)
- rejected(已失敗)
那么如何許下JS中的承諾呢:
var p = new Promise(function(){
// 回調函數
})
Promise中接收一個回調函數(簡單說就是一個容器),傳入某個未來才會結束的事件(通常是一個異步操作)的結果;此時打印,你會得到一個Promise實例(實例化對象),展開之后默認顯示:
Promise {<pending>}
[[Prototype]]: Promise,
[[PromiseState]]: "pending",
[[PromiseResult]]: undefined,
此時我們會看到
[[PromiseState]]用于存儲Promise實例(當前承諾)的狀態 ,默認顯示pending ;
[[PromiseResult]]用于表示Promise實例中存儲的數據 , 默認顯示undefined;
[[Prototype]]表示原型 以后再做講解
但是有人就會問了,既然有三種狀態,而默認是pending(進行中)狀態,那么我們如何將當前Promise實例的狀態改為另外兩種:
這就不得不提到Promise的第一特征了:
對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。
可以理解為,狀態并不是自行改變的,而是由異步(同步也可以)的結果決定了.。
所以Promise在創建過程中,傳入的回調函數接收了改變狀態的兩個方法 ,resolve,reject作為形式參數傳入.
var p = new Promise(function(resolve,reject){//形式參數
// 回調函數
})
resolve() 將Promise實例的狀態 由pending(進行中) 改為 fulfilled(已成功)
reject() 將Promise實例的狀態 由pending(進行中) 改為rejected(已失敗)
而且resolve() reject() 方法在調用時 還可以接收一個參數,作為數據傳入,存儲到Promise實例中。
同步代碼展示:
var p = new Promise(function(resolve,reject){//形式參數
// 回調函數
resolve(111)
})
console.log(p)
結果:
Promise {<fulfilled>: 111}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled" // 已成功
[[PromiseResult]]: 111 // 存儲數據
var p = new Promise(function (resolve, reject) {//形式參數
// 回調函數
reject(222)
})
Promise {<rejected>: 111}
[[Prototype]]: Promise
[[PromiseState]]: "rejected" // 已失敗
[[PromiseResult]]: 222 // 存儲數據
Uncaught (in promise) 111 // 報錯 (可以不用理會 后面會被then() / catch() 方法捕獲)
resolve() 和 reject() 方法,在同步代碼中調用,會立即改變Promise實例的狀態。
異步代碼展示:
var p = new Promise(function (resolve, reject) {//形式參數
setTimeout(function () {
resolve(111);
console.log("line 24:", p);
}, 2000)
})
console.log("line 27:", p);
結果依次為:
line 27: Promise {<pending>}
line 24: Promise {<fulfilled>: 111}
-------------------------------------兩次代碼請分開執行-------------------------------------------------
var p = new Promise(function (resolve, reject) {//形式參數
setTimeout(function () {
reject(111);
console.log("line 24:", p);
}, 2000)
})
console.log("line 27:", p);
結果依次為:
line 27: Promise {<pending>}
...2s后
line 24: Promise {<rejected>: 111}
報錯: 2.html:60 Uncaught (in promise) 111 (可以不用理會 后面會被then() / catch() 方法捕獲)
resolv e() 和 reject() 方法,在異步代碼中調用:由于JS是單線程,會優先在主線程執行同步代碼,異步代碼會放到任務隊列中,所以在頁面加載時,,Promise實例為pending(進行中)狀態,等待主線程執行完畢,任務隊列通知主線程,異步任務可以執行了,該任務才會進入主線程執行。
此時setTimeout中的回調函數被調用,執行了resolve() 或 reject() 方法, Promise實例會隨之改變狀態,并存儲傳入的數據。
那么resolve() reject() 方法,可以重復調用么?
這就要講到Promise的第二大特征:一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型) 注意,為了行文方便,本章后面的resolved統一只指fulfilled狀態,不包含rejected狀態。
由此可見Promise本質,就是一個狀態機,當該Promise對象創建出來之后,其狀態就是pending(進行中),然后通過程序來控制到底是執行已完成,還是執行已失敗。
因為Promise處理的是異步任務,所以我們還得對Promise做監聽,當Promise的狀態發生變化時,我們要執行相應的函數(then catch finally)。
Promise的動態方法
所謂Promise動態方法,即將封裝的方法存儲在Promise的原型對象(prototype)上,供所有通過構造函數創建的實例化對象使用。
Promise.prototype.then()
Promise 實例具有then方法,它的作用是為 Promise 實例添加狀態改變時的回調函數,前面說過 Promise實例的狀態可以從pending(進行中) 變為 fulfilled(已成功)或rejected(已失敗),所以then方法中接收兩個回調函數(它們都是可選的)。
p.then(resolveHandler,rejectHandler)
resolveHandler 第一參數,用于指定Promise實例,由pending(進行中) 變為 fulfilled(已成功)時執行的回調函數。
rejectHandler 第二參數,用于指定Promise實例,由pending(進行中) 變為 rejected(已失敗)時執行的回調函數。
可以理解為 then()方法中的兩個回調函數,提前規定好了狀態改變時需要執行的內容,等待以后Promise實例的狀態之后再執行對應的回調函數 (本質還是回調函數,要用魔法打敗魔法)。
注意:resolveHandler rejectHandler 中可以傳入一個形式參數 接收Promise實例中存儲數據。
// 等待兩秒后 隨機一個0-1的數,如果 num>=0.5 就變為fulfilled,否則變為rejected
var p = new Promise(function (resolve, reject) {//形式參數
setTimeout(function () {
var num = Math.random();
if (num >= 0.5) {
resolve(111)
} else {
reject(2222);
}
console.log("line 24:", p);
}, 2000)
})
console.log("line 27:", p);
p.then(function (arg) {
console.log("line 50", arg);
}, function (arg) {
console.log("line 52", arg);
})
情況1: (num>=0.5)
line 27: Promise {<pending>}
...2秒后
line 24: Promise {<fulfilled>: 111}
line 50 111
情況2: (num<0.5)
line 27: Promise {<pending>}
...2秒后
2.html:81 line 24: Promise {<rejected>: 2222}
2.html:89 line 52 2222
解析:在異步代碼中調用: 由于JS是單線程,會優先在主線程執行同步代碼,異步代碼會放到任務隊列中,所以
- Promise創建完畢,Promise實例為pending(進行中)狀態;
- 調用 then()方法,傳入兩個回調函數,提前規定好了狀態改變時需要執行的內容,等待以后Promise實例的狀態之后再執行對應的回調函數;
- 等待主線程執行完畢,任務隊列通知主線程,異步任務可以執行了,該任務才會進入主線程執行。=> 隨機一個0-1的隨機數,根據結果改變Promise實例的狀態 ,存儲傳入的數據 => 執行then方法中對應的回調函數。
還有一個有趣的地方:then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以采用鏈式寫法,即then方法后面再調用另一個then方法。
var p = new Promise(function (resolve, reject) {//形式參數
setTimeout(function () {
resolve(1);
}, 2000);
})
p.then(function (num) {
console.log("line 64:", num);
return 2;
}).then(function (num) {
console.log("line 67:", num);
return 2;
})
結果:
line 64: 1
line 67: 2
上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成以后,會將返回結果作為參數,傳入第二個回調函數。當然這里有一個前提,那就是Promise實例的狀態為已成功且代碼執行過程中不出錯的情況下。萬一出錯的話,那我們就可以使用then()方法的第二個回調函數或者catch()方法捕獲錯誤。
Promise.prototype.catch()
catch()方法是.then(null, rejection)或.then(undefined, rejection)的別名,用于指定發生錯誤時的回調函數。
function readText(url) {
var p = new Promise(function (resolve, reject) {
$.ajax({
type: "get",
url: url,
success: function (text) {
resolve(text);
},
error:function(err){
reject(err);
}
})
})
return p; // {pending}
}
readText("../data/1.txt").then(res => {
console.log("line 93", res)
}).catch(err => {
console.log(err);
})
上面代碼中,readText()方法返回一個 Promise 對象,如果該對象狀態變為resolved,則會調用then()方法指定的回調函數;如果異步操作拋出錯誤,狀態就會變為rejected,就會調用catch()方法指定的回調函數,處理這個錯誤。另外,then()方法指定的回調函數,如果運行中拋出錯誤,也會被catch()方法捕獲。
then和catch的鏈式操作
then()和catch()配合使用,實現的鏈式操作效果更佳。
var p = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1);
}, 2000);
})
console.log("line 21", p);
p.then(function (num) {
console.log("line 23:", num);
return 2;
}).then(function (num) {
console.log("line 25:", num);
}).then(function (num) {
console.log("line 28:", num);
}).catch(function (err) {
console.log(err);
})
結果:
line 21 Promise {<pending>}
...等待2s后
line 23: 1
line 25: 2
line 28: 3
上面的代碼使用then catch方法進行鏈式調用,依次指定了三個then()的回調函數以及一個catch()的回調函數。第一個回調函數完成以后,會將返回結果作為參數,傳入第二個回調函數,同樣第二個回調函數完成后,會將返回結果作為參數,傳入第三個回調函數,以此類推。當然這里同樣也有一個前提,那就是Promise實例的狀態為已成功且代碼執行過程中不出錯的情況下。萬一出錯的話,那catch()方法剛好捕獲鏈式操作過程中出現的錯誤。
如果then方法中回調函數的返回值也是一個Promise實例;
function readText(url) {
var p = new Promise(function (resolve, reject) {
$.ajax({
type: "get",
url: url,
success: function (text) {
resolve(text);
},
error: function (err) {
reject(err);
}
})
})
return p; // {pending}
}
// ajax恐怖回調的解決方式1:
// T = T1 + T2 + T3 (時間)
var str = "";
readText("../data/1.txt").then(txt => {
console.log("line 36", txt);
str += txt;
return readText("../data/2.txt");
}).then(txt => {
console.log("line 40", txt);
str += txt;
return readText("../data/3.txt");
}).then(txt => {
console.log("line 44", txt);
str += txt;
return str;
}).then(txt => {
console.log("line 48", txt);
}).catch(err => {
console.log("失敗:", err);
})
結果:
line 36 11111
line 40 22222
line 44 33333
line 48 111112222233333
上面代碼中,第一個then方法指定的回調函數,返回的是另一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。如果變為resolved,繼續鏈式調用后面then方法中指定的回調函數,如果狀態變為rejected,就會被catch方法捕獲。
注意:Promise 實例的錯誤具有“冒泡”性質,會一直向后傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。
Promise的靜態方法
所謂Promise靜態方法,即將封裝的方法存儲在構造函數Promise上,由構造函數自身調用。
Promise.all
Promise.all()方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.all([p1, p2, p3]);
相較于then方法的鏈式操作,Promise.all()更注重并發,即依次按順序發送多個請求,等待所有的請求均有結果之后,對應請求順序將數據依次存放到數組中返回。
模擬封裝(簡易版)
Promise.myAll = function (list) {
return new Promise(function (resolve, reject) { // 返回一個新的Promise實例
var arr = [];
for (let i = 0; i < list.length; i++) {
let p = list[i];// 每一個Promise實例(如果傳入的數據非Promise實例 會通過Promise.resolve轉化)
// 提前規定好每一個實例 成功和失敗時執行的內容 => 請求有結果之后會執行
p.then(res => { //等待異步操作有結果之后 對應下標放到數組中
arr[i] = res;
if (arr.length === list.length) { // 所有的請求全都成功 => (也是一個異步操作)
resolve(arr);
}
}).catch(err => { //只要有一個失敗 就走catch
console.log(err);
reject(err);
})
}
})
}
const p = Promise.myAll([p1, p2, p3]);
通過以上代碼可以發現: (摘自ECMAScript 6 入門)
Promise.all()方法接受一個數組作為參數,p1、p2、p3都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve方法,將參數轉為 Promise 實例,再進一步處理。另外,Promise.all()方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。
p的狀態由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
Promise.race
Promise.race()方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
相較于Promise.all方法, Promise.race方法更側重狀態改變的速度。
模擬封裝(簡易版)
Promise.myRace = function (list) {
return new Promise(function (resolve, reject) { // 返回一個新的Promise實例
for (let i = 0; i < list.length; i++) {
let p = list[i];// 每一個Promise實例 (如果傳入的數據非Promise實例 會通過Promise.resolve轉化)
// 提前規定好每一個實例 成功和失敗時執行的內容 => 請求有結果之后會執行
p.then(res => { //等待異步操作有結果之后 對應下標放到數組中
resolve(res);
}).catch(err => {
reject(err);
})
}
})
}
const p = Promise.myRace([p1, p2, p3]);
通過以上代碼可以發現: (摘自ECMAScript 6 入門)
只要p1、p2、p3之中有一個實例率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。
Promise.allSettled
Promise.allSettled()方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.allSettled([p1, p2, p3]);
相較于Promise.all方法,Promise.allSettled跟注重于不管每一個Promise實例是成功還是失敗,都會將結果依次按請求順序放到數組中。
模擬封裝(簡易版)
Promise.myAllSettled = function (list) {
return new Promise(function (resolve, reject) { // 返回一個新的Promise實例
var arr = [];
for (let i = 0; i < list.length; i++) {
let p = list[i];// 每一個Promise實例 (如果傳入的數據非Promise實例 會通過Promise.resolve轉化)
// 提前規定好每一個實例 成功和失敗時執行的內容 => 請求有結果之后會執行
p.then(res => { //等待異步操作有結果之后 對應下標放到數組中
var obj = { status: "fulfilled", value: txt };
arr[i] = obj;
if (arr.length === list.length) { // 請求全都成功 => arr(異步)
resolve(arr);
}
}).catch(err => {
var obj = { status: "rejected", reason: err };
arr[i] = obj;
if (arr.length === list.length) { // 請求全都成功 => arr(異步)
resolve(arr);
}
})
}
})
}
const p = Promise.myAllSettled([p1, p2, p3])
值得注意的地方時,在將數據放到數組的過程中,傳入的Promise實例的狀態不同,放置的結果稍有不同(數組中的每個成員是一個對象,對象的格式是固定的)。
// 異步操作成功時
{status: 'fulfilled', value: value}
// 異步操作失敗時
{status: 'rejected', reason: reason}
成員對象的status屬性的值只可能是字符串fulfilled或字符串rejected,用來區分異步操作是成功還是失敗。如果是成功(fulfilled),對象會有value屬性,如果是失敗(rejected),會有reason屬性,對應兩種狀態時前面異步操作的返回值。
總結
有了Promise對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加容易。
Promise也有一些缺點。首先,無法取消Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。第三,當處于pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
升級用法 async await
ES2017 標準引入了 async 函數,使得異步操作變得更加方便。
async 函數是什么?一句話,它就是 Generator 函數的語法糖。
基本用法
async function fn() {
var str = "";
var text = await readText("../data/1.txt"); // Promise實例
console.log("line 29:",text);
str += text;
var text = await readText("../data/2.txt"); // Promise實例
console.log("line 33:",text);
str += text;
var text = await readText("../data/3.txt"); // Promise實例
console.log("line 37:",text);
str += text;
return str;
}
fn().then(res=>{
console.log("line 44:",res)
}).catch(err=>{
console.log("line 46:",err)
})
結果:
line 29: 11111
line 33: 22222
line 37: 33333
line 44: 111112222233333
async函數的返回值也是一個Promise實例,在async函數執行的過程中,一旦遇到await就會先返回pending(進行中)狀態的Promise實例,等待異步操作有結果之后,繼續執行await之后的語句,依次類推,語句全部執行完畢且無錯誤的情況下,則返回的Promise實例會變為已成功,否則會變為已失敗。
聲明:本文由網站用戶竹子發表,超夢電商平臺僅提供信息存儲服務,版權歸原作者所有。若發現本站文章存在版權問題,如發現文章、圖片等侵權行為,請聯系我們刪除。