面試問到 Promise,這樣回答最完美了

原文鏈接:https://mp.weixin.qq.com/s?__biz=MzU3NjczNDk2MA==&mid=2247484501&idx=1&sn=022dc4845cfce89fe4e0f3845b4b4a0d&chksm=fd0e17caca799edc6519df774c3dbe9066dd0d8a2f195cff7d34e13f1c611497ada5bd4063eb&mpshare=1&scene=24&srcid=&sharer_sharetime=1566952126636&sharer_s

promise是什麼?

 

Promise是異步編程的一種解決方案,比傳統的回調函數和事件更合理和強大。

所謂Promise,簡單來說就是一個容器,裏面保存着某個未來纔會結束的事情(通常是一個異步操作)。從語法上說,Promise是一個對象,從他可以獲取異步操作的消息。

特點:

  • 對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(以失敗)。只有異步操作的結果可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來。

  • 一旦狀態改變,就不會再變,任何時候都是可以得到這個結果的。Promise對象的狀態改變只有兩種可能:*從pending變爲fulfilled和從pending變爲rejected。只要這兩種情況發生,狀態就會凝固,不會再變了。再對Promise對象添加回調函數也會立即得到這個結果。

有了Promise對象,就可以將異步操作以同步操作的流程表達出來。

缺點:

首先無法取消Promise,一旦新建他就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise內部跑出的錯誤無法反應到外部。當pending的時候,無法知道進展到了哪一步。

基本用法

ES6規定,Promise對象是一個構造函數,用來生成Promise實例。

下面代碼創造了一個Promise實例。

const promise = new Promise(function(resolve, reject) {    if(success) {        resolve(value)    } else {        reject(error)    }})

 

Promise構造函數接受一個函數作爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由JavaScript引擎提供,不用自己部署。

resolve函數的作用是,將Promise對象的狀態從"未完成"變成"成功"。(即從pending變爲resolved)。在異步操作成功的時候調用,並將異步操作結果作爲參數傳遞出去;

reject函數的作用是,將promise對象的狀態從"未完成"變成"失敗"(即從pending變爲rejected)。在異步操作失敗時調用,並將異步操作報出的錯誤,作爲參數傳遞出去。

Promise實例生成後,可以用then方法分別指定resolve狀態和rejected狀態的回調函數。

promise.then(function(value) {      }, function(error) {})

 

then方法可以接受兩個回調函數作爲參數,

第一個回調函數是promise對象的狀態變爲resolved的時候調用,

第二個回調函數是promise對象的狀態變爲rejected時調用。

其中第二個函數是可選的,不一定需要提供。

這兩個函數都接受Promise對象傳出的值作爲參數。

function timeout(ms) {    return new Promise((resolve, reject) => {        setTimeout(resolve, ms, 'done')//setTimeout 傳參    })}timeout(100).then((value) => {    console.log(value)//done})

 

上面代碼中,timeout方法返回一個Promise實例,表示一段時間後纔會發生的結果。

過了指定的時間以後,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//Hi//resolved

上面代碼中,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('count not load...'))        }        image.src = url    })}

上面代碼中,使用Promise包裝一個圖片加載的異步操作,如果加載成功就調用resolve方法, 否則就調用rejected方法。

1. Promise.prototype.then()

Promise實例具有then方法,也就是說then方法時定義在原型對象上的。

它的作用是爲Promise實例添加狀態改變時的回調函數。

前面說過,then方法的第一個參數是resolved狀態的回調函數,第二個參數是rejected狀態的回調函數(可選)。

then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)因此可以採用鏈式寫法,即then方法後面再調用另一個then方法。

採用鏈式的then可以指定一組按照次序調用的回調函數。這時,前一個回調函數可能返回一個還是Promise對象(即有異步操作),這時候一個回調函數就會等該Promise對象的狀態發生變化,纔會被調用

getJSON('/post/1.json').then(function(post) {    return getJSON(post.commentURL)}).then(function funcA() {    console.log("resolved:", comments)}, function funcB(err) {    console.log("rejected:", err)})

上面代碼中,第一個then方法指定的回調函數,返回的是一個Promise對象。這時,第二個方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。如果變爲resolved,就調用funcA, 如果狀態變爲rejected,就調用funcB.

2. Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數

getJSON('/post/1.json').then(function(posts) {    //...}).catch(function() {    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)})

如果Promise狀態以及變成resolved,再拋出錯誤是無效的。因爲Promise的狀態一旦改變,就永久保持該狀態,不會再變了。

Promise對象的錯誤具有冒泡性質,會一直向後傳遞,直到被捕獲爲止,也就是說錯誤總會被下一個catch語句捕獲。

3. Promise.prototype.finally()

finally方法用於指定不管Promise對象最後狀態如何,都會執行的操作。

4.Pomise.all的使用

Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數內所有的 promise 都“完成(resolved)”或參數中不包含 promise 時回調完成(resolve);如果參數中  promise 有一個失敗(rejected),此實例回調失敗(reject),失敗原因的是第一個失敗 promise 的結果。

具體代碼如下:

let t1 = new Promise((resolve,reject)=>{    resolve("t1-success")})let t2 = new Promise((resolve,reject)=>{    resolve("t2-success")})let t3 =Promise.reject("t3-error");Promise.all([t1,t2,t3]).then(res=>{    console.log(res)}).catch(error=>{    console.log(error)})//打印出來是t3-error

Promse.all在處理多個異步處理時非常有用,比如說一個頁面上需要等兩個或多個ajax的數據回來以後才正常顯示,在此之前只顯示loading圖標。

let request = (time,id) => {    return new Promise((resolve, reject) => {      setTimeout(() => {        resolve(`第${id}個請求${time / 1000}秒`)      }, time)    })  }    let p1 = request(3000,1)  let p2 = request(2000,2)    Promise.all([p1, p2]).then((result) => {    console.log(result)       // [ '第1個請求3秒', '第2個請求2秒' ]  }).catch((error) => {    console.log(error)  })

需要特別注意的是,Promise.all獲得的成功結果的數組裏面的數據順序和Promise.all接收到的數組順序是一致的,即p1的結果在前,即便p1的結果獲取的比p2要晚。這帶來了一個絕大的好處:在前端開發請求數據的過程中,偶爾會遇到發送多個請求並根據請求順序獲取和使用數據的場景,使用Promise.all毫無疑問可以解決這個問題。

5、Promise.race的使用

Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。

顧名思義,Promse.race就是賽跑的意思,意思就是說,Promise.race([p1, p2, p3])裏面哪個結果獲得的快,就返回那個結果,不管結果本身是成功狀態還是失敗狀態。

let f1 = new Promise((resolve, reject) => {    setTimeout(() => {      resolve('success')    },1000)  })    let f2 = new Promise((resolve, reject) => {    setTimeout(() => {      reject('failed')    }, 500)  })    Promise.race([f1, f2]).then((result) => {    console.log(result)  }).catch((error) => {    console.log(error)  // 打開的是 'failed'  })

原理是挺簡單的,但是在實際運用中還沒有想到什麼的使用場景會使用到。

舉例:超時取消

我們來看一下如何使用Promise.race來實現超時機制。

當然XHR有一個 timeout 屬性,使用該屬性也可以簡單實現超時功能,但是爲了能支持多個XHR同時超時或者其他功能,我們採用了容易理解的異步方式在XHR中通過超時來實現取消正在進行中的操作。

1. 讓Promise等待指定時間

首先我們來看一下如何在Promise中實現超時。

所謂超時就是要在經過一定時間後進行某些操作,使用 setTimeout 的話很好理解。

首先我們來串講一個單純的在Promise中調用 setTimeout 的函數。

//delayPromise.jsfunction delayPromise(ms) {    return new Promise(function (resolve) {        setTimeout(resolve, ms);    });}

delayPromise(ms) 返回一個在經過了參數指定的毫秒數後進行onFulfilled操作的promise對象,這和直接使用 setTimeout 函數比較起來只是編碼上略有不同,如下所示。

setTimeout(function () {    alert("已經過了100ms!");}, 100);// == 幾乎同樣的操作delayPromise(100).then(function () {    alert("已經過了100ms!");});

在這裏 promise對象 這個概念非常重要,請切記。

2. Promise.race中的超時

我們可以將剛纔的 delayPromise 和其它promise對象一起放到 Promise.race 中來是實現簡單的超時機制。

//simple-timeout-promise.jsfunction delayPromise(ms) {    return new Promise(function (resolve) {        setTimeout(resolve, ms);    });}function timeoutPromise(promise, ms) {    var timeout = delayPromise(ms).then(function () {            throw new Error('Operation timed out after ' + ms + ' ms');        });    return Promise.race([promise, timeout]);}

 

函數 timeoutPromise(比較對象promise, ms) 接收兩個參數,第一個是需要使用超時機制的promise對象,第二個參數是超時時間,它返回一個由 Promise.race 創建的相互競爭的promise對象。

之後我們就可以使用 timeoutPromise 編寫下面這樣的具有超時機制的代碼了。

function delayPromise(ms) {    return new Promise(function (resolve) {        setTimeout(resolve, ms);    });}function timeoutPromise(promise, ms) {    var timeout = delayPromise(ms).then(function () {            throw new Error('Operation timed out after ' + ms + ' ms');        });    return Promise.race([promise, timeout]);}// 運行示例var taskPromise = new Promise(function(resolve){    // 隨便一些什麼處理    var delay = Math.random() * 2000;    setTimeout(function(){        resolve(delay + "ms");    }, delay);});timeoutPromise(taskPromise, 1000).then(function(value){    console.log("taskPromise在規定時間內結束 : " + value);}).catch(function(error){    console.log("發生超時", error);});

雖然在發生超時的時候拋出了異常,但是這樣的話我們就不能區分這個異常到底是_普通的錯誤_還是_超時錯誤_了。

爲了能區分這個 Error 對象的類型,我們再來定義一個Error 對象的子類 TimeoutError。

擴展知識:定製Error對象

Error 對象是ECMAScript的內建(build in)對象。

但是由於stack trace等原因我們不能完美的創建一個繼承自 Error 的類,不過在這裏我們的目的只是爲了和Error有所區別,我們將創建一個 TimeoutError 類來實現我們的目的。

在ECMAScript6中可以使用 class 語法來定義類之間的繼承關係。

class MyError extends Error{    // 繼承了Error類的對象}

爲了讓我們的 TimeoutError 能支持類似 error instanceof TimeoutError 的使用方法,我們還需要進行如下工作。

//TimeoutError.jsfunction copyOwnFrom(target, source) {    Object.getOwnPropertyNames(source).forEach(function (propName) {        Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));    });    return target;}function TimeoutError() {    var superInstance = Error.apply(null, arguments);    copyOwnFrom(this, superInstance);}TimeoutError.prototype = Object.create(Error.prototype);TimeoutError.prototype.constructor = TimeoutError;

我們定義了 TimeoutError 類和構造函數,這個類繼承了Error的prototype。

它的使用方法和普通的 Error 對象一樣,使用 throw 語句即可,如下所示。

var promise = new Promise(function(){throw TimeoutError("timeout");});promise.catch(function(error){ console.log(error instanceof TimeoutError);// true});

有了這個 TimeoutError 對象,我們就能很容易區分捕獲的到底是因爲超時而導致的錯誤,還是其他原因導致的Error對象了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章