Promise 知識彙總和麪試情況
寫在前面
Javascript
異步編程先後經歷了四個階段,分別是Callback
階段,Promise
階段,Generator
階段和Async/Await
階段。Callback
很快就被發現存在回調地獄和控制權問題,Promise
就是在這個時間出現,用以解決這些問題,Promise
並非一個新事務,而是按照一個規範實現的類,這個規範有很多,如 Promise/A
,Promise/B
,Promise/D
以及 Promise/A
的升級版 Promise/A+
,最終 ES6 中採用了 Promise/A+ 規範。後來出現的Generator
函數以及Async
函數也是以Promise
爲基礎的進一步封裝,可見Promise
在異步編程中的重要性。
關於Promise
的資料已經很多,但每個人理解都不一樣,不同的思路也會有不一樣的收穫。這篇文章會着重寫一下Promise
的實現以及筆者在日常使用過程中的一些心得體會。
實現 Promise
規範解讀
Promise/A+ 規範主要分爲術語、要求和注意事項三個部分,我們重點看一下第二部分也就是要求部分,以筆者的理解大概說明一下,具體細節參照完整版 Promise/A+ 標準。
1、
Promise
有三種狀態pending
,fulfilled
和rejected
。(爲了一致性,此文章稱fulfilled
狀態爲resolved
狀態)
- 狀態轉換隻能是
pending
到resolved
或者pending
到rejected
;- 狀態一旦轉換完成,不能再次轉換。
2、
Promise
擁有一個then
方法,用以處理resolved
或rejected
狀態下的值。
then
方法接收兩個參數onFulfilled
和onRejected
,這兩個參數變量類型是函數,如果不是函數將會被忽略,並且這兩個參數都是可選的。then
方法必須返回一個新的promise
,記作promise2
,這也就保證了then
方法可以在同一個promise
上多次調用。(ps:規範只要求返回promise
,並沒有明確要求返回一個新的promise
,這裏爲了跟 ES6 實現保持一致,我們也返回一個新promise
)onResolved/onRejected
有返回值則把返回值定義爲x
,並執行 [[Resolve]](promise2, x);onResolved/onRejected
運行出錯,則把promise2
設置爲rejected
狀態;onResolved/onRejected
不是函數,則需要把promise1
的狀態傳遞下去。3、不同的
promise
實現可以的交互。
規範中稱這一步操作爲
promise
解決過程,函數標示爲 [[Resolve]](promise, x),promise
爲要返回的新promise
對象,x
爲onResolved/onRejected
的返回值。如果x
有then
方法且看上去像一個promise
,我們就把 x 當成一個promis
e 的對象,即thenable
對象,這種情況下嘗試讓promise
接收x
的狀態。如果x
不是thenable
對象,就用x
的值來執行promise
。[[Resolve]](promise, x) 函數具體運行規則:
- 如果
promise
和x
指向同一對象,以TypeError
爲據因拒絕執行promise
;- 如果
x
爲Promise
,則使promise
接受x
的狀態;- 如果
x
爲對象或者函數,取x.then
的值,如果取值時出現錯誤,則讓promise
進入rejected
狀態,如果then
不是函數,說明x
不是thenable
對象,直接以x
的值resolve
,如果then
存在並且爲函數,則把x
作爲then
函數的作用域this
調用,then
方法接收兩個參數,resolvePromise
和rejectPromise
,如果resolvePromise
被執行,則以resolvePromise
的參數value
作爲x
繼續調用 [[Resolve]](promise, value),直到x
不是對象或者函數,如果rejectPromise
被執行則讓promise
進入rejected
狀態;- 如果
x
不是對象或者函數,直接就用x
的值來執行promise
。
代碼實現
規範解讀第 1 條,代碼實現:
class Promise {
// 定義Promise狀態,初始值爲pending
status = 'pending';
// 狀態轉換時攜帶的值,因爲在then方法中需要處理Promise成功或失敗時的值,所以需要一個全局變量存儲這個值
data = '';
// Promise構造函數,傳入參數爲一個可執行的函數
constructor(executor) {
// resolve函數負責把狀態轉換爲resolved
function resolve(value) {
this.status = 'resolved';
this.data = value;
}
// reject函數負責把狀態轉換爲rejected
function reject(reason) {
this.status = 'rejected';
this.data = reason;
}
// 直接執行executor函數,參數爲處理函數resolve, reject。因爲executor執行過程有可能會出錯,錯誤情況需要執行reject
try {
executor(resolve, reject);
} catch(e) {
reject(e)
}
}
}
第 1 條就是實現完畢了,相對簡單,配合代碼註釋很容易理解。
規範解讀第 2 條,代碼實現:
/**
* 擁有一個then方法
* then方法提供:狀態爲resolved時的回調函數onResolved,狀態爲rejected時的回調函數onRejected
* 返回一個新的Promise
*/
then(onResolved, onRejected) {
// 設置then的默認參數,默認參數實現Promise的值的穿透
onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e };
onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e };
let promise2;
promise2 = new Promise((resolve, reject) => {
// 如果狀態爲resolved,則執行onResolved
if (this.status === 'resolved') {
try {
// onResolved/onRejected有返回值則把返回值定義爲x
const x = onResolved(this.data);
// 執行[[Resolve]](promise2, x)
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}
// 如果狀態爲rejected,則執行onRejected
if (this.status === 'rejected') {
try {
const x = onRejected(this.data);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}
});
return promise2;
}
現在我們就按照規範解讀第 2 條,實現了上述代碼,上述代碼很明顯是有問題的,問題如下
resolvePromise
未定義;then
方法執行的時候,promise
可能仍然處於pending
狀態,因爲executor
中可能存在異步操作(實際情況大部分爲異步操作),這樣就導致onResolved/onRejected
失去了執行時機;onResolved/onRejected
這兩個函數需要異步調用 (官方Promise
實現的回調函數總是異步調用的)。
解決辦法:
- 根據規範解讀第 3 條,定義並實現
resolvePromise
函數; then
方法執行時如果promise
仍然處於pending
狀態,則把處理函數進行儲存,等resolve/reject
函數真正執行的的時候再調用。promise.then
屬於微任務,這裏我們爲了方便,用宏任務setTiemout
來代替實現異步,具體細節特別推薦這篇文章。
好了,有了解決辦法,我們就把代碼進一步完善:
class Promise {
// 定義Promise狀態變量,初始值爲pending
status = 'pending';
// 因爲在then方法中需要處理Promise成功或失敗時的值,所以需要一個全局變量存儲這個值
data = '';
// Promise resolve時的回調函數集
onResolvedCallback = [];
// Promise reject時的回調函數集
onRejectedCallback = [];
// Promise構造函數,傳入參數爲一個可執行的函數
constructor(executor) {
// resolve函數負責把狀態轉換爲resolved
function resolve(value) {
this.status = 'resolved';
this.data = value;
for (const func of this.onResolvedCallback) {
func(this.data);
}
}
// reject函數負責把狀態轉換爲rejected
function reject(reason) {
this.status = 'rejected';
this.data = reason;
for (const func of this.onRejectedCallback) {
func(this.data);
}
}
// 直接執行executor函數,參數爲處理函數resolve, reject。因爲executor執行過程有可能會出錯,錯誤情況需要執行reject
try {
executor(resolve, reject);
} catch(e) {
reject(e)
}
}
/**
* 擁有一個then方法
* then方法提供:狀態爲resolved時的回調函數onResolved,狀態爲rejected時的回調函數onRejected
* 返回一個新的Promise
*/
then(onResolved, onRejected) {
// 設置then的默認參數,默認參數實現Promise的值的穿透
onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e };
onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e };
let promise2;
promise2 = new Promise((resolve, reject) => {
// 如果狀態爲resolved,則執行onResolved
if (this.status === 'resolved') {
setTimeout(() => {
try {
// onResolved/onRejected有返回值則把返回值定義爲x
const x = onResolved(this.data);
// 執行[[Resolve]](promise2, x)
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
// 如果狀態爲rejected,則執行onRejected
if (this.status === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.data);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
// 如果狀態爲pending,則把處理函數進行存儲
if (this.status = 'pending') {
this.onResolvedCallback.push(() => {
setTimeout(() => {
try {
const x = onResolved(this.data);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallback.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.data);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
// [[Resolve]](promise2, x)函數
resolvePromise(promise2, x, resolve, reject) {
}
}
至此,規範中關於then
的部分就全部實現完畢了。代碼添加了詳細的註釋,參考註釋不難理解。
規範解讀第 3 條,代碼實現:
// [[Resolve]](promise2, x)函數
resolvePromise(promise2, x, resolve, reject) {
let called = false;
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'))
}
// 如果x仍然爲Promise的情況
if (x instanceof Promise) {
// 如果x的狀態還沒有確定,那麼它是有可能被一個thenable決定最終狀態和值,所以需要繼續調用resolvePromise
if (x.status === 'pending') {
x.then(function(value) {
resolvePromise(promise2, value, resolve, reject)
}, reject)
} else {
// 如果x狀態已經確定了,直接取它的狀態
x.then(resolve, reject)
}
return
}
if (x !== null && (Object.prototype.toString(x) === '[object Object]' || Object.prototype.toString(x) === '[object Function]')) {
try {
// 因爲x.then有可能是一個getter,這種情況下多次讀取就有可能產生副作用,所以通過變量called進行控制
const then = x.then
// then是函數,那就說明x是thenable,繼續執行resolvePromise函數,直到x爲普通值
if (typeof then === 'function') {
then.call(x, (y) => {
if (called) return;
called = true;
this.resolvePromise(promise2, y, resolve, reject);
}, (r) => {
if (called) return;
called = true;
reject(r);
})
} else { // 如果then不是函數,那就說明x不是thenable,直接resolve x
if (called) return ;
called = true;
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
這一步驟非常簡單,只要按照規範轉換成代碼即可。
最後,完整的Promise
按照規範就實現完畢了,是的,規範裏並沒有規定catch
、Promise.resolve
、Promise.reject
、Promise.all
等方法,接下來,我們就看一看Promise
的這些常用方法。
Promise 其他方法實現
1、catch 方法
catch
方法是對then
方法的封裝,只用於接收reject(reason)
中的錯誤信息。因爲在then
方法中onRejected
參數是可不傳的,不傳的情況下,錯誤信息會依次往後傳遞,直到有onRejected
函數接收爲止,因此在寫promise
鏈式調用的時候,then
方法不傳onRejected
函數,只需要在最末尾加一個catch()
就可以了,這樣在該鏈條中的promise
發生的錯誤都會被最後的catch
捕獲到。
catch(onRejected) {
return this.then(null, onRejected);
}
2、done 方法
catch
在promise
鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,但是catch
方法內部也可能出現錯誤,所以有些promise
實現中增加了一個方法done
,done
相當於提供了一個不會出錯的catch
方法,並且不再返回一個promise
,一般用來結束一個promise
鏈。
done() {
this.catch(reason => {
console.log('done', reason);
throw reason;
});
}
3、finally 方法
finally
方法用於無論是resolve
還是reject
,finall
y 的參數函數都會被執行。
finally(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};
4、Promise.all 方法
Promise.all
方法接收一個promise
數組,返回一個新promise2
,併發執行數組中的全部promise
,所有promise
狀態都爲resolved
時,promise2
狀態爲resolved
並返回全部promise
結果,結果順序和promise
數組順序一致。如果有一個promise
爲rejected
狀態,則整個promise2
進入rejected
狀態。
static all(promiseList) {
return new Promise((resolve, reject) => {
const result = [];
let i = 0;
for (const p of promiseList) {
p.then(value => {
result[i] = value;
if (result.length === promiseList.length) {
resolve(result);
}
}, reject);
i++;
}
});
}
5、Promise.race 方法
Promise.race
方法接收一個promise
數組, 返回一個新promise2
,順序執行數組中的promise
,有一個promise
狀態確定,promise2
狀態即確定,並且同這個promise
的狀態一致。
static race(promiseList) {
return new Promise((resolve, reject) => {
for (const p of promiseList) {
p.then((value) => {
resolve(value);
}, reject);
}
});
}
6、Promise.resolve 方法 / Promise.reject
Promise.resolve
用來生成一個rejected
完成態的promise
,Promise.reject
用來生成一個rejected
失敗態的promise
。
static resolve(value) {
let promise;
promise = new Promise((resolve, reject) => {
this.resolvePromise(promise, value, resolve, reject);
});
return promise;
}
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
常用的方法基本就這些,Promise
還有很多擴展方法,這裏就不一一展示,基本上都是對then
方法的進一步封裝,只要你的then
方法沒有問題,其他方法就都可以依賴then
方法實現。
Promise 面試相關
面試相關問題,筆者只說一下我司這幾年的情況,並不能代表全部情況,參考即可。
Promise
是我司前端開發職位,nodejs
開發職位,全棧開發職位,必問的一個知識點,主要問題會分佈在Promise
介紹、基礎使用方法以及深層次的理解三個方面,問題一般在 3-5 個,根據面試者回答情況會適當增減。
1、簡單介紹下 Promise。
Promise
是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise
對象。有了Promise
對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise
對象提供統一的接口,使得控制異步操作更加容易。
(當然了也可以簡單介紹promise
狀態,有什麼方法,callback
存在什麼問題等等,這個問題是比較開放的)
- 提問概率:99%
- 評分標準:人性化判斷即可,此問題一般作爲引入問題。
- 加分項:熟練說出 Promise 具體解決了那些問題,存在什麼缺點,應用方向等等。
2、實現一個簡單的,支持異步鏈式調用的 Promise 類。
這個答案不是固定的,可以參考最簡實現 Promise,支持異步鏈式調用
- 提問概率:50%(手擼代碼題,因爲這類題目比較耗費時間,一場面試並不會出現很多,所以出現頻率不是很高,但卻是必備知識)
- 加分項:基本功能實現的基礎上有
onResolved/onRejected
函數異步調用,錯誤捕獲合理等亮點。
3、Promise.then 在 Event Loop 中的執行順序。(可以直接問,也可以出具體題目讓面試者回答打印順序)
JS
中分爲兩種任務類型:macrotask
和microtask
,其中macrotask
包含:主代碼塊,setTimeout
,setInterval
,setImmediate
等(setImmediate
規定:在下一次Event Loop
(宏任務)時觸發);microtask
包含:Promise
,process.nextTick
等(在node
環境下,process.nextTick
的優先級高於Promise
)
Event Loop
中執行一個macrotask
任務(棧中沒有就從事件隊列中獲取)執行過程中如果遇到microtask
任務,就將它添加到微任務的任務隊列中,macrotask
任務執行完畢後,立即執行當前微任務隊列中的所有microtask
任務(依次執行),然後開始下一個macrotask
任務(從事件隊列中獲取)
瀏覽器運行機制可參考這篇文章
- 提問概率:75%(可以理解爲 4 次面試中 3 次會問到,順便可以考察面試者對
JS
運行機制的理解) - 加分項:擴展講述瀏覽器運行機制。
4、闡述 Promise 的一些靜態方法。
Promise.deferred
、Promise.all
、Promise.race
、Promise.resolve
、Promise.reject
等
- 提問概率:25%(相對基礎的問題,一般在其他問題回答不是很理想的情況下提問,或者爲了引出下一個題目而提問)
- 加分項:越多越好
5、Promise 存在哪些缺點。
1、無法取消Promise
,一旦新建它就會立即執行,無法中途取消。
2、如果不設置回調函數,Promise
內部拋出的錯誤,不會反應到外部。
3、吞掉錯誤或異常,錯誤只能順序處理,即便在Promise
鏈最後添加catch
方法,依然可能存在無法捕捉的錯誤(catch
內部可能會出現錯誤)
4、閱讀代碼不是一眼可以看懂,你只會看到一堆then
,必須自己在then
的回調函數里面理清邏輯。
- 提問概率:25%(此問題作爲提高題目,出現概率不高)
- 加分項:越多越合理越好(網上有很多說法,不一一佐證)
(此題目,歡迎大家補充答案)
6、使用 Promise 進行順序(sequence)處理。
1、使用async
函數配合await
或者使用generator
函數配合yield
。
2、使用promise.then
通過for
循環或者Array.prototype.reduce
實現。
function sequenceTasks(tasks) {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(() => task).then(pushValue);
}, Promise.resolve());
}
- 提問概率:90%(我司提問概率極高的題目,即能考察面試者對
promise
的理解程度,又能考察編程邏輯,最後還有bind
和reduce
等方法的運用) - 評分標準:說出任意解決方法即可,其中只能說出
async
函數和generator
函數的可以得到 20% 的分數,可以用promise.then
配合for
循環解決的可以得到 60% 的分數,配合Array.prototype.reduce
實現的可以得到最後的 20% 分數。
7、如何停止一個 Promise 鏈?
在要停止的promise
鏈位置添加一個方法,返回一個永遠不執行resolve
或者reject
的Promise
,那麼這個promise
永遠處於pending
狀態,所以永遠也不會向下執行then
或catch
了。這樣我們就停止了一個promise
鏈。
Promise.cancel = Promise.stop = function() {
return new Promise(function(){})
}
- 提問概率:50%(此問題主要考察面試者羅輯思維)
(此題目,歡迎大家補充答案)
8、Promise 鏈上返回的最後一個 Promise 出錯了怎麼辦?
catch
在promise
鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,但是catch
方法內部也可能出現錯誤,所以有些promise
實現中增加了一個方法done
,done
相當於提供了一個不會出錯的catch
方法,並且不再返回一個promise
,一般用來結束一個promise
鏈。
done() {
this.catch(reason => {
console.log('done', reason);
throw reason;
});
}
- 提問概率:90%(同樣作爲出題率極高的一個題目,充分考察面試者對
promise
的理解程度) - 加分項:給出具體的
done()
方法代碼實現
9、Promise 存在哪些使用技巧或者最佳實踐?
1、鏈式promise
要返回一個promise
,而不只是構造一個promise
。
2、合理的使用Promise.all
和Promise.race
等方法。
3、在寫promise
鏈式調用的時候,then
方法不傳onRejected
函數,只需要在最末尾加一個catch()
就可以了,這樣在該鏈條中的promise
發生的錯誤都會被最後的catch
捕獲到。如果catch()
代碼有出現錯誤的可能,需要在鏈式調用的末尾增加done()
函數。
- 提問概率:10%(出題概率極低的一個題目)
- 加分項:越多越好
(此題目,歡迎大家補充答案)
至此,我司關於Promise
的一些面試題目就列舉完畢了,有些題目的答案是開放的,歡迎大家一起補充完善。總結起來,Promise
作爲 js 面試必問部分還是相對容易掌握並通過的。
總結
Promise 作爲所有 js 開發者的必備技能,其實現思路值得所有人學習,通過這篇文章,希望小夥伴們在以後編碼過程中能更加熟練、更加明白的使用 Promise。
參考鏈接:
http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
https://segmentfault.com/a/1190000016550260
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://segmentfault.com/a/1190000039699000