透過面試題來說說Promise

程式碼寫著寫著就懂了發表於2018-12-25

前言

我們先看看這幾個來自大廠的面試題

面試題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

Promise簡介

Promises物件是CommonJS工作組提出的一種規範,目的是為非同步操作提供統一介面

那麼,什麼是Promises?首先,它是一個物件,也就是說與其他JavaScript物件的用法,沒有什麼兩樣;其次,它起到代理作用(proxy),使得非同步操作具備同步操作(synchronous code)的介面,即充當非同步操作與回撥函式之間的中介,使得程式具備正常的同步執行的流程,回撥函式不必再一層層包裹起來。

簡單說,它的思想是,每一個非同步任務立刻返回一個Promise物件,由於是立刻返回,所以可以採用同步操作的流程。

Promise 表示一個非同步操作的最終結果,與之進行互動的方式主要是 then 方法,該方法註冊了兩個回撥函式,用於接收 promise 的終值或本 promise 不能執行的原因。

下圖是一張Promise的API結構圖。(引自https://github.com/leer0911/myPromise)

`Promise`的API結構圖

面試題解析

面試題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編輯器我們可以看到一些內部的引數和返回值。

透過面試題來說說Promise

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 最直接的好處就是鏈式呼叫

引自MND

面試題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 狀態,可以通過函式 resolvereject ,將狀態轉變為 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 的回撥函式都會被排在 successCallbackfailureCallback 返回的 Promise 後面。

特別重申一下,then 函式一定返回一個新的 Promise,跟原來的不同,下面的是錯誤的應用:

透過面試題來說說Promise

Promise物件的執行結果,最終只有兩種。

  • 得到一個值,狀態變為fulfilled
  • 丟擲一個錯誤,狀態變為rejected
promise.then(onFulfilled, onRejected);
複製程式碼

promise物件的then方法用來新增回撥函式。它可以接受兩個回撥函式,第一個是操作成功(fulfilled)時的回撥函式,第二個是操作失敗(rejected)時的回撥函式(可以不提供)。一旦狀態改變,就呼叫相應的回撥函式。

onFulfilledonRejected 都是可選引數。

  • 如果 onFulfilled 是函式,當 promise 執行結束後其必須被呼叫,其第一個引數為 promise 的終值,在 promise 執行結束前其不可被呼叫,其呼叫次數不可超過一次

  • 如果 onRejected 是函式,當 promise 被拒絕執行後其必須被呼叫,其第一個引數為 promise 的據因,在 promise 被拒絕執行前其不可被呼叫,其呼叫次數不可超過一次

  • onFulfilledonRejected 只有在執行環境堆疊僅包含平臺程式碼 ( 指的是引擎、環境以及 promise 的實施程式碼 )時才可被呼叫

  • 實踐中要確保 onFulfilledonRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。

  • onFulfilledonRejected 必須被作為函式呼叫即沒有 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的返回結果,很顯然應該是5setTimeout由於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處理返回給了下一個thenpromise的成功態,這時候最後一個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的執行順序,以及引數傳遞,這樣就能繞過更多的坑。

以上內容如果錯誤,歡迎指正,共同進步~

相關文章