javascript
方法 手写promise_JS探索-手写Promise
無意間在知乎上刷到Monad這個概念,去了解了一下,前端的Promise就是一種Monad模式,所以試著學習一下手寫一個Promise.
本文內容主要參考于
只會用?一起來手寫一個合乎規(guī)范的Promise?www.jianshu.comPromise是什么
所謂Promise,簡單說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進行處理。
Promise是處理異步編碼的一個解決方案,在Promise出現(xiàn)以前,異步代碼的編寫都是通過回調函數(shù)來處理的,回調函數(shù)本身沒有任何問題,只是當多次異步回調有邏輯關系時就會變得復雜:
const fs = require('fs'); fs.readFile('1.txt', (err,data) => {fs.readFile('2.txt', (err,data) => {fs.readFile('3.txt', (err,data) => {//可能還有后續(xù)代碼});}); });上面讀取了3個文件,它們是層層遞進的關系,可以看到多個異步代碼套在一起不是縱向發(fā)展的,而是橫向,不論是從語法上還是從排錯上都不好,于是Promise的出現(xiàn)可以解決這一痛點。
上述代碼如果改寫成Promise版是這樣:
可以看到,代碼是從上至下縱向發(fā)展了,更加符合人們的邏輯。
下面手寫一個Promise,按照Promises/A+規(guī)范,可以參照規(guī)范原文:
Promises/A+?promisesaplus.com手寫實現(xiàn)Promise是一道前端經(jīng)典的面試題,比如美團的面試就是必考題,Promise的邏輯還是比較復雜的,考慮的邏輯也比較多,下面總結手寫Promise的關鍵點,和怎樣使用代碼來實現(xiàn)它。
Promise代碼基本結構
實例化Promise對象時傳入一個函數(shù)作為執(zhí)行器,有兩個參數(shù)(resolve和reject)分別將結果變?yōu)槌晒B(tài)和失敗態(tài)。我們可以寫出基本結構
function Promise(executor) {this.state = 'pending'; //狀態(tài)this.value = undefined; //成功結果this.reason = undefined; //失敗原因function resolve(value) {}function reject(reason) {} }module.exports = Promise;其中state屬性保存了Promise對象的狀態(tài),規(guī)范中指明,一個Promise對象只有三種狀態(tài):等待態(tài)(pending)成功態(tài)(resolved)和失敗態(tài)(rejected)。
當一個Promise對象執(zhí)行成功了要有一個結果,它使用value屬性保存;也有可能由于某種原因失敗了,這個失敗原因放在reason屬性中保存。
then方法定義在原型上
每一個Promise實例都有一個then方法,它用來處理異步返回的結果,它是定義在原型上的方法,我們先寫一個空方法做好準備:
Promise.prototype.then = function (onFulfilled, onRejected) { };當實例化Promise時會立即執(zhí)行
當我們自己實例化一個Promise時,其執(zhí)行器函數(shù)(executor)會立即執(zhí)行,這是一定的:
let p = new Promise((resolve, reject) => {console.log('執(zhí)行了'); }); //運行結果:執(zhí)行了因此,當實例化Promise時,構造函數(shù)中就要馬上調用傳入的executor函數(shù)執(zhí)行
function Promise(executor) {var _this = this;this.state = 'pending';this.value = undefined;this.reason = undefined;executor(resolve, reject); //馬上執(zhí)行function resolve(value) {}function reject(reason) {} }已經(jīng)是成功態(tài)或是失敗態(tài)不可再更新狀態(tài)
規(guī)范中規(guī)定,當Promise對象已經(jīng)由pending狀態(tài)改變?yōu)榱顺晒B(tài)(resolved)或是失敗態(tài)(rejected)就不能再次更改狀態(tài)了。因此我們在更新狀態(tài)時要判斷,如果當前狀態(tài)是pending(等待態(tài))才可更新:
function resolve(value) {//當狀態(tài)為pending時再做更新if (_this.state === 'pending') {_this.value = value;//保存成功結果_this.state = 'resolved';}}function reject(reason) {//當狀態(tài)為pending時再做更新if (_this.state === 'pending') {_this.reason = reason;//保存失敗原因_this.state = 'rejected';}}以上可以看到,在resolve和reject函數(shù)中分別加入了判斷,只有當前狀態(tài)是pending才可進行操作,同時將成功的結果和失敗的原因都保存到對應的屬性上。之后將state屬性置為更新后的狀態(tài)。
then方法的基本實現(xiàn)
當Promise的狀態(tài)發(fā)生了改變,不論是成功或是失敗都會調用then方法,所以,then方法的實現(xiàn)也很簡單,根據(jù)state狀態(tài)來調用不同的回調函數(shù)即可:
Promise.prototype.then = function (onFulfilled, onRejected) {if (this.state === 'resolved') {//判斷參數(shù)類型,是函數(shù)執(zhí)行之if (typeof onFulfilled === 'function') {onFulfilled(this.value);}}if (this.state === 'rejected') {if (typeof onRejected === 'function') {onRejected(this.reason);}} };需要一點注意,規(guī)范中說明了,onFulfilled 和 onRejected 都是可選參數(shù),也就是說可以傳也可以不傳。傳入的回調函數(shù)也不是一個函數(shù)類型,那怎么辦?規(guī)范中說忽略它就好了。因此需要判斷一下回調函數(shù)的類型,如果明確是個函數(shù)再執(zhí)行它。
讓Promise支持異步
代碼寫到這里似乎基本功能都實現(xiàn)了,可是還有一個很大的問題,目前此Promise還不支持異步代碼,如果Promise中封裝的是異步操作,then方法無能為力:
let p = new Promise((resolve, reject) => {setTimeout(() => {resolve(1);}, 500); });p.then(data => console.log(data)); //沒有任何結果運行以上代碼發(fā)現(xiàn)沒有任何結果,本意是等500毫秒后執(zhí)行then方法,哪里有問題呢?原因是setTimeout函數(shù)使得resolve是異步執(zhí)行的,有延遲,當調用then方法的時候,此時此刻的狀態(tài)還是等待態(tài)(pending),因此then方法即沒有調用onFulfilled也沒有調用onRejected。
這個問題如何解決?我們可以參照發(fā)布訂閱模式,在執(zhí)行then方法時如果還在等待態(tài)(pending),就把回調函數(shù)臨時寄存到一個數(shù)組里,當狀態(tài)發(fā)生改變時依次從數(shù)組中取出執(zhí)行就好了,清楚這個思路我們實現(xiàn)它,首先在類上新增兩個Array類型的數(shù)組,用于存放回調函數(shù):
這樣當then方法執(zhí)行時,若狀態(tài)還在等待態(tài)(pending),將回調函數(shù)依次放入數(shù)組中:
Promise.prototype.then = function (onFulfilled, onRejected) {//等待態(tài),此時異步代碼還沒有走完if (this.state === 'pending') {if (typeof onFulfilled === 'function') {this.onFulfilledFunc.push(onFulfilled);//保存回調}if (typeof onRejected === 'function') {this.onRejectedFunc.push(onRejected);//保存回調}}//其它代碼略... };寄存好了回調,接下來就是當狀態(tài)改變時執(zhí)行就好了:
function resolve(value) {if (_this.state === 'pending') {_this.value = value;//依次執(zhí)行成功回調_this.onFulfilledFunc.forEach(fn => fn(value));_this.state = 'resolved';}}function reject(reason) {if (_this.state === 'pending') {_this.reason = reason;//依次執(zhí)行失敗回調_this.onRejectedFunc.forEach(fn => fn(reason));_this.state = 'rejected';}}至此,Promise已經(jīng)支持了異步操作,setTimeout延遲后也可正確執(zhí)行then方法返回結果。
鏈式調用
Promise處理異步代碼最強大的地方就是支持鏈式調用,這塊也是最復雜的,我們先梳理一下規(guī)范中是怎么定義的:
規(guī)范中說的很抽像,我們可以把不好理解的點使用代碼演示一下。
其中第3項,如果返回是個普通值就使用它包裝成Promise,我們用代碼來演示:
可見,當then返回了一個普通的值時,下一個then的成功態(tài)回調中即可取到上一個then的返回結果,說明了上一個then正是使用2來包裝成的Promise,這符合規(guī)范中說的。
第4項,如果then方法中沒有return語句,就視為返回一個用Undefined包裝的Promise對象
可以看到,當沒有返回任何值時不會報錯,沒有任何語句時實際上就是return undefined;即將undefined包裝成Promise對象傳給下一個then的成功態(tài)。
第6項,如果then方法沒有傳入任何回調,則繼續(xù)向下傳遞,這是什么意思呢?這就是Promise中值的穿透,還是用代碼演示一下:
以上代碼,在第一個then方法之后連續(xù)調用了兩個空的then方法 ,沒有傳入任何回調函數(shù),也沒有返回值,此時Promise會將值一值向下傳遞,直到你接收處理它,這就是所謂的值的穿透。
現(xiàn)在可以明白鏈式調用的原理,不論是何種情況then方法都會返回一個Promise對象,這樣才會有下個then方法。
搞清楚了這些點,我們就可以動手實現(xiàn)then方法的鏈式調用,一起來完善它:
首先,不論何種情況then都返回Promise對象,我們就實例化一個新promise2并返回。
接下來就處理根據(jù)上一個then方法的返回值來生成新Promise對象,由于這塊邏輯較復雜且有很多處調用,我們抽離出一個方法來操作,這也是規(guī)范中說明的:
resolvePromise方法用來封裝鏈式調用產(chǎn)生的結果,下面我們分別一個個情況的寫出它的邏輯,首先規(guī)范中說明,如果promise2和 x 指向同一對象,就使用TypeError作為原因轉為失敗。原文如下:
If promise and x refer to the same object, reject promise with a TypeError as the reason.這是什么意思?其實就是循環(huán)引用,當then的返回值與新生成的Promise對象為同一個(引用地址相同),則會拋出TypeError錯誤:
let promise2 = p.then(data => {return promise2; });運行結果:
TypeError: Chaining cycle detected for promise #<Promise>很顯然,如果返回了自己的Promise對象,狀態(tài)永遠為等待態(tài)(pending),再也無法成為resolved或是rejected,程序會死掉,因此首先要處理它:
function resolvePromise(promise2, x, resolve, reject) {if (promise2 === x) {reject(new TypeError('Promise發(fā)生了循環(huán)引用'));} }接下來就是分各種情況處理。當x就是一個Promise,那么就執(zhí)行它,成功即成功,失敗即失敗。若x是一個對象或是函數(shù),再進一步處理它,否則就是一個普通值:
function resolvePromise(promise2, x, resolve, reject) {if (promise2 === x) {reject(new TypeError('Promise發(fā)生了循環(huán)引用'));}if (x !== null && (typeof x === 'object' || typeof x === 'function')) {//可能是個對象或是函數(shù)} else {//否則是個普通值resolve(x);} }此時規(guī)范中說明,若是個對象,則嘗試將對象上的then方法取出來,此時如果報錯,那就將promise2轉為失敗態(tài)。原文:
If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason. //代碼略...if (x !== null && (typeof x === 'object' || typeof x === 'function')) {//可能是個對象或是函數(shù)try {let then = x.then;//取出then方法引用} catch (e) {reject(e);}} else {//否則是個普通值resolve(x);} }多說幾句,為什么取對象上的屬性有報錯的可能?Promise有很多實現(xiàn)(bluebird,Q等),Promises/A+只是一個規(guī)范,大家都按此規(guī)范來實現(xiàn)Promise才有可能通用,因此所有出錯的可能都要考慮到,假設另一個人實現(xiàn)的Promise對象使用Object.defineProperty()惡意的在取值時拋錯,我們可以防止代碼出現(xiàn)Bug。
此時,如果對象中有then,且then是函數(shù)類型,就可以認為是一個Promise對象,之后,使用x作為this來調用then方法。
這樣鏈式寫法就基本完成了。但是還有一種極端的情況,如果Promise對象轉為成功態(tài)或是失敗時傳入的還是一個Promise對象,此時應該繼續(xù)執(zhí)行,直到最后的Promise執(zhí)行完。
p.then(data => {return new Promise((resolve,reject)=>{//resolve傳入的還是Promiseresolve(new Promise((resolve,reject)=>{resolve(2);}));}); })此時就要使用遞歸操作了
很簡單,把調用resolve改寫成遞歸執(zhí)行resolvePromise方法即可,這樣直到解析Promise成一個普通值才會終止,即完成此規(guī)范:
//其他代碼略... if (x !== null && (typeof x === 'object' || typeof x === 'function')) {//可能是個對象或是函數(shù)try {let then = x.then; if (typeof then === 'function') {let y = then.call(x, (y) => {//遞歸調用,傳入y若是Promise對象,繼續(xù)循環(huán)resolvePromise(promise2, y, resolve, reject);}, (r) => {reject(r);});} else {resolve(x);}} catch (e) {reject(e);}} else {//是個普通值,最終結束遞歸resolve(x); }到此,鏈式調用的代碼已全部完畢。在相應的地方調用resolvePromise方法即可。
最后的最后
其實,寫到此處Promise的真正源碼已經(jīng)寫完了,但是距離100分還差一分,是什么呢?
規(guī)范中說明,Promise的then方法是異步執(zhí)行的。
ES6的原生Promise對象已經(jīng)實現(xiàn)了這一點,但是我們自己的代碼是同步執(zhí)行,不相信可以試一下,那么如何將同步代碼變成異步執(zhí)行呢?可以使用setTimeout函數(shù)來模擬一下:
setTimeout(()=>{//此入的代碼會異步執(zhí)行 },0);利用此技巧,將代碼then執(zhí)行處的所有地方使用setTimeout變?yōu)楫惒郊纯?#xff0c;舉個栗子:
setTimeout(() => {try {let x = onFulfilled(value);resolvePromise(promise2, x, resolve, reject);} catch (e) {reject(e);} },0);好了,現(xiàn)在已經(jīng)是滿分的Promise源碼了。
附上完整代碼
function Promise(executor) { let self = this this.status = 'pending' //當前狀態(tài) this.value = undefined //存儲成功的值 this.reason = undefined //存儲失敗的原因 this.onResolvedCallbacks = []//存儲成功的回調 this.onRejectedCallbacks = []//存儲失敗的回調 function resolve(value) {if (self.status == 'pending') {self.status = 'resolved'self.value = valueself.onResolvedCallbacks.forEach(fn => fn());} } function reject(error) {if (self.status == 'pending') {self.status = 'rejected'self.reason = errorself.onRejectedCallbacks.forEach(fn => fn())} } try {executor(resolve, reject) } catch (error) {reject(error) } } Promise.prototype.then = function (infulfilled, inrejected) { let self = this let promise2 infulfilled = typeof infulfilled === 'function' ? infulfilled : function (val) {return val } inrejected = typeof inrejected === 'function' ? inrejected : function (err) {throw err } if (this.status == 'resolved') {promise2 = new Promise(function (resolve, reject) {//x可能是一個promise,也可能是個普通值setTimeout(function () {try {let x = infulfilled(self.value)resolvePromise(promise2, x, resolve, reject)} catch (err) {reject(err)}});}) } if (this.status == 'rejected') {promise2 = new Promise(function (resolve, reject) {//x可能是一個promise,也可能是個普通值setTimeout(function () {try {let x = inrejected(self.reason)resolvePromise(promise2, x, resolve, reject)} catch (err) {reject(err)}});}) } if (this.status == 'pending') {promise2 = new Promise(function (resolve, reject) {self.onResolvedCallbacks.push(function () {//x可能是一個promise,也可能是個普通值setTimeout(function () {try {let x = infulfilled(self.value)resolvePromise(promise2, x, resolve, reject)} catch (err) {reject(err)}});})self.onRejectedCallbacks.push(function () {//x可能是一個promise,也可能是個普通值setTimeout(function () {try {let x = inrejected(self.reason)resolvePromise(promise2, x, resolve, reject)} catch (err) {reject(err)}});})}) } return promise2 } function resolvePromise(p2, x, resolve, reject) { if (p2 === x && x != undefined) {reject(new TypeError('類型錯誤')) } //可能是promise,看下對象中是否有then方法,如果有~那就是個promise if (x !== null && (typeof x === 'object' || typeof x === 'function')) {try {//為了防止出現(xiàn) {then:11}這種情況,需要判斷then是不是一個函數(shù)let then = x.thenif (typeof then === 'function') {then.call(x, function (y) {//y 可能還是一個promise,那就再去解析,知道返回一個普通值為止resolvePromise(p2, y, resolve, reject)}, function (err) {reject(err)})} else {//如果then不是function 那可能是對象或常量resolve(x)}} catch (e) {reject(e)} } else {//說明是一個普通值resolve(x) } }總結
以上是生活随笔為你收集整理的方法 手写promise_JS探索-手写Promise的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: xcode+文字支持html元素,iOS
- 下一篇: php和ajax的同步和异步请求,aja