前言
我們先看看這幾個來自大廠的面試題
面試題1:
const promise = new Promise(function(resolve,reject){
console.log(1)
resolve()
console.log(2)
})
console.log(3)
複製程式碼
面試題2:
setTimeout(function () {
console.log(1);
}, 0)
new Promise(function (resolve) {
console.log(2);
for (var i = 0; i < 100; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
})
console.log(5);
複製程式碼
面試題3:
Promise.resolve(1)
.then((res)=>{
console.log(res)
return 2
})
.catch( (err) => 3)
.then(res=>console.log(res))。
複製程式碼
面試題4:
Promise.resolve(1)
.then( (x) => x + 1 )
.then( (x) => {throw new Error('My Error')})
.catch( () => 1)
.then( (x) => x + 1)
.then((x) => console.log(x))
.catch( (x) => console.log(error))
複製程式碼
如果你看完這些題一臉懵逼,恭喜你,你可以繼續往下看了,else
,出門左拐大佬。
首先簡單介紹一些Promise
。
Promise簡介
Promises
物件是CommonJS
工作組提出的一種規範,目的是為非同步操作提供統一介面。
那麼,什麼是Promises
?首先,它是一個物件,也就是說與其他JavaScript
物件的用法,沒有什麼兩樣;其次,它起到代理作用(proxy)
,使得非同步操作具備同步操作(synchronous code)
的介面,即充當非同步操作與回撥函式之間的中介,使得程式具備正常的同步執行的流程,回撥函式不必再一層層包裹起來。
簡單說,它的思想是,每一個非同步任務立刻返回一個Promise
物件,由於是立刻返回,所以可以採用同步操作的流程。
Promise
表示一個非同步操作的最終結果,與之進行互動的方式主要是then
方法,該方法註冊了兩個回撥函式,用於接收promise
的終值或本promise
不能執行的原因。
下圖是一張Promise
的API結構圖。(引自https://github.com/leer0911/myPromise)
面試題解析
面試題1:
const promise = new Promise(function(resolve,reject){
console.log(1)
resolve()
console.log(2)
})
console.log(3)
// 1
// 2
// 3
複製程式碼
解析:
Promise
類似於XMLHttpRequest
,從建構函式Promise
來建立一個新Promise
物件作為介面。
要想建立一個Promise
物件嗎可以使用new
來呼叫Promise
的構造器來進行例項化。
var promise = new Promise(function(resolve, reject) {
// 非同步處理
// 處理結束後、呼叫resolve 或 reject
});
複製程式碼
new Promise
的時候, 需要傳遞一個executor
執行器 ,執行器函式會預設被內部所執行。借用VSCode
編輯器我們可以看到一些內部的引數和返回值。
new Promise
內部的執行器會立即執行它裡面的程式碼,這裡的resolve()
並不會阻塞下面的程式碼執行,我們可以理解new Promise(function(){...})
這個就是一段同步程式碼而已。所以第一道題的答案顯而易見。
鞏固一下,下面的程式碼你一定可以準確的得出結果了。
setTimeout(function () {
console.log('setTimeout')
}, 0);
let p = new Promise(function (resolve,reject) {
resolve();
console.log('a')
});
複製程式碼
結果是:
// a
// setTimeout
複製程式碼
這裡會有EventLoop
的一些知識,補充知識連結:說一說javascript的非同步程式設計,到目前來說可以得到兩點:
1. 我們完全可以把`new Promise(function(){...})`看成是同步程式碼。
2. `Promise`會優先於`setTimeout`執行。
複製程式碼
約定
不同於老式的傳入回撥,在應用 Promise
時,我們將會有以下約定:
- 在 JavaScript 事件佇列的當前執行完成之前,回撥函式永遠不會被呼叫。
- 通過 .then 形式新增的回撥函式,甚至都在非同步操作完成之後才被新增的函式,都會被呼叫。
- 通過多次呼叫 .then,可以新增多個回撥函式,它們會按照插入順序並且獨立執行。
因此,
Promise
最直接的好處就是鏈式呼叫
面試題2:
setTimeout(function () {
console.log(1);
}, 0)
new Promise(function (resolve) {
console.log(2);
for (var i = 0; i < 100; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
})
console.log(5);
// 2
// 3
// 5
// 4
// 1
複製程式碼
解析:
Then 方法
可以把 Promise
看成一個狀態機。初始是 pending
狀態,可以通過函式 resolve
和 reject
,將狀態轉變為 resolved
或者 rejected
狀態,狀態一旦改變就不能再次變化。
then
函式會返回一個 Promise
例項,並且該返回值是一個新的例項而不是之前的例項。因為 Promise 規範規定除了 pending
狀態,其他狀態是不可以改變的,如果返回的是一個相同例項的話,多個 then
呼叫就失去意義了。
MDN
中對於promise
的介紹一個小案例非常的簡單易懂,如下:
一個常見的需求就是連續執行兩個或者多個非同步操作,這種情況下,每一個後來的操作都在前面的操作執行成功之後,帶著上一步操作所返回的結果開始執行。我們可以通過創造一個 Promise chain
來完成這種需求。
then
函式會返回一個新的 Promise
,跟原來的不同:
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
複製程式碼
或者
const promise2 = doSomething().then(successCallback, failureCallback);
複製程式碼
第二個物件(promise2)
不僅代表doSomething()
函式的完成,也代表了你傳入的 successCallback
或者failureCallback
的完成,這也可能是其他非同步函式返回的 Promise
。這樣的話,任何被新增給 promise2
的回撥函式都會被排在 successCallback
或 failureCallback
返回的 Promise
後面。
特別重申一下,then
函式一定返回一個新的 Promise
,跟原來的不同,下面的是錯誤的應用:
Promise
物件的執行結果,最終只有兩種。
- 得到一個值,狀態變為
fulfilled
- 丟擲一個錯誤,狀態變為
rejected
promise.then(onFulfilled, onRejected);
複製程式碼
promise
物件的then
方法用來新增回撥函式。它可以接受兩個回撥函式,第一個是操作成功(fulfilled
)時的回撥函式,第二個是操作失敗(rejected
)時的回撥函式(可以不提供)。一旦狀態改變,就呼叫相應的回撥函式。
onFulfilled
和 onRejected
都是可選引數。
-
如果
onFulfilled
是函式,當promise
執行結束後其必須被呼叫,其第一個引數為promise
的終值,在promise
執行結束前其不可被呼叫,其呼叫次數不可超過一次 -
如果
onRejected
是函式,當promise
被拒絕執行後其必須被呼叫,其第一個引數為promise
的據因,在promise
被拒絕執行前其不可被呼叫,其呼叫次數不可超過一次 -
onFulfilled
和onRejected
只有在執行環境堆疊僅包含平臺程式碼 ( 指的是引擎、環境以及promise
的實施程式碼 )時才可被呼叫 -
實踐中要確保
onFulfilled
和onRejected
方法非同步執行,且應該在then
方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。 -
onFulfilled
和onRejected
必須被作為函式呼叫即沒有this
值 ( 也就是說在 嚴格模式(strict
) 中,函式this
的值為undefined
;在非嚴格模式中其為全域性物件。) -
then
方法可以被同一個promise
呼叫多次 -
then
方法必須返回一個promise
物件
為了避免意外,即使是一個已經變成 resolve
狀態的 Promise
,傳遞給 then
的函式也總是會被非同步呼叫:
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
複製程式碼
至此,我們再回過頭來看面試題:
setTimeout(function () {
console.log(1);
}, 0)
new Promise(function (resolve) {
console.log(2);
for (var i = 0; i < 100; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
})
console.log(5);
// 2
// 3
// 5
// 4
// 1
複製程式碼
new Promise
構造器之後,會返回一個promise
物件,對於這個promise
物件,我們呼叫他的then
方法來設定resolve
後的回撥函式。
該promise
物件會在for
迴圈滿足i==99
時被resolve()
,這時then
的回撥函式會被呼叫。
基於第一道題的基礎,我們知道最後被列印的一定是1
,然後是2
,接著是3
,由於.then
是一個非同步函式,用來接受promise
的返回結果,很顯然應該是5
,setTimeout
由於EventLoop
的原因是最後執行,所以後面是4
,最後是1
.
面試題3:
Promise.resolve(1)
.then((res)=>{
console.log(res)
return 2
})
.catch( (err) => 3)
.then(res=>console.log(res))
// 1
// 2
複製程式碼
解析:
New Promise的快捷方式
靜態方法Promise.resolve(value)
可以認為是new Promise()
方法的快捷方式。
比如Promise.resolve(1);
,可以認為是一下程式碼的語法糖:
new Promise(function(resolve){
resolve(1);
});
複製程式碼
在這段程式碼中的 resolve(1);
會讓這個 promise
物件立即進入確定(即resolved
)狀態,
並將 1
傳遞給後面then
裡所指定的 onFulfilled
函式。
方法Promise.resolve(value)
的返回值也是一個promise
物件,所以我們可以像下面那樣
接著對其返回值進行.then
呼叫。
Promise.resolve(42).then(function(value){
console.log(value)
})
複製程式碼
簡單總結一下 Promise.resolve
方法的話,可以認為它的作用就是將傳遞給它的引數填 充(Fulfilled
)到promise
物件後並返回這個promise
物件。
Promise.resolve(value)
方法返回一個以給定值解析後的Promise
物件。但如果這個值是個thenable
(即帶有then
方法),返回的promise
會“跟隨”這個thenable
的物件,採用它的最終狀態(指resolved/rejected/pending/settled
);如果傳入的value
本身就是promise
物件,則該物件作為Promise.resolve
方法的返回值返回;否則以該值為成功狀態返回promise
物件。
語法為:
Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable);
複製程式碼
我們知道.then()
同樣是返回一個promise
物件才能實現鏈式呼叫,所以連續的.then()
是同樣的道理,多個 then
方法呼叫串連在了一起,各函式也會嚴 格按照 resolve → then → then → then
的順序執行,並且傳給每個 then
方法的 value
的值都是前一個promise
物件通過 return
返回的值。並且上面已經提到then
方法每次都會建立並返回一個新的promise
物件。
Promise 的鏈式呼叫
then
方法執行完會判斷返回的結果,如果是promise
會把這個promise
執行,會取到它的結果。成功態(onFulfilled
)和失敗態(onRejected
)。如果成功了就會把成功的結果傳遞給後面then
裡所指定的 onFulfilled
函式,失敗了傳遞給onRejected
函式。
Promise.resolve(1)
.then((value)=>{
console.log('value',value)
},
(reason)=>{
console.log('reason',reason)
})
// value 1
複製程式碼
Promise.reject(2)
.then((value)=>{
console.log('value',value)
},
(reason)=>{
console.log('reason',reason)
})
// reason 2
複製程式碼
上面的兩個案例可以很清楚的看到,promise
分別在成功態和失敗態的時候傳遞給後面的then
函式的呼叫輸出結果。下一層的then
是調成功還是失敗是根據上面的promise
返回的是成功還是失敗決定的。
說到這裡第三題的答案已經不用說了。
面試題4:
Promise.resolve(1)
.then( (x) => x + 1 )
.then( (x) => {throw new Error('My Error')})
.catch( () => 1)
.then( (x) => x + 1)
.then((x) => console.log(x))
.catch( (x) => console.log(error))
// 2
複製程式碼
Catch 的後續鏈式操作
在一個失敗操作(即一個 catch
)之後可以繼續使用鏈式操作,即使鏈式中的一個動作失敗之後還能有助於新的動作繼續完成。請閱讀下面的例子:
new Promise((resolve, reject) => {
console.log('Initial');
resolve();
})
.then(() => {
throw new Error('Something failed');
console.log('Do this');
})
.catch(() => {
console.log('Do that');
})
.then(() => {
console.log('Do this whatever happened before');
});
複製程式碼
輸出結果如下:
Initial
Do that
Do this whatever happened before
複製程式碼
注意,由於“Something failed”
錯誤導致了拒絕操作,所以“Do this”
文字沒有被輸出。
解析:
這道題唯一的疑惑可能是在第二個then
這裡,這個錯誤狀態為什麼沒有被列印出來,我們知道catch
是用來捕獲錯誤的,但是這裡catch
是可以捕獲到錯誤的,但是這段程式碼沒有對捕獲的錯誤進行處理而是繼續返回了1
作為下一個Promise
的引數,所以在第三個then
中我們獲取到了一個1
作為成功態,然後又對其進行+1
處理返回給了下一個then
的promise
的成功態,這時候最後一個then
的第一個函式onFulfilled
就能獲取到一個value
列印出來就是2
,沒有錯誤資訊返回所以最後的catch
沒有輸出。
我們也可以對上面的題改一種寫法,就是另一種答案了:
Promise.resolve(1)
.then( (x) => x + 1 )
.then( (x) => {throw new Error('My Error')})
.catch( (err) => console.log(err))
.then( (x) => {
console.log(x)
return x + 1
})
.then((x) => console.log(x))
.catch( (x) => console.log(error))
// Error: My Error
at Promise.resolve.then.then
// undefined
// NaN
複製程式碼
我們在第一個catch
中對錯誤資訊進行了處理,但是我們沒有給下面的then
返回一個成功態的結果,所以預設是undefined
,這樣就會導致後面的結果完全不一樣。
大家可以把這些面試題進行多次變形,改寫來去理解promise
的執行順序,以及引數傳遞,這樣就能繞過更多的坑。
以上內容如果錯誤,歡迎指正,共同進步~