本文我是在進修阮一峰先生的ECMAScript 6 入門的Promise章節時,為了加深影象,通篇都是照抄原文的。
原文地點:阮一峰ECMAScipt入門
1.Promise的寄義
(1)Promise對象代表一個異步操縱,有三種狀況:pending(進行中)、fulfilled(已勝利)和rejected(已失利)。只需異步操縱的效果,能夠決議當前是哪種狀況,任何其他操縱都沒法轉變這個狀況。
(2)一旦狀況轉變,就不會再變,任何時候都能夠取得這個效果。Promise對象的狀況轉變,只需兩種能夠:從pending變成fulfilled和從pending變成rejected。只需這兩種狀況發作,狀況就凝結了,不會再變了,會一向堅持這個效果,這時候就稱為 resolved(已定型)。假如轉變已發作了,你再對Promise對象增加回調函數,也會馬上取得這個效果。這與事宜(Event)完整差別,事宜的特點是,假如你錯過了它,再去監聽,是得不到效果的。
有了Promise對象,就能夠將異步操縱以同步操縱的流程表達出來,避免了層層嵌套的回調函數。別的,Promise對象供應一致的接口,使得掌握異步操縱越發輕易。
Promise也有一些瑕玷。起首,沒法作廢Promise,一旦新建它就會馬上實行,沒法半途作廢。其次,假如不設置回調函數,Promise內部拋出的毛病,不會迴響反映到外部。第三,當處於pending狀況時,沒法得知現在願望到哪個階段(剛剛開始照樣行將完成)。
2、基礎用法
ES6 劃定,Promise對象是一個組織函數,用來天生Promise實例
// 下面代碼製造了一個promise實例
const promise = new Promise(function(resolve, reject) {
// ...some code
if (/*異步操縱勝利*/) {
resolve(value);
} else {
reject(error)
}
});
Promise組織函數接收一個函數作為參數,該函數的兩個參數離別是resolve和reject。它們是兩個函數,由JavaScript 引擎供應,不必本身布置。
resolve函數的作用是,將Promise對象的狀況從“未完成”變成“勝利”(即從pending 變成 resolved),在異步操縱勝利時挪用,並將異步操縱的效果作為參數通報出去;reject函數的作用是,將Promise對象的狀況從“未完成”變成“失利”(即從pending 變成 rejected),在異步操縱失利時挪用,並將異步操縱報出的毛病,作為參數通報出去。
Promise實例天生今後,能夠用then要領離別指定resolved狀況和rejected狀況的回調函數。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then要領能夠接收兩個回調函數作為參數。第一個回調函數是Promise對象的狀況變成resolved時挪用,第二個回調函數是Promise對象的狀況變成rejected時挪用。个中,第二個函數是可選的,不一定要供應。這兩個函數都接收Promise對象傳出的值作為參數。
下面是一個promise對象的簡樸例子:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done')
});
}
timeout().then((value) => {
console.log(value);
})
上面代碼中,timeout要領返回一個Promise實例,示意一段時候今後才會發作的效果。過了指定的時候(ms參數)今後,Promise實例的狀況變成resolved,就會觸發then要領綁定的回調函數。
Promise 新建后就會馬上實行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
上面代碼中,promise 新建后馬上實行,所以起首輸出的是Promise。然後,then要領指定的回調函數,將在當前劇本一切同步任務實行完后才會實行,所以resolved末了輸出。
下面是異步圖片加載的例子:
function loadImageAsync(url) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = function () {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at' + url));
}
image.src= url;
})
}
上面代碼中,運用Promise包裝了一個圖片加載的異步操縱。假如加載勝利,就挪用resolve要領,不然就挪用reject要領。
下面是一個用Promise對象完成ajax操縱的例子:
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
const getJSon = function(url) {
const Promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return Promise;
}
getJSON("/posts.json").then(function(json) {
console.log('Contents:' + json);
}, function(error) {
console.log('出錯了', error);
})
上面代碼中,getJSON是對 XMLHttpRequest 對象的封裝,用於發出一個針對 JSON 數據的 HTTP 要求,而且返回一個Promise對象。須要注重的是,在getJSON內部,resolve函數和reject函數挪用時,都帶有參數。
假如挪用resolve函數和reject函數時帶有參數,那末它們的參數會被通報給回調函數。reject函數的參數通常是Error對象的實例,示意拋出的毛病;resolve函數的參數除了平常的值之外,還多是另一個 Promise 實例,比方像下面如許。
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
上面代碼中,p1和p2都是 Promise 的實例,然則p2的resolve要領將p1作為參數,即一個異步操縱的效果是返回另一個異步操縱。
注重,這時候p1的狀況就會通報給p2,也就是說,p1的狀況決議了p2的狀況。假如p1的狀況是pending,那末p2的回調函數就會守候p1的狀況轉變;假如p1的狀況已是resolved或許rejected,那末p2的回調函數將會馬上實行。
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代碼中,p1是一個 Promise,3 秒以後變成rejected。p2的狀況在 1 秒以後轉變,resolve要領返回的是p1。由於p2返回的是另一個 Promise,致使p2本身的狀況無效了,由p1的狀況決議p2的狀況。所以,背面的then語句都變成針對後者(p1)。又過了 2 秒,p1變成rejected,致使觸發catch要領指定的回調函數。
附上本身的明白:p2的狀況在一秒后變成resolve它的參數是p1,所以p2的狀況由p1決議,p1狀況此時是pending,它在三秒后狀況變成rejected,則p2狀況為rejected,所以觸發了catch要領指定的回調函數而不是then要領的。
注重,挪用resolve或reject並不會閉幕 Promise 的參數函數的實行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
上面代碼中,挪用resolve(1)今後,背面的console.log(2)照樣會實行,而且會起首打印出來。這是由於馬上 resolved 的 Promise 是在本輪事宜輪迴的末端實行,老是晚於本輪輪迴的同步任務。
平常來說,挪用resolve或reject今後,Promise 的任務就完成了,後繼操縱應當放到then要領內里,而不應當直接寫在resolve或reject的背面。所以,最幸虧它們前面加上return語句,如許就不會有不測。
new Promise((resolve, reject) => {
return resolve(1);
// 背面的語句不會實行
console.log(2);
})
3、Promise.prototype.then()
Promise 實例具有then要領,也就是說,then要領是定義在原型對象Promise.prototype上的。它的作用是為 Promise 實例增加狀況轉變時的回調函數。前面說過,then要領的第一個參數是resolved狀況的回調函數,第二個參數(可選)是rejected狀況的回調函數。
then要領返回的是一個新的Promise實例(注重,不是本來誰人Promise實例)。因而能夠採納鏈式寫法,即then要領背面再挪用另一個then要領。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的代碼運用then要領,順次指定了兩個回調函數。第一個回調函數完成今後,會將返回效果作為參數,傳入第二個回調函數。
採納鏈式的then,能夠指定一組根據序次挪用的回調函數。這時候,前一個回調函數,有能夠返回的照樣一個Promise對象(即有異步操縱),這時候后一個回調函數,就會守候該Promise對象的狀況發作變化,才會被挪用。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(functon funcA(comments) {
console.log("resolved:", comments);
}, function funcB(err){
console.log("rejected:", err);
})
上面代碼中,第一個then要領指定的回調函數,返回的是另一個Promise對象。這時候,第二個then要領指定的回調函數,就會守候這個新的Promise對象狀況發作變化。假如變成resolved,就挪用funcA,假如狀況變成rejected,就挪用funcB。
假如採納箭頭函數,上面的代碼能夠寫得更簡約。
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved:", comments),
err => console.log("rejected:", err)
);
4、Promise.prototype.catch()
Promise.prototype.catch要領是.then(null, rejection)的別號,用於指定發作毛病時的回調函數。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處置懲罰 getJSON 和 前一個回調函數運轉時發作的毛病
console.log('發作毛病!', error);
});
上面代碼中,getJSON要領返回一個 Promise 對象,假如該對象狀況變成resolved,則會挪用then要領指定的回調函數;假如異步操縱拋出毛病,狀況就會變成rejected,就會挪用catch要領指定的回調函數,處置懲罰這個毛病。別的,then要領指定的回調函數,假如運轉中拋出毛病,也會被catch要領捕捉。
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同於
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
下面是一個例子
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面代碼中,promise拋出一個毛病,就被catch要領指定的回調函數捕捉。注重,上面的寫法與下面兩種寫法是等價的。
// 寫法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 寫法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
比較上面兩種寫法,能夠發明reject要領的作用,等同於拋出毛病。
假如 Promise 狀況已變成resolved,再拋出毛病是無效的。
const promise = new promise(function(resolve, reject) {
resolve("ok");
});
promise
.then((value) => console.log(value) )
.catch((error) => console.log(error) );
// ok
上面代碼中,Promise 在resolve語句背面,再拋出毛病,不會被捕捉,即是沒有拋出。由於 Promise 的狀況一旦轉變,就永遠堅持該狀況,不會再變了。
Promise 對象的毛病具有“冒泡”性子,會一向向後通報,直到被捕捉為止。也就是說,毛病老是會被下一個catch語句捕捉。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 處置懲罰前面三個Promise發生的毛病
});
上面代碼中,一共有三個 Promise 對象:一個由getJSON發生,兩個由then發生。它們當中任何一個拋出的毛病,都邑被末了一個catch捕捉。
平常來說,不要在then要領內里定義 Reject 狀況的回調函數(即then的第二個參數),老是運用catch要領。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,由於x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
上面代碼中,someAsyncThing函數發生的 Promise 對象,內部有語法毛病。瀏覽器運轉到這一行,會打印出毛病提醒ReferenceError: x is not defined,然則不會退出歷程、停止劇本實行,2 秒以後照樣會輸出123。這就是說,Promise 內部的毛病不會影響到 Promise 外部的代碼,淺顯的說法就是“Promise 會吃掉毛病”。
這個劇本放在服務器實行,退出碼就是0(即示意實行勝利)。不過,Node 有一個unhandledRejection事宜,特地監聽未捕捉的reject毛病,上面的劇本會觸發這個事宜的監聽函數,能夠在監聽函數內里拋出毛病。
process.on('unhandledRejection', function (err, p) {
throw err;
});
上面代碼中,unhandledRejection事宜的監聽函數有兩個參數,第一個是毛病對象,第二個是報錯的 Promise 實例,它能夠用來相識發作毛病的環境信息。
注重,Node 有計劃在將來取銷unhandledRejection事宜。假如 Promise 內部有未捕捉的毛病,會直接停止歷程,而且歷程的退出碼不為 0。
再看下面的例子。
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test
上面代碼中,Promise 指定鄙人一輪“事宜輪迴”再拋出毛病。到了誰人時候,Promise 的運轉已完畢了,所以這個毛病是在 Promise 函數體外拋出的,會冒泡到最外層,成了未捕捉的毛病。
平常老是發起,Promise 對象背面要跟catch要領,如許能夠處置懲罰 Promise 內部發作的毛病。catch要領返回的照樣一個 Promise 對象,因而背面還能夠接着挪用then要領。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,由於x沒有聲明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
上面代碼運轉完catch要領指定的回調函數,會接着運轉背面誰人then要領指定的回調函數。假如沒有報錯,則會跳過catch要領。
Promise.resolve()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// carry on
上面的代碼由於沒有報錯,跳過了catch要領,直接實行背面的then要領。此時,如果then要領內里報錯,就與前面的catch無關了。
catch要領當中,還能再拋出毛病。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,由於x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行會報錯,由於 y 沒有聲明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
上面代碼中,catch要領拋出一個毛病,由於背面沒有別的catch要領了,致使這個毛病不會被捕捉,也不會通報到外層。假如改寫一下,效果就不一樣了。
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行會報錯,由於y沒有聲明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
上面代碼中,第二個catch要領用來捕捉前一個catch要領拋出的毛病。
5、Promise.prototype.finally()
finally要領用於指定不論 Promise 對象末了狀況怎樣,都邑實行的操縱。該要領是 ES2018 引入規範的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中,不論promise末了的狀況,在實行完then或catch指定的回調函數今後,都邑實行finally要領指定的回調函數。
下面是一個例子,服務器運用 Promise 處置懲罰要求,然後運用finally要領關掉服務器。
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
finally要領的回調函數不接收任何參數,這意味着沒有辦法曉得,前面的 Promise 狀況究竟是fulfilled照樣rejected。這表明,finally要領內里的操縱,應當是與狀況無關的,不依賴於 Promise 的實行效果。
finally本質上是then要領的慣例。
promise
.finally(() => {
// 語句
});
// 等同於
promise
.then(
result => {
// 語句
return result;
},
error => {
// 語句
throw error;
}
);
上面代碼中,假如不運用finally要領,一樣的語句須要為勝利和失利兩種狀況各寫一次。有了finally要領,則只須要寫一次。
它的完成也很簡樸。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面代碼中,不論前面的 Promise 是fulfilled照樣rejected,都邑實行回調函數callback。
從上面的完成還能夠看到,finally要領老是會返回本來的值。
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
6、Promise.all()
Promise.all要領用於將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,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對象的數組
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
上面代碼中,promises是包括 6 個 Promise 實例的數組,只需這 6 個實例的狀況都變成fulfilled,或許个中有一個變成rejected,才會挪用Promise.all要領背面的回調函數。
下面是另一個例子。
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 實例,本身定義了catch要領,那末它一旦被rejected,並不會觸發Promise.all()的catch要領。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了
7、Promise.race()
Promise.race要領一樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
上面代碼中,只需p1、p2、p3當中有一個實例領先轉變狀況,p的狀況就隨着轉變。誰人領先轉變的 Promise 實例的返回值,就通報給p的回調函數。
Promise.race要領的參數與Promise.all要領一樣,假如不是 Promise 實例,就會先挪用下面講到的Promise.resolve要領,將參數轉為 Promise 實例,再進一步處置懲罰。
下面是一個例子,假如指定時候內沒有取得效果,就將 Promise 的狀況變成reject,不然變成resolve。
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(console.log)
.catch(console.error);
8、Promise.resolve()
偶然須要將現有對象轉為 Promise 對象,Promise.resolve要領就起到這個作用。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代碼將 jQuery 天生的deferred對象,轉為一個新的 Promise 對象。
Promise.resolve等價於下面的寫法。
Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
Promise.resolve要領的參數分紅四種狀況。
(1)參數是一個 Promise 實例
假如參數是 Promise 實例,那末Promise.resolve將不做任何修正、一成不變地返回這個實例。
(2)參數是一個thenable對象
thenable對象指的是具有then要領的對象,比方下面這個對象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
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要領的對象,或基礎就不是對象
假如參數是一個原始值,或許是一個不具有then要領的對象,則Promise.resolve要領返回一個新的 Promise 對象,狀況為resolved。
const 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要領。
const p = Promise.resolve();
p.then(function () {
// ...
});
上面代碼的變量p就是一個 Promise 對象。
須要注重的是,馬上resolve的 Promise 對象,是在本輪“事宜輪迴”(event loop)的完畢時,而不是鄙人一輪“事宜輪迴”的開始時。
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’)則是馬上實行,因而最早輸出。
9、Promise.reject()
Promise.reject(reason)要領也會返回一個新的 Promise 實例,該實例的狀況為rejected。
const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
上面代碼天生一個 Promise 對象的實例p,狀況為rejected,回調函數會馬上實行。
注重,Promise.reject()要領的參數,會一成不變地作為reject的來由,變成後續要領的參數。這一點與Promise.resolve要領不一致。
const thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代碼中,Promise.reject要領的參數是一個thenable對象,實行今後,背面catch要領的參數不是reject拋出的“出錯了”這個字符串,而是thenable對象。