本文摘自 阿爾卑斯de祕密 的 面向面試題和實際談promise。
Promise是JS異步編程中的重要概念,異步抽象處理對象,是目前比較流行Javascript異步編程解決方案之一。這句話說的很明白了,Promise是一種用於解決異步問題的思路、方案或者對象方式。在js中,常用異步的地方是Ajax交互。好比在es5時代,jQuery的ajax的使用success來完成異步的:html
$.ajax({
url:'/xxx',
success:()=>{},
error: ()=>{}
})複製代碼
這種方法能夠清楚的讓讀代碼的人明白那一部分是Ajax請求成功的回調函數和失敗的回調函數。可是問題來了,當一次請求須要連續請求多個接口時,這段代碼彷彿進入了一團亂麻中:es6
// 第一次
$.ajax({
url:'/xxx',
success:()=>{
// 第二次
$.ajax({
url:'/xxx',
success:()=>{
// 第三次
$.ajax({
url:'/xxx',
success:()=>{
// 可能還會有
},
error: ()=>{}
})
},
error: ()=>{}
})
},
error: ()=>{}
}) 複製代碼
也許由於success和error這兩個函數的存在,理解這段代碼會很簡單,可是當咱們更改需求的時候,這將成爲一個棘手的問題。這就是回調地獄。面試
固然,這是es5時代。當js這門語言發展到es6時代時,Promise的出現給異步帶來了變革。Promise提供一個then,來爲異步提供回調函數:ajax
而其先進之處則是,能夠在then方法中繼續寫Promise對象並返回,而後繼續調用then來進行回調操做。編程
說完了Promise是什麼,下面讓咱們研究一下Promise怎麼使用。首先,Promise是一個對象,所以,咱們使用new的方式新建一個。而後給它傳一個函數做爲參數,這個函數呢也有兩個參數,一個叫resolve(決定),一個叫reject(拒絕),這兩個參數也是函數。緊接着,咱們使用then來調用這個Promise:數組
const fn = new Promise(function (resolve, reject) {
setTimeout(()=>{
let num = Math.ceil(Math.random() * 10) // 假設num爲7
if (num > 5) {
resolve(num) //返回7
} else {
reject(num)
}
},2000)
})
fn.then((res)=>{
console.log(res) // 7
},(err)=>{
console.log(err)
})複製代碼
這就是最簡單的Promise的使用。假設2秒鐘以後生成隨機數爲7,所以resolve回調函數運行,then走第一個函數,console.log(7)。假設2秒鐘以後生成隨機數爲3,所以reject回調函數運行,then走第二個函數,console.log(3)。promise
那你可能說了,Promise要是就這點能耐也沒什麼大不了的啊?咱們上面說了Promise的先進之處在於能夠在then方法中繼續寫Promise對象並返回,而後繼續調用then來進行回調操做:bash
fn = new Promise(function (resolve, reject) {
let num = Math.ceil(Math.random() * 10)
if (num > 5) {
resolve(num)
} else {
reject(num)
}
})
// 第一次回調
fn.then((res)=>{
console.log(`res==>${res}`)
return new Promise((resolve,reject)=>{
if(2*res>15){
resolve(2*res)
}else{
reject(2*res)
}
})
},(err)=>{
console.log(`err==>${err}`)
}).then((res)=>{ // 第二次回調
console.log(res)
},(err)=>{
console.log(`err==>${err}`)
})複製代碼
這就能夠代替了上面相似es5時代的jQurey的success的嵌套式的回調地獄的產生,讓代碼清爽了許多。這裏的resolve就至關於之前的success。app
在Promise的內部,有一個狀態管理器的存在,有三種狀態:pending、fulfilled、rejected。dom
(1) promise 對象初始化狀態爲 pending。
(2) 當調用resolve(成功),會由pending => fulfilled。
(3) 當調用reject(失敗),會由pending => rejected。
所以,看上面的的代碼中的resolve(num)實際上是將promise的狀態由pending改成fulfilled,而後向then的成功回掉函數傳值,reject反之。可是須要記住的是注意promsie狀態 只能由 pending => fulfilled/rejected, 一旦修改就不能再變(記住,必定要記住,下面會考到)。
當狀態爲fulfilled(rejected反之)時,then的成功回調函數會被調用,並接受上面傳來的num,進而進行操做。promise.then方法每次調用,都返回一個新的promise對象 因此能夠鏈式寫法(不管resolve仍是reject都是這樣)。
then方法用於註冊當狀態變爲fulfilled或者reject時的回調函數:
// onFulfilled 是用來接收promise成功的值
// onRejected 是用來接收promise失敗的緣由
promise.then(onFulfilled, onRejected);複製代碼
須要注意的地方是then方法是異步執行的。
// resolve(成功) onFulfilled會被調用
const promise = new Promise((resolve, reject) => {
resolve('fulfilled'); // 狀態由 pending => fulfilled
});
promise.then(result => { // onFulfilled
console.log(result); // 'fulfilled'
}, reason => { // onRejected 不會被調用
})
// reject(失敗) onRejected會被調用
const promise = new Promise((resolve, reject) => {
reject('rejected'); // 狀態由 pending => rejected
});
promise.then(result => { // onFulfilled 不會被調用
}, reason => { // onRejected
console.log(rejected); // 'rejected'
})複製代碼
catch在鏈式寫法中能夠捕獲前面then中發送的異常。
fn = new Promise(function (resolve, reject) {
let num = Math.ceil(Math.random() * 10)
if (num > 5) {
resolve(num)
} else {
reject(num)
}
})
fn..then((res)=>{
console.log(res)
}).catch((err)=>{
console.log(`err==>${err}`)
})複製代碼
其實,catch至關於then(null,onRejected),前者只是後者的語法糖而已。
Promise.resolve 返回一個fulfilled狀態的promise對象,Promise.reject 返回一個rejected狀態的promise對象。
Promise.resolve('hello').then(function(value){
console.log(value);
});
Promise.resolve('hello');
// 至關於
const promise = new Promise(resolve => {
resolve('hello');
});
// reject反之複製代碼
但從字面意思上理解,可能爲一個狀態所有怎麼樣的意思,讓我看一下其用法,就能夠看明白這個靜態方法:
var p1 = Promise.resolve(1),
p2 = Promise.reject(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((res)=>{
//then方法不會被執行
console.log(results);
}).catch((err)=>{
//catch方法將會被執行,輸出結果爲:2
console.log(err);
});複製代碼
大概就是做爲參數的幾個promise對象一旦有一個的狀態爲rejected,則all的返回值就是rejected。
當這幾個做爲參數的函數的返回狀態爲fulfilled時,至於輸出的時間就要看誰跑的慢了:
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('1s') //1s後輸出
resolve(1)
},1000)
})
let p10 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('10s') //10s後輸出
resolve(10)
},10000)
})
let p5 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('5s') //5s後輸出
resolve(5)
},5000)
})
Promise.all([p1, p10, p5]).then((res)=>{
console.log(res); // 最後輸出
})複製代碼
這段代碼運行時,根據看誰跑的慢的原則,則會在10s以後輸出[1,10,5]。over,all收工。
promise.race()方法也能夠處理一個promise實例數組但它和promise.all()不一樣,從字面意思上理解就是競速,那麼理解起來上就簡單多了,也就是說在數組中的元素實例那個率先改變狀態,就向下傳遞誰的狀態和異步結果。可是,其他的仍是會繼續進行的。
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('1s') //1s後輸出
resolve(1)
},1000)
})
let p10 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('10s') //10s後輸出
resolve(10) //不傳遞
},10000)
})
let p5 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('5s') //5s後輸出
resolve(5) //不傳遞
},5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
console.log(res); // 最後輸出
})複製代碼
所以,在這段代碼的結尾咱們的結果爲
1s
1
5s
10s複製代碼
咱們能夠根據race這個屬性作超時的操做:
//請求某個圖片資源
let requestImg = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
});
//延時函數,用於給請求計時
let timeOut = new Promise(function(resolve, reject){
setTimeout(function(){
reject('圖片請求超時');
}, 5000);
});
Promise.race([requestImg, timeout]).then((res)=>{
console.log(res);
}).catch((err)=>{
console.log(err);
});複製代碼
1.
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);複製代碼
輸出結果爲:1,2,4,3。
解題思路:then方法是異步執行的。
2.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
reject('error')
}, 1000)
})
promise.then((res)=>{
console.log(res)
},(err)=>{
console.log(err)
})複製代碼
輸出結果:success
解題思路:Promise狀態一旦改變,沒法在發生變動。
3.
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)複製代碼
輸出結果:1
解題思路:Promise的then方法的參數指望是函數,傳入非函數則會發生值穿透。
4.
setTimeout(()=>{
console.log('setTimeout')
})
let p1 = new Promise((resolve)=>{
console.log('Promise1')
resolve('Promise2')
})
p1.then((res)=>{
console.log(res)
})
console.log(1)複製代碼
輸出結果:
Promise1
1
Promise2
setTimeout
解題思路:這個牽扯到js的執行隊列問題,整個script代碼,放在了macrotask queue中,執行到setTimeout時會新建一個macrotask queue。可是,promise.then放到了另外一個任務隊列microtask queue中。script的執行引擎會取1個macrotask queue中的task,執行之。而後把全部microtask queue順序執行完,再取setTimeout所在的macrotask queue按順序開始執行。(具體參考www.zhihu.com/question/36…)
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});複製代碼
輸出結果:1 2
解題思路:Promise首先resolve(1),接着就會執行then函數,所以會輸出1,而後在函數中返回2。由於是resolve函數,所以後面的catch函數不會執行,而是直接執行第二個then函數,所以會輸出2。
6.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('開始');
resolve('success');
}, 5000);
});
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});複製代碼
輸出結果:
開始
success 5002
success 5002
解題思路:promise 的.then
或者.catch
能夠被調用屢次,但這裏 Promise 構造函數只執行一次。或者說 promise 內部狀態一經改變,而且有了一個值,那麼後續每次調用.then
或者.catch
都會直接拿到該值。
7.
let p1 = new Promise((resolve,reject)=>{
let num = 6
if(num<5){
console.log('resolve1')
resolve(num)
}else{
console.log('reject1')
reject(num)
}
})
p1.then((res)=>{
console.log('resolve2')
console.log(res)
},(rej)=>{
console.log('reject2')
let p2 = new Promise((resolve,reject)=>{
if(rej*2>10){
console.log('resolve3')
resolve(rej*2)
}else{
console.log('reject3')
reject(rej*2)
}
})
return p2
}).then((res)=>{
console.log('resolve4')
console.log(res)
},(rej)=>{
console.log('reject4')
console.log(rej)
})複製代碼
輸出結果:
reject1
reject2
resolve3
resolve4
12
解題思路:咱們上面說了Promise的先進之處在於能夠在then方法中繼續寫Promise對象並返回。
function Promise(fn){
var status = 'pending'
function successNotify(){
status = 'fulfilled'//狀態變爲fulfilled
toDoThen.apply(undefined, arguments)//執行回調
}
function failNotify(){
status = 'rejected'//狀態變爲rejected
toDoThen.apply(undefined, arguments)//執行回調
}
function toDoThen(){
setTimeout(()=>{ // 保證回調是異步執行的
if(status === 'fulfilled'){
for(let i =0; i< successArray.length;i ++) {
successArray[i].apply(undefined, arguments)//執行then裏面的回掉函數
}
}else if(status === 'rejected'){
for(let i =0; i< failArray.length;i ++) {
failArray[i].apply(undefined, arguments)//執行then裏面的回掉函數
}
}
})
}
var successArray = []
var failArray = []
fn.call(undefined, successNotify, failNotify)
return {
then: function(successFn, failFn){
successArray.push(successFn)
failArray.push(failFn)
return undefined // 此處應該返回一個Promise
}
}
}複製代碼
解題思路:Promise中的resolve和reject用於改變Promise的狀態和傳參,then中的參數必須是做爲回調執行的函數。所以,當Promise改變狀態以後會調用回調函數,根據狀態的不一樣選擇須要執行的回調函數。
首先,Promise是一個對象,如同其字面意思同樣,表明了將來某時間纔會知道結果的時間,不受外界因素的印象。Promise一旦觸發,其狀態只能變爲fulfilled或者rejected,而且已經改變不可逆轉。Promise的構造函數接受一個函數做爲參數,該參數函數的兩個參數分別爲resolve和reject,其做用分別是將Promise的狀態由pending轉化爲fulfilled或者rejected,而且將成功或者失敗的返回值傳遞出去。then有兩個函數做爲Promise狀態改變時的回調函數,當Promise狀態改變時接受傳遞來的參數並調用相應的函數。then中的回調的過程爲異步操做。catch方法是對.then(null,rejectFn)的封裝(語法糖),用於指定發生錯誤時的回掉函數。通常來講,建議不要再then中定義rejected狀態的回調函數,應該使用catch方法代替。all和race都是競速函數,all結束的時間取決於最慢的那個,其做爲參數的Promise函數一旦有一個狀態爲rejected,則總的Promise的狀態就爲rejected;而race結束的時間取決於最快的那個,一旦最快的那個Promise狀態發生改變,那個其總的Promise的狀態就變成相應的狀態,其他的參數Promise仍是會繼續進行的。