Promise原始碼詳解
學習知識要善於思考,思考,再思考。 —— 愛因斯坦
1.回撥地獄
曾幾何時,我們的程式碼是這樣的,為了拿到回撥的結果,不得不callback hell
,這種環環相扣的程式碼可以說是相當噁心了
let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})
複製程式碼
終於,我們的蓋世英雄
出現了,他身披金甲聖衣、駕著七彩祥雲。好吧打岔兒了,沒錯他就是我們的Promise
,那讓我們來看看用了Promise
之後,上面的程式碼會變成什麼樣吧
let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})
複製程式碼
如上所示
真的是很方便,有木有?意中人可以說是Swag
到變形了。那麼言歸正傳,我們怎麼才能自己寫一個這麼Swag
的解決非同步神器呢?
2.重點開始,小眼睛都看過來
2.1 Promise/A+
首先我們要知道自己手寫一個Promise
,應該怎麼去寫,誰來告訴我們怎麼寫,需要遵循什麼樣的規則。當然這些你都不用擔心,其實業界都是通過一個規則指標來生產Promise
的。讓我們來看看是什麼東西。傳送門☞Promise/A+
2.2 constructor
我們先宣告一個類,叫做Promise
,裡面是建構函式。如果es6還有問題的可以去阮大大的部落格上學習一下(傳送門☞es6)
class Promise{
constructor(executor){
//控制狀態,使用了一次之後,接下來的都不被使用
this.status = 'pendding'
this.value = undefined
this.reason = undefined
//定義resolve函式
let resolve = (data)=>{
//這裡pendding,主要是為了防止executor中呼叫了兩次resovle或reject方法,而我們只呼叫一次
if(this.status==='pendding'){
this.status = 'resolve'
this.value = data
}
}
//定義reject函式
let reject = (data)=>{
if(this.status==='pendding'){
this.status = 'reject'
this.reason = data
}
}
//executor方法可能會丟擲異常,需要捕獲
try{
//將resolve和reject函式給使用者
executor(resolve,reject)
}catch(e){
//如果在函式中丟擲異常則將它注入reject中
reject(e)
}
}
}
複製程式碼
那麼接下來我會分析上面程式碼的作用,原理
executor:
這是例項Promise
物件時在構造器中傳入的引數,一般是一個function(resolve,reject){}
status:``Promise
的狀態,一開始是預設的pendding狀態,每當呼叫道resolve和reject方法時,就會改變其值,在後面的then方法中會用到value:
resolve回撥成功後,呼叫resolve方法裡面的引數值reason:
reject回撥成功後,呼叫reject方法裡面的引數值resolve:
宣告resolve方法在構造器內,通過傳入的executor方法傳入其中,用以給使用者回撥reject:
宣告reject方法在構造器內,通過傳入的executor方法傳入其中,用以給使用者回撥
2.3 then
then方法是Promise
中最為重要的方法,他的用法大家都應該已經知道,就是將Promise
中的resolve或者reject的結果拿到,那麼我們就能知道這裡的then方法需要兩個引數,成功回撥和失敗回撥,上程式碼!
then(onFufilled,onRejected){
if(this.status === 'resolve'){
onFufilled(this.value)
}
if(this.status === 'reject'){
onRejected(this.reason)
}
}
複製程式碼
這裡主要做了將構造器中resolve和reject的結果傳入onFufilled
和onRejected
中,注意這兩個是使用者傳入的引數,是個方法。所以你以為這麼簡單就完了?要想更Swag
的應對各種場景,我們必須得再完善。繼續往下走!
3.非同步的Promise
之前我們只是處理了同步情況下的Promise,簡而言之所有操作都沒有非同步的成分在內。那麼如果是非同步該怎麼辦?
3.1 callback!!!!
最早處理非同步的方法就是callback,就相當於我讓你幫我掃地,我會在給你發起任務時給你一個手機,之後我做自己的事情去,不用等你,等你掃完地就會打手機給我,誒,我就知道了地掃完了。這個手機就是callback,回撥函式。
首先我們需要改一下構造器裡的程式碼,分別新增兩個回撥函式的陣列,分別對應成功回撥和失敗回撥。他們的作用是當成功執行resolve或reject時,執行callback。
//存放成功回撥的函式
this.onResolvedCallbacks = []
//存放失敗回撥的函式
this.onRejectedCallbacks = []
let resolve = (data)=>{
if(this.status==='pendding'){
this.status = 'resolve'
this.value = data
//監聽回撥函式
this.onResolvedCallbacks.forEach(fn=>fn())
}
}
let reject = (data)=>{
if(this.status==='pendding'){
this.status = 'reject'
this.reason = data
this.onRejectedCallbacks.forEach(fn=>fn())
}
}
複製程式碼
然後是then需要多加一個狀態判斷,當Promise中是非同步操作時,需要在我們之前定義的回撥函式陣列中新增一個回撥函式。
if(this.status === 'pendding'){
this.onResolvedCallbacks.push(()=>{
// to do....
let x = onFufilled(this.value)
resolvePromise(promise2,x,resolve,reject)
})
this.onRejectedCallbacks.push(()=>{
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
})
}
複製程式碼
ok!大功告成,非同步已經解決了
3.2 resolvePromise
這也是
Promise
中的重頭戲,我來介紹一下,我們在用Promise的時候可能會發現,當then函式中return了一個值,我們可以繼續then下去,不過是什麼值,都能在下一個then中獲取,還有,當我們不在then中放入引數,例:promise.then().then()
,那麼其後面的then依舊可以得到之前then返回的值,可能你現在想很迷惑。讓我來解開你心中的憂愁,follow me。
then(onFufilled,onRejected){
//解決onFufilled,onRejected沒有傳值的問題
onFufilled = typeof onFufilled === 'function'?onFufilled:y=>y
//因為錯誤的值要讓後面訪問到,所以這裡也要跑出個錯誤,不然會在之後then的resolve中捕獲
onRejected = typeof onRejected === 'function'?onRejected:err=>{ throw err ;}
//宣告一個promise物件
let promise2
if(this.status === 'resolve'){
//因為在.then之後又是一個promise物件,所以這裡肯定要返回一個promise物件
promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
//因為穿透值的緣故,在預設的跑出一個error後,不能再用下一個的reject來接受,只能通過try,catch
try{
//因為有的時候需要判斷then中的方法是否返回一個promise物件,所以需要判斷
//如果返回值為promise物件,則需要取出結果當作promise2的resolve結果
//如果不是,直接作為promise2的resolve結果
let x = onFufilled(this.value)
//抽離出一個公共方法來判斷他們是否為promise物件
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
}
if(this.status === 'reject'){
promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
try{
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
}
if(this.status === 'pendding'){
promise2 = new Promise((resolve,reject)=>{
this.onResolvedCallbacks.push(()=>{
// to do....
setTimeout(()=>{
try{
let x = onFufilled(this.value)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
this.onRejectedCallbacks.push(()=>{
setTimeout(()=>{
try{
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
})
})
}
return promise2
}
複製程式碼
一下子多了很多方法,不用怕,我會一一解釋
- 返回
Promise
?:首先我們要注意的一點是,then有返回值,then了之後還能在then,那就說明之前的then返回的必然是個Promise
。 - 為什麼外面要包一層
setTimeout
?:因為Promise本身是一個非同步方法,屬於微任務一列,必須得在執行棧執行完了在去取他的值,所以所有的返回值都得包一層非同步setTimeout。 - 為什麼開頭有兩個判斷?:這就是之前想要解決的如果then函式中的引數不是函式,那麼我們需要做處理。如果onFufilled不是函式,就需要自定義個函式用來返回之前resolve的值,如果onRejected不是函式,自定義個函式丟擲異常。這裡會有個小坑,如果這裡不丟擲異常,會在下一個then的onFufilled中拿到值。又因為這裡丟擲了異常所以所有的onFufilled或者onRejected都需要try/catch,這也是Promise/A+的規範。當然本人覺得成功的回撥不需要丟擲異常也可以,大家可以仔細想想。
resolvePromise
是什麼?:這其實是官方Promise/A+的需求。因為你的then可以返回任何職,當然包括Promise
物件,而如果是Promise
物件,我們就需要將他拆解,直到它不是一個Promise
物件,取其中的值。
那就讓我們來看看這個
resolvePromise
到底長啥樣。
function resolvePromise(promise2,x,resolve,reject){
//判斷x和promise2之間的關係
//因為promise2是上一個promise.then後的返回結果,所以如果相同,會導致下面的.then會是同一個promise2,一直都是,沒有盡頭
if(x === promise2){//相當於promise.then之後return了自己,因為then會等待return後的promise,導致自己等待自己,一直處於等待
return reject(new TypeError('迴圈引用'))
}
//如果x不是null,是物件或者方法
if(x !== null && (typeof x === 'object' || typeof x === 'function')){
//為了判斷resolve過的就不用再reject了,(比如有reject和resolve的時候)
let called
try{//防止then出現異常,Object.defineProperty
let then = x.then//取x的then方法可能會取到{then:{}},並沒有執行
if(typeof then === 'function'){
//我們就認為他是promise,call他,因為then方法中的this來自自己的promise物件
then.call(x,y=>{//第一個引數是將x這個promise方法作為this指向,後兩個引數分別為成功失敗回撥
if(called) return;
called = true
//因為可能promise中還有promise,所以需要遞迴
resolvePromise(promise2,y,resolve,reject)
},err=>{
if(called) return;
called = true
//一次錯誤就直接返回
reject(err)
})
}else{
//如果是個普通物件就直接返回resolve作為結果
resolve(x)
}
}catch(e){
if(called) return;
called = true
reject(e)
}
}else{
//這裡返回的是非函式,非物件的值,就直接放在promise2的resolve中作為結果
resolve(x)
}
}
複製程式碼
它的作用是用來將onFufilled的返回值進行判斷取值處理,把最後獲得的值放入最外面那層的
Promise
的resolve函式中。
- 引數
promise2
(then函式返回的Promise物件),x
(onFufilled函式的返回值),resolve、reject
(最外層的Promise上的resolve和reject)。 - 為什麼要在一開始判斷
promise2
和x
?:首先在Promise/A+中寫了需要判斷這兩者如果相等,需要丟擲異常,我就來解釋一下為什麼,如果這兩者相等,我們可以看下下面的例子,第一次p2是p1.then出來的結果是個Promise
物件,這個Promise
物件在被建立的時候呼叫了resolvePromise(promise2,x,resolve,reject)函式,又因為x等於其本身,是個Promise
,就需要then方法遞迴它,直到他不是Promise
物件,但是x(p2)的結果還在等待,他卻想執行自己的then方法,就會導致等待。
let p1 = new Promise((resolve,reject)=>{
resolve()
})
let p2 = p1.then(d=>{
return p2
})
複製程式碼
- called是用來幹嘛的?:called變數主要是用來判斷如果
resolvePromise
函式已經resolve或者reject了,那就不需要在執行下面的resolce或者reject。 - 為什麼取then這個屬性?:因為我們需要去判斷x是否為Promise,then屬性如果為普通值,就直接resolve掉,如果是個function,就是Promise物件,之後我們就需要將這個x的then方法進行執行,用call的原因是因為then方法裡面this指向的問題。
- 為什麼要遞迴去呼叫
resolvePromise
函式?:相信細心的人已經發現了,我這裡使用了遞迴呼叫法,首先這是Promise/A+中要求的,其次是業務場景的需求,當我們碰到那種Promise的resolve裡的Promise的resolve裡又包了一個Promise的話,就需要遞迴取值,直到x不是Promise物件。
4.完善Promise
我們現在已經基本完成了Promise的then方法,那麼現在我們需要看看他的其他方法。
4.1 catch
相信大家都知道catch這個方法是用來捕獲Promise中的reject的值,也就是相當於then方法中的onRejected回撥函式,那麼問題就解決了。我們來看程式碼。
//catch方法
catch(onRejected){
return this.then(null,onRejected)
}
複製程式碼
該方法是掛在Promise原型上的方法。當我們呼叫catch傳callback的時候,就相當於是呼叫了then方法。
4.2 resolve/reject
大家一定都看到過Promise.resolve()、Promise.reject()
這兩種用法,它們的作用其實就是返回一個Promise物件,我們來實現一下。
//resolve方法
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
})
}
//reject方法
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
})
}
複製程式碼
這兩個方法是直接可以通過class呼叫的,原理就是返回一個內部是resolve或reject的Promise物件。
4.3 all
all方法可以說是Promise中很常用的方法了,它的作用就是將一個陣列的Promise物件放在其中,當全部resolve的時候就會執行then方法,當有一個reject的時候就會執行catch,並且他們的結果也是按著陣列中的順序來排放的,那麼我們來實現一下。
//all方法(獲取所有的promise,都執行then,把結果放到陣列,一起返回)
Promise.all = function(promises){
let arr = []
let i = 0
function processData(index,data){
arr[index] = data
i++
if(i == promises.length){
resolve(arr)
}
}
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data)
},reject)
}
})
}
複製程式碼
其原理就是將引數中的陣列取出遍歷,每當執行成功都會執行
processData
方法,processData
方法就是用來記錄每個Promise的值和它對應的下標,當執行的次數等於陣列長度時就會執行resolve,把arr的值給then。這裡會有一個坑,如果你是通過arr陣列的長度來判斷他是否應該resolve的話就會出錯,為什麼呢?因為js陣列的特性,導致如果先出來的是1位置上的值進arr,那麼0位置上也會多一個空的值,所以不合理。
4.4 race
race方法雖然不常用,但是在Promise方法中也是一個能用得上的方法,它的作用是將一個Promise
陣列放入race中,哪個先執行完,race就直接執行完,並從then中取值。我們來實現一下吧。
//race方法
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
}
})
}
複製程式碼
原理大家應該看懂了,很簡單,就是遍歷陣列執行Promise,如果有一個
Promise
執行成功就resolve。
Promise語法糖 deferred
語法糖這三個字大家一定很熟悉,作為一個很Swag的前端工程師,對async/await這對兄弟肯定很熟悉,沒錯他們就是generator的語法糖。而我們這裡要講的語法糖是Promise的。
//promise語法糖 也用來測試
Promise.deferred = Promise.defer = function(){
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
複製程式碼
什麼作用呢?看下面程式碼你就知道了
let fs = require('fs')
let Promise = require('./promises')
//Promise上的語法糖,為了防止巢狀,方便呼叫
//壞處 錯誤處理不方便
function read(){
let defer = Promise.defer()
fs.readFile('./1.txt','utf8',(err,data)=>{
if(err)defer.reject(err)
defer.resolve(data)
})
return defer.Promise
}
複製程式碼
沒錯,我們可以方便的去呼叫他語法糖defer中的
Promise
物件。那麼它還有沒有另外的方法呢?答案是有的。我們需要在全域性上安裝promises-aplus-tests外掛npm i promises-aplus-tests -g
,再輸入promises-aplus-tests [js檔名] 即可驗證你的Promise的規範。
5.結尾
今天我們就做了一個屬於自己的Promise
專案,理解裡面的原始碼,方法原理,希望大家都有收穫。當然有什麼意見大家都可以在評論區評論,peace and love。
- git地址:github.com/Shinemax1/P…