小邵教你玩轉promise源碼

NO IMAGE

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

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

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

Author: 邵威儒
Email: [email protected]
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的執行結果會傳遞給下一次then中
return 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,而是編程思維,大家可以多多往深裡想一想,也希望大家可以和我進行交流,共同進步

相關文章

小邵教你玩轉nodejs之nodejs概念、事件環機制(1)

小邵教你玩轉ES6

小邵教你玩轉JS面向對象

小邵教你玩轉Generator+co/asyncawait