Promise簡介
Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。(摘自<ECMAScript 6 入門>),但是對于Promise的創建和執行流程,其實有很多人初學時,容易走入誤區。
今天就以一首歌來帶大家學習Promise。
Promise創建
Promise翻譯過來的意思就是"承諾",其實Promise的創建本質就是一個許下承諾的過程,想到這我的腦海中不由得浮現了一首經典老歌,海鳴威 《你的承諾》。
歌中相知相戀的兩個人,奈何情深緣淺,終于還是分開,許下了“不為彼此難過, 過各自的生活"這樣的承諾。但是随後歌曲又以"Oh baby 你答應我的我都記得 ,但是你卻忘了你的承諾 ,不是說好彼此都不再聯絡, 誰都别再犯錯",暗示承諾的狀态發生改變。
由此不難想象以下兩種情況:
同樣,JS中的Promise的創建也是許下代碼承諾的一種方式,它默認也包含三種狀态:
那麼如何許下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是單線程,會優先在主線程執行同步代碼,異步代碼會放到任務隊列中,所以
還有一個有趣的地方: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 awaitES2017 标準引入了 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實例會變為已成功,否則會變為已失敗。
你學會了麼?文章底部留言告訴我喲。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!