小邵教你玩轉promise原始碼

邵威儒發表於2018-08-12

前言:大家好,我叫邵威儒,大家都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程式猿的坑,從大學買的第一本vb和自學vb,我就與程式設計結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟體,至今進入了前端領域,看到不少朋友都寫文章分享,自己也弄一個玩玩,以下文章純屬個人理解,便於記錄學習,肯定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享個人對技術的通俗理解,共同成長!

後續我會陸陸續續更新javascript方面,儘量把javascript這個學習路徑體系都寫一下
包括前端所常用的es6、angular、react、vue、nodejs、koa、express、公眾號等等
都會從淺到深,從入門開始逐步寫,希望能讓大家有所收穫,也希望大家關注我~

原始碼地址:github.com/iamswr/prom…
文章列表:juejin.im/user/5a84f8…

Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/


JavaScript作為單執行緒語言,其特點也是其缺陷,特點就是不用處理多執行緒引發的佔用資源、衝突啪啦啪啦等,缺陷就是同一時間,只能做一件事情,那麼會存在一個問題,網路傳輸是有延遲的,比如A發一條資訊到B伺服器,在B伺服器還沒返回資訊給A時,那麼A就會一直在等待接收資訊,會造成頁面的假死,那麼該怎麼辦?俗話說得好,程式猿改變世界,於是乎出現了非同步的概念,我會分以下幾點,去講述我對前端非同步的理解:

  1. callback
  2. 閉包、高階函式
  3. promise
  4. generator ( 文章地址:juejin.im/post/5b7512…
  5. async / await ( 文章地址:juejin.im/post/5b7512…

一.callback

回撥函式被認為是一種高階函式,一種被作為引數傳遞給另一個函式(在這稱作"otherFunction")的高階函式,回撥函式會在otherFunction內被呼叫(或執行)。回撥函式的本質是一種模式(一種解決常見問題的模式),因此回撥函式也被稱為回撥模式。

是不是看起來一頭懵逼,到底什麼是回撥函式?有什麼作用?我對回撥函式理解,從真正意義上讓我突然間恍然大悟的,就是當初研究jQuery底層原始碼的時候,我們看以下一段程式碼:

<img src='../a.jpg'></img>
<img src='../b.jpg'></img>
<img src='../c.jpg'></img>

$("img").attr("title",function(index,attr){
    console.log(index) // 依次返回0 1 2
});
複製程式碼

在attr方法的第二個引數,傳入了一個function,而該函式,會依次獲取$('img')的DOM物件對應index和attr,我們可以在該function裡,寫我們需要的業務邏輯,那麼這樣有什麼好處呢?

我的理解是,假如我要封裝一個庫,造一個輪子,那麼要考慮到通用性和複用性並且提供一個途徑,讓使用者任意發揮想象寫業務邏輯,並且把相關可能使用到的引數,都傳給使用者。

假設,我們現在有一個需求,要寫一個判斷型別的方法,常見的方法有以下幾種:

 - typeOf // 簡單的資料型別判斷,棧區
 - instanceof // 複雜的資料型別,堆區
 - constructor // 複雜的資料型別,主要是用在繼承的改寫指向的建構函式,很少用於判斷型別
 - Object.prototype.toString.call() // 絕大多數庫底層都是使用該方式,返回值如[object String]
複製程式碼

首先,我們寫一個isType方法

function isType(content,type,fn){
    // 型別判斷
    let t = Object.prototype.toString
                              .call(content)
                              .replace(/\[object\s|\]/g,'')
                              
    // 判斷完成後,執行傳入的callback函式
    fn(type,t)
}
複製程式碼

現在我們要判斷一個值的型別,然後拿到這個型別,最終執行我們需要做的事情

isType('hello swr','String',function(type,t){ // 作為引數傳入的函式,接收isType函式內的fn中type和t這兩個引數
    console.log(type === t) // true
})
複製程式碼

小邵教你玩轉promise原始碼
首先執行了isType函式,然後執行isType函式內部的程式碼,isType內的fn(type,t),這裡的fn方法,實際就是我們傳入的第三個引數,即裡面只有一行console.log(type === t)的函式,而t則是isType函式內的t,可以稍微把整個流程體會一下,就差不多明白回撥函式是幹嘛的了~

那麼問題就出現了,比如我們使用node.js的時候,進行檔案讀取操作時,想獲取的值是一種巢狀依賴關係時,會出現什麼問題呢?

目錄結構:
- iamswr
  - A.txt
  - B.txt
  - C.txt
  
  其中
  A.txt檔案裡的內容為字串B.txt
  B.txt檔案裡的內容為字串C.txt
  C.txt檔案裡的內容為字串'hello swr'
  
  那麼當我們想獲取到'hello swr',會遇到什麼問題呢?請看下面的程式碼
  let fs = require('fs')
  fs.readFile('A.txt','utf8',function(err,data){ // 此時回撥函式data值為'B.txt'
      fs.readFile(data,'utf8',function(err,data){ // 此時回撥函式data值為'C.txt'
          fs.readFile(data,'utf8',function(err,data){
            console.log(data) // 'hello swr'
          })
      })
  })
複製程式碼

以上這個例子如果巢狀依賴層次更高一些,那程式碼變得十分難維護以及難閱讀,我們在企業開發當中,經常會遇到想得到的資料,是通過巢狀依賴的關係,最終才獲得需要的資料,陷入了回撥地獄,而es6中,promise解決了這個讓前端頭疼的問題,後面我會詳細講promise,下面我們先了解一下閉包、高階函式。


二.閉包、高階函式

我個人理解,閉包實際上是一種函式,所以閉包技術也是函式技術的一種;閉包能做的事情函式幾乎都能做,閉包有最大的兩個用處,一個是可以讀取函式內部的變數,另一個就是讓這些變數的值始終保持在記憶體中。

1.封閉作用域

在javascript中,如果一個物件不被引用了,那麼這個物件會被GC回收,否則則一直保留在記憶體中,那麼利用這個特點,配合閉包使用,有以下幾個優點:封閉作用域、儲存作用域、作用域鏈條。

不汙染全域性變數,當團隊協作時,比如A大佬,封裝了jQuery庫,而jQuery庫內是有大量變數,如果不使用閉包,則jQuery庫內的變數會汙染整個專案,甚至和其他團員的變數有衝突

外部無法獲取閉包內的變數,封閉了作用域
(function(){
    var str = 'hello swr'
    console.log(str) // 'hello swr'
})()

console.log(str) // 報錯

我們用原生js來寫程式碼的時候,會存在一個問題,
比如有5個button標籤
var btns = document.getElementsByTagName('button');
for(var i=0; i< btns.length; i++){   
   var btn = btns[i];        
   btn.onclick = function () {            
     alert('點選了第' + i + '個按鈕');       
   }
}
無論我們點選哪個button,都是彈出'點選了第5個按鈕',
因為btn.onclick事件是非同步觸發的,當事件被觸發時,
for迴圈早已經結束,此時變數I的值已經是5,
所有onclick事件函式從內到外查詢變數i時,查詢到的值總是5。

可以通過封閉作用域把每次迴圈的i值都封閉起來,
當時間函式順著作用域鏈從內到外查詢變數i時,
會先找到被封閉在閉包環境中的i,
如果有5個按鈕, 則i的值就是0,1,2,3,4
var btns = document.getElementsByTagName('button');
for(var i=0; i< btns.length; i++){   
    (function (i) {        
       var btn = btns[i];        
       btn.onclick = function () {            
         alert('點選了第' + i + '個按鈕');       
       }    
     })(i);
}

複製程式碼

2.作用域鏈

我們知道,在es6之前,只有函式是有作用域的說法,在es6出現了,則有了塊級作用域的說法,比如

(function person(){
    var name = '邵威儒'
    console.log(name) // '邵威儒'
})()

console.log(name) // 報錯
複製程式碼

在函式外部,是訪問不了內部的name,這就是作用域。 在es6出了一個新的概念,就是塊級作用域

{
    let name = '邵威儒'
    console.log(name) // '邵威儒'
}

console.log(name) // 報錯
複製程式碼

效果和閉包一樣

3.儲存作用域

函式巢狀函式,那麼內部的那個函式將形成作用域閉包。簡單的說,這種閉包能夠達到的好處就是讓指令能夠繫結一些全域性資料去執行,優點是全域性資料隱藏化、 將資料繫結在指令上執行,讓指令不再依賴全域性資料。

function plus(num){
    ++num
    return function(){
        console.log(num)
    }
}

let toPlus = plus(5)
此時toPlus實際上為
function(){
    console.log(num)
}
而這個num實際上就是plus函式內作用域的num,此時我們無法從外部修改num,而且把plus函式內的資料隱藏化,將資料繫結在toPlus上執行。
複製程式碼

實際開發中遇到的問題

比如說,我們實際開發中會遇到一個問題,就是某個函式,要等多個非同步執行完畢後才執行,這種情況怎麼做呢?

一般會想到以下這個辦法

let fs = require('fs')
let arr = []

fs.readFile('./a.txt','utf8',function(err,data){
    arr.push(data) // 假設data為'hello'
})

fs.readFile('./b.txt','utf8',function(err,data){
    arr.push(data) // 假設data為'swr'
})

console.log(arr) // 我們希望列印出來是['hello','swr']或['swr','hello'],但是列印出來的卻是[]
這是為什麼呢?
是因為javascript執行原理,是先執行同步,再執行非同步的,而fs.readFile方法屬於非同步方法,所以還沒執行完畢,就已經執行了console.log(arr)了
複製程式碼

對於這種並非依賴巢狀獲取,我們稱為“同步”獲取,此同步非非同步同步的那個同步,特別是這種非同步請求的資料,獲取到的時間先後順序不同,那我們該如何實現“同步”獲取呢?

let fs = require('fs')

function after(times,callback){
    let arr = []
    return function(data){
        arr.push(data)
        if(--times === 0){
            callback(arr)
        }
    }
}

let fn = after(2,function(arr){
    console.log(arr) // 當fn執行兩次後,則會執行該回撥函式
})

fs.readFile('./a.txt','utf8',function(err,data){
    fn(data) // 假設data為'hello'
})

fs.readFile('./b.txt','uft8',function(err,data)=>{
    fn(data) // 假設data為'swr'
})

最終當2個fs.readFile讀取完畢後,執行了fn()達到2次時,則會列印出['hello','swr']或者['swr','hello']
複製程式碼

雖然以上的方式,實現了我們需要的需求,但是問題來了,難道我們每一次都要特意寫一個after函式嗎?其實還有一個概念,叫做釋出訂閱,訂閱就類似你收藏了這個電臺,而釋出,則是這個電臺向所有收藏了本電臺的粉絲進行廣播,看下面程式碼

let fs = require('fs')

let event = {
    arr:[], // 存需要執行的函式
    result:[], // 存結果
    on(fn){ // 訂閱
        this.arr.push(fn)
    },
    emit(data){ // 釋出
        this.result.push(data)
        this.arr.forEach(fn=>fn(this.result))
    }
}

event.on(function(data){
    if(data.length === 2){
        console.log(data) // ['hello','swr'] 或者 ['swr','hello']
    }
})

fs.readFile('./a.txt','utf8',(err,data)=>{
    event.emit(data) // data為'hello'
})

fs.readFile('./b.txt','utf8',(err,data)=>{
    event.emit(data) // data為'swr'
})

當兩個fs.readFile讀取完成,並且在其回撥函式內執行了event.emit,最終會列印出['hello','swr'] 或者 ['swr','hello']
複製程式碼

三.Promise

囉囉嗦嗦說了那麼多,主要是想大家瞭解一下回撥函式以及閉包,因為這概念和promise的緊密關聯的,promise部分我主要是想和大家根據promiseAplus規範,逐步手寫一個promise的底層實現方式。

首先,promise怎麼理解?我在知乎上看到一篇比較通俗易懂的小故事,大家可以看看,zhuanlan.zhihu.com/p/19622332

早上,老爸說:“兒子,天氣如何?” 每週一早上,老爸問兒子下午的天氣情況,兒子可以到自家房子旁邊小山上使用望遠鏡來觀看。兒子在出發時許諾(Promise)老爸(會通知老爸天氣情況)。 此刻,老爸決定,如果天氣不錯,明天就出去捕魚,否則就不去。而且如果兒子無法獲得天氣預報的話,也不去捕魚。 30分鐘左右,兒子回來了,每週的結局都不一樣。

結局A:成功獲得了(retrieved)天氣預報,晴天 :) 兒子成功獲取了天氣預報,天空晴朗,陽光明媚!承諾(Promise)兌現了(resolved),於是老爸決定開始為週日的捕魚做準備。

結局B:同樣成功獲得了天氣預報,雨天:( 兒子成功獲得了天氣預報,只不過是烏雲密佈,要下雨。承諾(Promise)兌現了(resolved),只是老爸決定呆在家裡,因為天氣很糟糕。

結局C:沒法獲得天氣預報:-/ 出了問題,兒子沒法得知天氣預報,因為霧很大,就算站在小山上也無法看清。兒子沒辦法物件他離開時許下的諾言, promise was rejected!老爸決定留下來,這並不值得冒險。

Promise的一些特性

首先我們要了解PromiseA+規範 promisesaplus.com/

  • promise是有相容性問題的,node環境下預設支援,還可以下載相應外掛來解決相容性問題
  • promise是有三種狀態的,等待態pending / 成功態resolved / 失敗態rejected
  • promise的狀態是可以轉換的,可以從pending -> resolved 或 pending -> rejected,但是resolved不能轉換為rejected/pending,rejected不能轉換為resolved/pending,簡而言之即狀態只會更改一次
// Promise建構函式的第一個引數為executor
let promise = new Promise(function(resolve,reject){
    console.log('我是會被立即執行的喲')
})

// promise的例項都有then方法
promise.then(()=>{ // 成功的回撥
    
},()=>{ // 失敗的回撥
    
})
複製程式碼
  • executor預設在new的時候會自動執行
  • 每個promise的例項都有then方法
  • then方法中,有兩個引數,分別是成功的回撥函式和失敗的回撥函式
// 預設時為pending態,既不會走成功的回撥也不會走失敗的回撥
promise.then(()=>{
    console.log('success1')
},()=>{
    console.log('error1')
})

console.log('2')

在這段程式碼中,只會列印出'2',因為promise一直處於pending態,不會走then後的回撥函式
複製程式碼
let promise = new Promise(function(resolve,reject){
    console.log('1')
    resolve() // 更改pending狀態為resolved
})

promise.then(()=>{
    console.log('success1')
},()=>{
    console.log('error1')
})

console.log('2')

此時輸出順序為'1' -> '2' -> 'success1'
複製程式碼
  • then方法是非同步的,屬於微任務,從上面的例子可以看出,先執行完同步程式碼,再執行非同步程式碼
let promise = new Promise(function(resolve,reject){
    console.log('1')
    setTimeout(()=>{ // 非同步行為
        resolve() // 更改狀態為成功
    },1000)
})

promise.then(()=>{
    console.log("success1")
})

promise.then(()=>{
    console.log('success2')
})

console.log("2")

此時輸出順序為'1' -> '2' -> 'success1' -> 'success2'
複製程式碼
  • 同一個promise的例項可以then多次,成功時會呼叫所有的成功方法,失敗時會呼叫所有的失敗方法
  • new Promise中可以支援非同步行為
let promise = new Promise(function(resolve,reject){
    throw new Error('出錯了') // 丟擲錯誤
})

promise.then(()=>{
    console.log('success1')
},()=>{
    console.log('error1')
})

此時輸出為 'error1'
複製程式碼
  • 如果發現錯誤,就會進入失敗態

實現一個Promise

下面程式碼部分和原始碼實現部分要結合來看

// ----- 程式碼部分
// 1.executor預設在new的時候會自動執行
// 成功和失敗的視乎可以傳遞引數
let promise = new Promise((resolve,reject)=>{ // 6.resolve、reject函式對應原始碼實現部分的resolve、reject函式
    resolve('hello swr') // 11.執行resolve
})

// 7.Promise的例項都有then方法
promise.then((data)=>{ // 8.成功的回撥函式
    
},(err)=>{ // 9.失敗的回撥函式
    
})
複製程式碼
// ----- 原始碼實現部分
// 2.宣告一個Promise建構函式
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined // 12.因為value和reason值需要在Promise例項方法then中使用,所以把這兩個值,賦給new出來的例項
    function resolve(value){ // 3.宣告一個resolve函式
        self.value = value // 13.當呼叫了resolve並且傳引數時,則把這value值賦予self.value
    }
    
    function reject(reason){ // 4.宣告一個reject函式
        self.reason = reason // 13.當呼叫了reject並且傳引數時,則把這reason值賦予self.reason
    }
    executor(resolve,reject) // 5.把resolve、reject函式傳到executor
}

// 因為Promise的例項都有then方法,那麼意味著then方法是在Promise的原型物件中的方法
// 10.對應上面成功的回撥函式onFulfilled以及失敗的回撥函式onRejected
Promise.prototype.then = function(onFulfilled,onRejected){
    
}

module.exports = Promise // 把Promise暴露出去
複製程式碼

此時,我們會發現,如何去判斷呼叫resolve還是reject呢? 這個時候我們在內部應該維護一個狀態,而我們之前說過了Promise有三種狀態,分別為pending、resolved、rejected,那麼我們接著看下面的程式碼。

// ----- 程式碼部分
let promise = new Promise((resolve,reject)=>{
    resolve('hello swr') // 5.暫時忽略此行
    resolve('看看同時執行resolve和reject會發生什麼?')  // 5.此行執行resovle
    reject('看看同時執行resolve和reject會發生什麼?') // 5.此行執行reject
})

promise.then((data)=>{
    console.log('success:' + data) // 5.當呼叫了resolve函式,則輸出success:hello swr
},(err)=>{
    
})
複製程式碼
// ----- 原始碼實現部分
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined 
    self.status = 'pending' // 1.在內部維護一個status狀態
    function resolve(value){
        self.value = value 
        self.status = 'resolved' // 2.當呼叫了resolve時,更改狀態為resolved
    }
    
    function reject(reason){
        self.reason = reason 
        self.status = 'rejected' // 2.當呼叫了reject時,更改狀態為rejected
    }
    executor(resolve,reject)
}

Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    // 3.當我們在then中,執行了成功或者失敗的回撥函式時,首先要判斷目前處於什麼狀態
    if(self.status === 'resolved'){
        onFulfilled(self.value) // 4.當呼叫了resolve函式後,會執行成功的回撥函式,並且把resolve中傳遞的值,傳遞給成功的回撥函式
    }
    
    if(self.status === 'rejected'){
        onRejected(self.reason) // 4.當呼叫了reject函式後,會執行成功的回撥函式,並且把reject中傳遞的值,傳遞給失敗的回撥函式
    }
}

module.exports = Promise
複製程式碼

當我們在上面5中同時執行resolve和reject,會發現都能夠執行,那麼就違背了狀態只能更改一次的原則了,下面我們來解決這個問題。

// ----- 程式碼部分
let promise = new Promise((resolve,reject)=>{
    resolve('看看同時執行resolve和reject會發生什麼?') // 1. 此時執行resolve和reject
    reject('看看同時執行resolve和reject會發生什麼?') // 3.此時即使呼叫reject,因為resolve已經呼叫了一次,從pending更改為resolve,所以在第一次呼叫後,多次呼叫也不會生效
    
    // 4.以上resolve、reject暫時忽略掉,我們考慮一個情況,當promise丟擲錯誤時,怎麼去處理呢?
    throw new Error('出錯啦')
})

promise.then((data)=>{
    console.log('success:' + data)
},(err)=>{
    
})
複製程式碼
// ----- 原始碼實現部分
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined 
    self.status = 'pending'
    function resolve(value){
        if(self.status === 'pending'){ // 2.此時新增一個狀態判斷,當狀態為pending的時候才能執行
            self.value = value 
            self.status = 'resolved'
        }
    }
    
    function reject(reason){
        if(self.status === 'pending'){ // 2.此時新增一個狀態判斷,當狀態為pending的時候才能執行
            self.reason = reason 
            self.status = 'rejected'
        }
    }
    
    // 5.當我們在執行executor時,內部丟擲錯誤的時候,可以利用try catch來處理這個問題
    try{
        executor(resolve,reject)
    }catch(error){
        reject(error)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    if(self.status === 'resolved'){
        onFulfilled(self.value) 
    }
    
    if(self.status === 'rejected'){
        onRejected(self.reason) 
    }
}

module.exports = Promise
複製程式碼

這樣我們就解決了多次呼叫,只認第一次的更改狀態,並且當丟擲錯誤時,使用try catch來處理,那麼接下來,我們想一下,目前我們都是new一個Promise,然後呼叫then,這整個流程,彷彿沒任何問題,但是,現在問題出現了,如果此時resolve或者reject是處於setTimeout(()=>{resolve()},3000)中,即處於非同步中,當我們new一個Promise時,不會馬上執行非同步程式碼,而是直接執行了promise.then這個函式,而此時因為self.status的狀態依然是處於pending,所以不會執行resolve或者reject,當同步程式碼執行完畢後,執行非同步程式碼時,更改了狀態為resolved或者rejected時,此時then方法已經執行完畢了,不會再次執行then的方法,那麼此時我們該如何處理?

還存在一個問題,就是上面所說的,同一個promise的例項可以then多次,成功時會呼叫所有的成功方法,失敗時會呼叫所有的失敗方法,那這個又該如何處理呢?

可以利用我們前面所說的釋出訂閱的思路來解決,現在我們看下面程式碼。

// ----- 程式碼部分
let promise = new Promise((resolve,reject)=>{
    setTimeout(()=>{ // 1.此時resolve處於非同步
        resolve('hello swr')
    },3000)
})

promise.then((data)=>{ // 多個then
    console.log('success1:' + data)
},(err)=>{
    
})
promise.then((data)=>{ // 多個then
    console.log('success2:' + data)
},(err)=>{
    
})
複製程式碼
// ----- 原始碼實現部分
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined 
    self.status = 'pending'
    self.onResolvedCallbacks = [] // 2.可能new Promise中會有非同步的操作,此時我們把非同步操作時,執行的then函式的成功回撥,統一儲存在該陣列中
    self.onRejectedCallbacks = [] // 2.可能new Promise中會有非同步的操作,此時我們把非同步操作時,執行的then函式的失敗回撥,統一儲存在該陣列中
    function resolve(value){
        if(self.status === 'pending'){ 
            self.value = value 
            self.status = 'resolved'
            // 4.當呼叫resolve時,把該陣列中存放的成功回撥都執行一遍,如果是非同步,則會把成功的回撥都存到該陣列裡了,如果是非同步,則沒存到。
            self.onResolvedCallbacks.forEach(fn=>fn())
        }
    }
    
    function reject(reason){
        if(self.status === 'pending'){ 
            self.reason = reason 
            self.status = 'rejected'
            // 4.當呼叫reject時,把該陣列中存放的失敗回撥都執行一遍,如果是非同步,則會把成功的回撥都存到該陣列裡了,如果是非同步,則沒存到。
            self.onRejectedCallbacks.forEach(fn=>fn())
        }
    }
    
    try{
        executor(resolve,reject)
    }catch(error){
        reject(error)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    if(self.status === 'resolved'){
        onFulfilled(self.value) 
    }
    
    if(self.status === 'rejected'){
        onRejected(self.reason) 
    }
    
    // 3.當new Promise中有resolve、reject處於非同步中,執行then的時候,狀態為pending,
    if(self.status === 'pending'){
        self.onResolvedCallbacks.push(()=>{
            onFulfilled(self.value)
        }) // 3. 把成功的回撥函式,存到該陣列中,這樣寫的好處,就是把引數傳進去,不需要將來遍歷onResolvedCallbacks時,再傳參
        self.onRejectedCallbacks.push(()=>{
            onRejected(self.reason)
        }) // 3. 把失敗的回撥函式,存到該陣列中,這樣寫的好處,就是把引數傳進去,不需要將來遍歷onRejectedCallbacks時,再傳參
    }
}

module.exports = Promise
複製程式碼

到此為止,我們簡版的Promise實現得差不多了,小夥伴們可以對著程式碼敲一下,感受一下,體會一下。


Promise的鏈式呼叫

其實Promise的核心在於鏈式呼叫,Promise主要是解決2個問題:

  • 回撥地獄
  • 併發非同步io操作,同一時間內把這個結果拿到,即比如有兩個非同步io操作,當這2個獲取完畢後,才執行相應的程式碼,比如前面所說的after函式,釋出訂閱、Promise.all等。

首先,比如回撥地獄怎麼解決呢?那麼我們來看下面的程式碼,並且改為promise。

// 回撥函式
let fs = require('fs')
fs.readFile('./a.txt','utf8',(err,data)=>{ // 往fs.readFile方法傳遞了第三個為函式的引數
    if(err){
        console.log(err)
        return
    }
    console.log(data)
})

// 改寫為Promise
let fs = require('fs')
function read(filePath,encoding){
    return new Promise((resolve,reject)=>{
        fs.readFile(filePath,encoding,(err,data)=>{
            if(err) reject(err)
            resolve()
        })
    })
}

read('./a.txt','utf8').then((data)=>{ // 在這裡則不再需要傳回撥函式進去,而是採用then來達到鏈式呼叫
    console.log(data)
},(err)=>{
    console.log(err)
})

// 這樣看好像Promise也沒什麼優勢,那麼接下來我們對比一下
// 假設有3個檔案
// - 1.txt    文字內容為'2.txt'
// - 2.txt    文字內容為'3.txt'
// - 3.txt    文字內容為'hello swr'

// 用回撥函式
fs.readFile('./1.txt','utf8',(err,data)=>{
    fs.readFile(data,'utf8',(err,data)=>{
        fs.readFile(data,'utf8',(err,data)=>{
            console.log(data) // hello swr
        })
    })
})

// 用Promise
read('./1.txt','utf8')
.then((data)=>{
    // 1.如果一個promise執行完後,返回的還是一個promise,
    //   會把這個promise的執行結果會傳遞給下一次thenreturn read(data,'utf8')
})
.then((data)=>{
    return read(data,'utf8')
})
.then((data)=>{
    // 2.如果在then中返回的不是一個promise,
    //   而是一個普通值,會將這個普通值作為下次then的成功的結果
    return data.split('').reverse().join('')
})
.then((data)=>{
    console.log(data) // rws olleh
    // 3.如果當前then中失敗了,會走下一個then的失敗回撥
    throw new Error('出錯')
})
.then(null,(err)=>{
    console.log(err) // Error:出錯   報錯了
    // 4.如果在then中不返回值,雖然沒有顯式返回,
    //   但是預設是返回undefined,是屬於普通值,依然會把這個普通值傳到
    //   下一個then的成功回撥中
})
.then((data)=>{
    console.log(data) // undefined
})
複製程式碼

從上面可以看得出,改寫為Promise的程式碼,更好閱讀和維護,從用Promise方式可以得出結論:

  • 1.如果一個promise執行完後,返回的還是一個promise,會把這個promise的執行結果會傳遞給下一次then中
  • 2.如果在then中返回的不是一個promise,而是一個普通值,會將這個普通值作為下次then的成功的結果
  • 3.如果當前then中失敗了,會走下一個then的失敗回撥
  • 4.如果在then中不返回值,雖然沒有顯式返回,但是預設是返回undefined,是屬於普通值,依然會把這個普通值傳到下一個then的成功回撥中
// 如果在then中丟擲錯誤,會怎樣呢?
// 情景一,會被下一個then中的失敗回撥捕獲
read('./1.txt','utf8')
.then((data)=>{
    throw new Error('出錯了')
})
.then(null,(err)=>{
    console.log(err) // Error:出錯了   報錯
})

// 情景二,如果沒有被失敗的回撥捕獲,丟擲錯誤最終會變成異常
read('./1.txt','utf8')
.then((data)=>{
    throw new Error('出錯了')
})

// 情景三,如果沒有被失敗的回撥捕獲,那麼最終會被catch捕獲到
read('./1.txt','utf8')
.then((data)=>{
    throw new Error('出錯了')
})
.then((data)=>{
    
})
.catch((err)=>{
    console.log(err) // Error:出錯了   報錯
})

// 情景四,如果被失敗的回撥捕獲了,那麼不會被catch捕獲到
read('./1.txt','utf8')
.then((data)=>{
    throw new Error('出錯了')
})
.then(null,(err)=>{
    console.log(err) // Error:出錯了   報錯
})
.catch((err)=>{
    console.log(err)  // 不會執行到這裡
})
複製程式碼
  • 5.catch是錯誤沒有處理的情況下才會執行
  • 6.then中可以不寫東西

穿插一個與jquery的鏈式呼叫區別

jquery的鏈式呼叫,是通過其內部執行完後return this,返回自身這個物件,達到鏈式呼叫的目的,那為什麼Promise不採用這種方式呢?

我們可以看以下程式碼,感受一下。

let promise = new Promise((resolve,reject)=>{
    resolve() // 執行resolve,使狀態從pending變為resolved
})

let promise2 = promise.then(()=>{
    throw new Error() // 丟擲錯誤
    return this // 返回自身
})

// 那麼我在promise2中,調then,那麼它會執行失敗的回撥嗎?答案是不會的。
// 因為我們不可能讓狀態既成功又失敗的
// promise成功了,如果返回this,那不能走向失敗
promise2.then(()=>{
    console.log('來到這裡了') 
},()=>{
    console.log('會來到這裡嗎?')
})

// 此時then中返回自身後,promise2其實就是promise,而我們想達到
// 的是把當前的then返回後,傳到下一個then中,但是我們這樣返回this,
// 其實會變得很矛盾,因為狀態已經從pending變為resolved,不可能又從resolved變成rejected的
// 所以得出結論,返回的必須是一個新的promise,因為promise成功後不能再走失敗
// 只能建立一個新的promise再執行業務邏輯,返回同一個promise的話,就不能既成功又失敗
複製程式碼
實現Promise鏈式呼叫
// ----- 程式碼部分
let promise = new Promise((resolve,reject)=>{
    resolve()
})

// 2.返回的值為promise2 為什麼這樣規定呢?這是promiseA+規範規定的,我們要遵循
let promise2 = promise.then((data)=>{
    return x // 1.then中的返回值x可能是普通值也可能是promise,並且傳給下一個then
}).then((data)=>{
    console.log(data) // x的值
})
複製程式碼
// ----- 原始碼實現部分
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined 
    self.status = 'pending'
    self.onResolvedCallbacks = []  
    self.onRejectedCallbacks = []
    function resolve(value){
        if(self.status === 'pending'){ 
            self.value = value 
            self.status = 'resolved'
            self.onResolvedCallbacks.forEach(fn=>fn())
        }
    }
    
    function reject(reason){
        if(self.status === 'pending'){ 
            self.reason = reason 
            self.status = 'rejected'
            self.onRejectedCallbacks.forEach(fn=>fn())
        }
    }
    
    try{
        executor(resolve,reject)
    }catch(error){
        reject(error)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    let self = this
    let promise2 // 3.上面講promise鏈式呼叫時,已經說了返回的是一個新的promise物件,那麼我們宣告一個新的promise
    
    // 4.那麼我們new一個新的promise,並且把以下程式碼放到promise中
    let promise2 = new Promise((resolve,reject)=>{
        if(self.status === 'resolved'){
            // 7.當執行成功回撥的時候,可能會出現異常,那麼就把這個異常作為promise2的錯誤的結果
            try{
                let x = onFulfilled(self.value) // 6.這裡的x,就是上面then中執行完返回的結果,我們在這裡宣告一個x用來接收
                // 8.根據promiseA+規範,我們應該提供一個函式來處理promise2
                //   我個人的理解是,then中不管是成功回撥還是失敗回撥,其返回
                //   值,有可能是promise,也有可能是普通值,也有可能是丟擲錯誤
                //   那麼我們就需要一個函式來處理這幾種不同的情況
                //   這個函式我們宣告為resolvePromise吧
                resolvePromise(promise2,x,resolve,reject)
                // 9. 這裡的promise2就是當前的promise2,x則是執行then中成功回撥後返回的結果,如果是成功則調promise2的resolve,失敗則調reject
            }catch(e){
                reject(e) // 注意:這裡的reject是這個promise2的reject
            }
        }
        
        if(self.status === 'rejected'){
            // 同6-7步
            try{
                let x = onRejected(self.reason) 
                // 同8-9
                resolvePromise(promise2,x,resolve,reject)
            }catch(e){
                reject(e)
            }
        }
    
        if(self.status === 'pending'){
            self.onResolvedCallbacks.push(()=>{
                // 同6-7步
                try{
                    let x =  onFulfilled(self.value)
                    // 同8-9
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e)
                }
            }) 
            self.onRejectedCallbacks.push(()=>{
                // 同6-7步
                try{
                    let x = onRejected(self.reason)
                    // 同8-9
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e)
                }
            })
        }
    })
    return promise2 // 5.在jquery中是return this,但是在promise中,則是返回一個新的promise物件
}

module.exports = Promise
複製程式碼

寫一個resolvePromise函式

接下來我們寫一下resolvePromise這個函式,整個Promise最核心的部分就是在這裡

// ----- 程式碼部分
let promise = new Promise((resolve,reject)=>{
    resolve()
})

let promise2 = promise.then((data)=>{
    return x 
}).then((data)=>{
    console.log(data) 
})

// 2.我們在resolvePromise函式中,在原生情況下,如果傳參的時候,promise2和x是同一個物件會發生什麼呢?
let promise = new Promise((resolve,reject)=>{
    resolve()
})

let promise2 = promise.then(()=>{
    return promise2 
    // 2.1報錯 UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>
    // 報錯的意思是,陷入了死迴圈,那怎麼理解呢?
    // promise2的成功或失敗是要取決於promise中then的返回結果,而返回的卻是promi2自己
    // 這樣就陷入死迴圈了,promise2是依賴於promise的then返回的結果,
    // 而then返回的結果是promise2,而then中的promise2,既不是成功也不是失敗,不能自己等於自己
})

// 7.當取一個物件上的屬性,可能存在報異常的情況,怎麼理解呢?
// 因為這個方法有可能不是自己寫的,可能別人搞惡作劇亂寫的,看以下程式碼。
let obj = {}
// 給obj物件定義一個then方法,當我們去obj物件中呼叫then方法時
// 就會執行裡面的get,而get則是丟擲異常
Object.defineProperty(obj,'then',{
    get(){
        throw new Error()
    }
})

// 10.為什麼要用call呢?解決了什麼問題?看一下以下程式碼
首先我們執行
promise.then(()=>{
    console.log(this) // 此時this是指向該promise的,物件的方法中this是指向這個物件的
})

但是我們在下面通過let then = promise.then,來判斷是否promise,是否會異常
當我們執行then時,裡面的this還是會指向這個promise嗎?答案是不一定的,
因為此時then,如果在全域性下執行,指向的可能就是window了,所以為了讓this的
指向正確,我們需要通過
then.call(promise),來把then的this指向promise
複製程式碼
// ----- 原始碼實現部分
function Promise(executor){
    let self = this
    self.value = undefined
    self.reason = undefined 
    self.status = 'pending'
    self.onResolvedCallbacks = []  
    self.onRejectedCallbacks = []
    function resolve(value){
        if(self.status === 'pending'){ 
            self.value = value 
            self.status = 'resolved'
            self.onResolvedCallbacks.forEach(fn=>fn())
        }
    }
    
    function reject(reason){
        if(self.status === 'pending'){ 
            self.reason = reason 
            self.status = 'rejected'
            self.onRejectedCallbacks.forEach(fn=>fn())
        }
    }
    
    try{
        executor(resolve,reject)
    }catch(error){
        reject(error)
    }
}

// 1.宣告一個resolvePromise函式
// 這個函式非常核心,所有的promise都遵循這個規範,所有的promise可以通用,
/**
 * 
 * @param {*} promise2 then的返回值,返回新的promise
 * @param {*} x then中成功函式或者失敗函式的返回值
 * @param {*} resolve promise2的resolve
 * @param {*} reject promise2的reject
 */
function resolvePromise(promise2,x,resolve,reject){
    // 3.從2中我們可以得出,自己不能等於自己
    // 當promise2和x是同一個物件的時候,則走reject
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle detected for promise'))
    }
    // 4.因為then中的返回值可以為promise,當x為物件或者函式,才有可能返回的是promise
    let called
    if(x !== null && (typeof x === 'object' || typeof x === 'function')){
        // 8.從第7步,可以看出為什麼會存在丟擲異常的可能,所以使用try catch處理
        try{
            // 6.因為當x為promise的話,是存在then方法的
            // 但是我們取一個物件上的屬性,也有可能出現異常,我們可以看一下第7步
            let then = x.then 
            
            // 9.我們為什麼在這裡用call呢?解決了什麼問題呢?可以看上面的第10步
            // x可能還是個promise,那麼就讓這個promise執行
            // 但是還是存在一個惡作劇的情況,就是{then:{}}
            // 此時需要新增一個判斷then是否函式
            if(typeof === 'function'){
                then.call(x,(y)=>{ // y是返回promise後的成功結果
                    // 一開始我們在這裡寫的是resolve(y),但是考慮到一點
                    // 這個y,有可能還是一個promise,
                    // 也就是說resolve(new Promise(...))
                    // 所以涉及到遞迴,我們把resolve(y)改成以下
                    
                    // 12.限制既調resolve,也調reject
                    if(called) return
                    called = true
                    
                    resolvePromise(promise2,y,resolve,reject)
                    // 這樣的話,程式碼會一直遞迴,取到最後一層promise
                    
                    // 11.這裡有一種情況,就是不能既調成功也調失敗,只能挑一次,
                    // 但是我們前面不是處理過這個情況了嗎?
                    // 理論上是這樣的,但是我們前面也說了,resolvePromise這個函式
                    // 是所有promise通用的,也可以是別人寫的promise,如果別人
                    // 的promise可能既會調resolve也會調reject,那麼就會出問題了,所以我們接下來要
                    // 做一下限制,這個我們寫在第12步
                    
                },(err)=>{ // err是返回promise後的失敗結果
                    if(called) return
                    called = true
                    reject(err)
                })
            }else{
                resolve(x) // 如果then不是函式的話,那麼則是普通物件,直接走resolve成功
            }
        }catch(e){ // 當出現異常則直接走reject失敗
            if(called) return
            called = true
            reject(e)
        }
    }else{ // 5.x為一個常量,則是走resolve成功
        resolve(x)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    // onFulfilled、onRejected是可選引數
    onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
    onRejected = typeof onRejected === 'function'?onRejected: err=>{throw err}
    let self = this
    let promise2 
    let promise2 = new Promise((resolve,reject)=>{
        if(self.status === 'resolved'){
            // 13.根據promiseA+規範,onFulfilled或onRejected必須
            // 被呼叫不是當前的上下文,then方法是非同步的
            setTimeout(()=>{
                try{
                    let x = onFulfilled(self.value)
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e) 
                }
            },0)
        }
        
        if(self.status === 'rejected'){
            // 同13
            setTimeout(()=>{
                try{
                    let x = onRejected(self.reason) 
                    resolvePromise(promise2,x,resolve,reject)
                }catch(e){
                    reject(e)
                }
            },0)
        }
    
        if(self.status === 'pending'){
            self.onResolvedCallbacks.push(()=>{
                // 同13
                setTimeout(()=>{
                    try{
                        let x =  onFulfilled(self.value)
                        resolvePromise(promise2,x,resolve,reject)
                    }catch(e){
                        reject(e)
                    }
                },0)
            }) 
            self.onRejectedCallbacks.push(()=>{
                // 同13
                setTimeout(()=>{
                    try{
                        let x = onRejected(self.reason)
                        resolvePromise(promise2,x,resolve,reject)
                    }catch(e){
                        reject(e)
                    }
                },0)
            })
        }
    })
    return promise2
}

// 14.到目前為止,根據promiseA+規範的程式碼寫得差不多了,我們可以通過測試程式碼來測試我們是否寫得正確,下面我們寫一段測試程式碼

Promise.defer = Promise.deferred = function(){
    let dfd = {}
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

// 14.接下來我們要安裝一個外掛,npm install promises-aplus-test -g

module.exports = Promise
複製程式碼
// 完整程式碼 也順便帶大家理順一下
function Promise(executor) {
    let self = this;
    self.value = undefined;  // 成功的值
    self.reason = undefined;  // 失敗的值
    self.status = 'pending'; // 目前promise的狀態pending
    self.onResolvedCallbacks = []; // 可能new Promise的時候會存在非同步操作,把成功和失敗的回撥儲存起來
    self.onRejectedCallbacks = [];
    function resolve(value) { // 把狀態更改為成功
        if (self.status === 'pending') { // 只有在pending的狀態才能轉為成功態
            self.value = value;
            self.status = 'resolved';
            self.onResolvedCallbacks.forEach(fn => fn()); // 把new Promise時非同步操作,存在的成功回撥儲存起來
        }
    }
    function reject(reason) {  // 把狀態更改為失敗
        if (self.status === 'pending') { // 只有在pending的狀態才能轉為失敗態
            self.reason = reason;
            self.status = 'rejected';
            self.onRejectedCallbacks.forEach(fn => fn()); // 把new Promise時非同步操作,存在的失敗回撥儲存起來
        }
    }
    try {
        // 在new Promise的時候,立即執行的函式,稱為執行器
        executor(resolve, reject);
    } catch (e) { // 如果執行executor丟擲錯誤,則會走失敗reject
        reject(e);
    }
}

// 這個函式為核心,所有的promise都遵循這個規範
// 主要是處理then中返回的值x和promise2的關係
function resolvePromise(promise2,x,resolve,reject){
    // 當promise2和then返回的值x為同一個物件時,變成了自己等自己,會陷入死迴圈
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'));
    }
    let called;
    // x可能是一個promise也可能是一個普通值
    if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
        try{
            let then = x.then; 
            if(typeof then === 'function'){
                then.call(x,y=>{ 
                    if(called) return; 
                    called = true;
                    resolvePromise(promise2,y,resolve,reject);
                },err=>{ 
                    if(called) return;
                    called = true;
                    reject(err);
                });
            }else{
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true;
            reject(e);
        }
    }else{ 
        resolve(x);
    }
}

// then呼叫的時候,都是屬於非同步,是一個微任務
// 微任務會比巨集任務先執行
// onFulfilled為成功的回撥,onRejected為失敗的回撥
Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
    onRejected = typeof onRejected === 'function'?onRejected: err=>{throw err}
    let self = this;
    let promise2;
    // 上面講了,promise和jquery的區別,promise不能單純返回自身,
    // 而是每次都是返回一個新的promise,才可以實現鏈式呼叫,
    // 因為同一個promise的pending resolve reject只能更改一次
    promise2 = new Promise((resolve, reject) => {
        if (self.status === 'resolved') {
            // 為什麼要加setTimeout?
            // 首先是promiseA+規範要求的
            // 其次是大家寫的程式碼,有的是同步,有的是非同步
            // 所以為了更加統一,就使用為setTimeout變為非同步了,保持一致性
            setTimeout(()=>{
                try { // 上面executor雖然使用try catch捕捉錯誤
                      // 但是在非同步中,不一定能夠捕捉,所以在這裡
                      // 用try catch捕捉
                    let x = onFulfilled(self.value);
                    // 在then中,返回值可能是一個promise,所以
                    // 需要resolvePromise對返回值進行判斷
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'rejected') {
            setTimeout(()=>{
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'pending') {
            self.onResolvedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
            self.onRejectedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
        }
    });
    return promise2
}
Promise.defer = Promise.deferred = function(){
    let dfd = {};
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd;
}
module.exports = Promise;
複製程式碼
執行promises-aplus-tests promise.js
複製程式碼

小邵教你玩轉promise原始碼

小邵教你玩轉promise原始碼

到此為止,我們已經寫了一個符合promiseA+規範的promise了,大家可以好好多看幾次。


接下來,我們完善一下這個promise,寫一下常用的promise方法

  • Promise.reject
  • Promise.resolve
  • catch
  • Promise.all
  • Promise.race
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason);
    })
}

Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{
        resolve(value);
    })
}

Promise.prototype.catch = function(onRejected){
    return this.then(null,onRejected);
};

Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0;
        function processData(index,data){
            arr[index] = data;
            if(++i == promises.length){
                resolve(arr);
            }
        }
        for(let i = 0;i<promises.length;i++){
            promises[i].then(data=>{
                processData(i,data);
            },reject);
        }
    })
}

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.reject
// 原生的Promise.resolve使用
Promise.resolve('hello swr').then((data)=>{ // 直接把成功的值傳遞給下一個then
    console.log(data) // hello swr
})

// 那麼Promise.resolve內部是怎麼實現的呢?
Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{ // 在內部new一個Promise物件
       resolve(value) 
    })
}

// 同理,Promise.reject內部也是類似實現的
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason)
    })
}
複製程式碼
catch是怎樣實現呢?
// 原生Promise的catch使用
Promise.reject('hello swr').catch((e)=>{
    console.log(e) // hello swr
})

// 上面這段程式碼相當於下面這段程式碼
Promise.reject('hello swr').then(null,(e)=>{ // then裡直接走了失敗的回撥
    console.log(e) // hello swr
})

// 內部實現
Promise.prototype.catch = function(onRejected){
    return this.then(null,onRejected) // 相當於then裡的成功回撥只傳個null
}


複製程式碼
Promise.all,這個方法非常重要,同時執行多個非同步,並且返回一個新的promise,成功的值是一個陣列,該陣列成員的順序是傳參給Promise.all的順序
// 原生Promise.all的使用
// 假設1.txt內容為hello 2.txt內容為swr
let fs = require('fs')
function read(filePath,encoding){
    return new Promise((resolve,reject)=>{ 
        fs.readFile(filePath,encoding,(err,data)=>{
            if(err) reject(err)
            resolve(data)
        })
    })
}

Promise.all([read('./1.txt','utf8'),read('./2.txt','utf8')]).then((data)=>{
    console.log(data) // 全部讀取成功後返回 ['hello','swr']
                      // 需要注意的是,當其中某個失敗的話,則會走失敗的回撥函式
})

// 內部實現
Promise.all = function(promises){ // promises 是一個陣列
    return new Promise((resolve,reject)=>{
        let arr = []
        let i = 0
        function processData(index,data){
            arr[index] = data
            // 5.我們能用arr.length === promises.length來判斷請求是否全部完成嗎?
            // 答案是不行的,假設arr[2] = 'hello swr'
            // 那麼列印這個arr,將是[empty × 2, "hello swr"],
            // 此時陣列長度也是為3,而陣列arr[0] arr[1]則為空
            // 那麼換成以下的辦法
            if(++i === promises.length){ // 6.利用i自增來判斷是否都成功執行
                resolve(arr) // 此時arr 為['hello','swr']
            }
        }
        
        for(let i = 0;i < promises.length;i++){ // 1.在此處遍歷執行
            promises[i].then((data)=>{ // 2.data是成功後返回的結果
                processData(i,data) // 4.因為Promise.all最終返回的是一個陣列成員按照順序排序的陣列
                                    // 而且非同步執行,返回並不一定按照順序
                                    // 所以需要傳當前的i
            },reject) // 3.如果其中有一個失敗的話,則呼叫reject
        }
    })
}
複製程式碼
Promise.race 該方法是同時執行多個非同步,然後哪個快,就用哪個的結果,race的意思是賽跑
// 原生Promise.race的使用
// 一個成功就走成功的回撥,一個失敗就走失敗的回撥
Promise.race([read('./1.txt','utf8'),read('./2.txt','utf8')]).then((data)=>{
    console.log(data) // 可能返回 'hello' 也可能返回 'swr' 看哪個返回快就用哪個作為結果
})

// 內部實現
Promise.race = function(promises){ // promises 是一個陣列
    return new Promise((resolve,reject)=>{
        for(let i = 0;i < promises.length;i++){ 
            promises[i].then(resolve,reject) // 和上面Promise.all有點類似
        }
    })
}
複製程式碼

Promise.defer = Promise.deferred 這個語法糖怎麼理解呢?

這個語法糖可以簡化一些操作,比如

let fs = require('fs')
// 寫法一:
function read(filePath,encoding){
    // 這裡的new Promise依然是傳遞了一個executor回撥函式
    // 我們該怎樣減少回撥函式巢狀呢?
    return new Promise((resolve,reject)=>{ 
        fs.readFile(filePath,encoding,(err,data)=>{
            if(err) reject(err)
            resolve(data)
        })
    })
}

// 寫法二:
// 這樣的寫法減少了一層回撥函式的巢狀
function read(filePath,encoding){
    let dfd = Promise.defer()
    fs.readFile(filePath,encoding,(err,data)=>{
        if(err) dfd.reject(err)
        dfd.resolve(data)
    })
    return dfd.promise
}

read('./1.txt','utf8').then((data)=>{
    console.log(data)
})


複製程式碼

結尾:第一次寫,都是想到哪寫到哪,請大家多多諒解~ 也希望對大家有所幫助,promise的原始碼實現,我最大的收穫並不是怎麼實現promise,而是程式設計思維,大家可以多多往深裡想一想,也希望大家可以和我進行交流,共同進步

相關文章