透過面試題來講說Promise

前言

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

面試題1:java

const promise = new Promise(function(resolve,reject){
  console.log(1)
  resolve()
  console.log(2)
})
console.log(3)
複製代碼

面試題2:git

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:github

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,出門左拐大佬。編程

首先簡單介紹一些Promisepromise

Promise簡介

Promises對象是CommonJS工做組提出的一種規範,目的是爲異步操做提供統一接口bash

那麼,什麼是Promises?首先,它是一個對象,也就是說與其餘JavaScript對象的用法,沒有什麼兩樣;其次,它起到代理做用(proxy),使得異步操做具有同步操做(synchronous code)的接口,即充當異步操做與回調函數之間的中介,使得程序具有正常的同步運行的流程,回調函數沒必要再一層層包裹起來。app

簡單說,它的思想是,每個異步任務馬上返回一個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編輯器咱們能夠看到一些內部的參數和返回值。

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對象的運行結果,最終只有兩種。

  • 獲得一個值,狀態變爲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的執行順序,以及參數傳遞,這樣就能繞過更多的坑。

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

相關文章
相關標籤/搜索