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
的實現以及筆者在平常使用過程當中的一些心得體會。node
Promise/A+規範主要分爲術語、要求和注意事項三個部分,咱們重點看一下第二部分也就是要求部分,以筆者的理解大概說明一下,具體細節參照完整版Promise/A+標準。git
一、
Promise
有三種狀態pending
,fulfilled
和rejected
。(爲了一致性,此文章稱fulfilled
狀態爲resolved
狀態)github
- 狀態轉換隻能是
pending
到resolved
或者pending
到rejected
;- 狀態一旦轉換完成,不能再次轉換。
二、
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
的狀態傳遞下去。三、不一樣的
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)函數具體運行規則:segmentfault
- 若是
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) } } }
規範解讀第2條,代碼實現:promise
/** * 擁有一個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
實現的回調函數老是異步調用的)。解決辦法:
resolvePromise
函數;then
方法執行時若是promise
仍然處於pending
狀態,則把處理函數進行儲存,等resolve/rejec
t函數真正執行的的時候再調用。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
的這些經常使用方法。
catch
方法是對then
方法的封裝,只用於接收reject(reason)
中的錯誤信息。由於在then
方法中onRejected
參數是可不傳的,不傳的狀況下,錯誤信息會依次日後傳遞,直到有onRejected
函數接收爲止,所以在寫promise
鏈式調用的時候,then
方法不傳onRejected
函數,只須要在最末尾加一個catch()
就能夠了,這樣在該鏈條中的promise
發生的錯誤都會被最後的catch
捕獲到。
catch(onRejected) { return this.then(null, onRejected); }
catch
在promise
鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,可是catch
方法內部也可能出現錯誤,因此有些promise
實現中增長了一個方法done
,done
至關於提供了一個不會出錯的catch
方法,而且再也不返回一個promise
,通常用來結束一個promise
鏈。
done() { this.catch(reason => { console.log('done', reason); throw reason; }); }
finally
方法用於不管是resolve
仍是reject
,finall
y的參數函數都會被執行。
finally(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; }); };
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++; } }); }
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); } }); }
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
是我司前端開發職位,nodejs
開發職位,全棧開發職位,必問的一個知識點,主要問題會分佈在Promise
介紹、基礎使用方法以及深層次的理解三個方面,問題通常在3-5個,根據面試者回答狀況會適當增減。
Promise
是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise
對象。有了Promise
對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise
對象提供統一的接口,使得控制異步操做更加容易。
(固然了也能夠簡單介紹promise
狀態,有什麼方法,callback
存在什麼問題等等,這個問題是比較開放的)
這個答案不是固定的,能夠參考最簡實現 Promise,支持異步鏈式調用
onResolved/onRejected
函數異步調用,錯誤捕獲合理等亮點。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
任務(從事件隊列中獲取)
瀏覽器運行機制可參考這篇文章
JS
運行機制的理解)Promise.deferred
、Promise.all
、Promise.race
、Promise.resolve
、Promise.reject
等
一、沒法取消Promise
,一旦新建它就會當即執行,沒法中途取消。
二、若是不設置回調函數,Promise
內部拋出的錯誤,不會反應到外部。
三、吞掉錯誤或異常,錯誤只能順序處理,即使在Promise
鏈最後添加catch
方法,依然可能存在沒法捕捉的錯誤(catch
內部可能會出現錯誤)
四、閱讀代碼不是一眼能夠看懂,你只會看到一堆then
,必須本身在then
的回調函數裏面理清邏輯。
(此題目,歡迎你們補充答案)
一、使用async
函數配合await
或者使用generator
函數配合yield
。
二、使用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()); }
promise
的理解程度,又能考察編程邏輯,最後還有bind
和reduce
等方法的運用)async
函數和generator
函數的能夠獲得20%的分數,能夠用promise.then
配合for
循環解決的能夠獲得60%的分數,配合Array.prototype.reduce
實現的能夠獲得最後的20%分數。在要中止的promise
鏈位置添加一個方法,返回一個永遠不執行resolve
或者reject
的Promise
,那麼這個promise
永遠處於pending
狀態,因此永遠也不會向下執行then
或catch
了。這樣咱們就中止了一個promise
鏈。
Promise.cancel = Promise.stop = function() { return new Promise(function(){}) }
(此題目,歡迎你們補充答案)
catch
在promise
鏈式調用的末尾調用,用於捕獲鏈條中的錯誤信息,可是catch
方法內部也可能出現錯誤,因此有些promise
實現中增長了一個方法done
,done
至關於提供了一個不會出錯的catch
方法,而且再也不返回一個promise
,通常用來結束一個promise
鏈。
done() { this.catch(reason => { console.log('done', reason); throw reason; }); }
promise
的理解程度)done()
方法代碼實現一、鏈式promise
要返回一個promise
,而不僅是構造一個promise
。
二、合理的使用Promise.all
和Promise.race
等方法。
三、在寫promise
鏈式調用的時候,then
方法不傳onRejected
函數,只須要在最末尾加一個catch()
就能夠了,這樣在該鏈條中的promise
發生的錯誤都會被最後的catch
捕獲到。若是catch()
代碼有出現錯誤的可能,須要在鏈式調用的末尾增長done()
函數。
(此題目,歡迎你們補充答案)
至此,我司關於Promise
的一些面試題目就列舉完畢了,有些題目的答案是開放的,歡迎你們一塊兒補充完善。總結起來,Promise
做爲js面試必問部分仍是相對容易掌握並經過的。
Promise做爲全部js開發者的必備技能,其實現思路值得全部人學習,經過這篇文章,但願小夥伴們在之後編碼過程當中能更加熟練、更加明白的使用Promise。
http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
http://www.javashuo.com/article/p-aqjjleyx-h.html