「前端進階」完全喫透Promise,深入JavaScript異步

完全喫透Promise

Promise晉級,需要的全部都在這

主要內容:

  1. promise基本實現原理
  2. promise 使用中難點(鏈式調用,API基本上返回都是一個新Promise,及參數傳遞)
  3. promise 對異常處理
  4. promise 簡單實現及規範

參考:


牛刀小試

對於現在的前端同學來說你不懂promise你都不好意思出門了。對於前端同學來說promise已經成爲了我們的必備技能。

那麼,下面我們就來說一說promise是什麼,它能幫助我們解決什麼問題,我們應該如何使用它?

這是我個人對promise的理解。歡迎吐槽 :)

Promise是什麼

promise的意思是承諾,有的人翻譯爲許願,但它們代表的都是未實現的東西,等待我們接下來去實現。

Promise最早出現在commnjs,隨後形成了Promise/A規範。在Promise這個技術中它本身代表以目前還不能使用的對象,但可以在將來的某個時間點被調用。使用Promise我們可以用同步的方式寫異步代碼。其實Promise在實際的應用中往往起到代理的作用。例如,我們像我們發出請求調用服務器數據,由於網絡延時原因,我們此時無法調用到數據,我們可以接着執行其它任務,等到將來某個時間節點服務器響應數據到達客戶端,我們即可使用promise自帶的一個回調函數來處理數據。

Promise能幫我們解決什麼痛點

JavaScript實現異步執行,在Promise未出現前,我們通常是使用嵌套的回調函數來解決的。但是使用回調函數來解決異步問題,簡單還好說,但是如果問題比較複雜,我們將會面臨回調金字塔的問題(pyramid of Doom)。

var a = function() {
    console.log('a');
};

var b = function() {
    console.log('b');
};

var c = function() {
    for(var i=0;i<100;i++){
        console.log('c')
    }
};

a(b(c())); // 100個c -> b -> a

我們要桉順序的執行a,b,c三個函數,我們發現嵌套回調函數確實可以實現異步操作(在c函數中循環100次,發現確實是先輸出100個c,然後在輸出b,最後是a)。但是你發現沒這種實現可讀性極差,如果是幾十上百且回調函數異常複雜,那麼代碼維護起來將更加麻煩。

那麼,接下來我們看一下使用promise(promise的實例可以傳入兩個參數表示兩個狀態的回調函數,第一個是resolve,必選參數;第二個是reject,可選參數)的方便之處。

var promise = new Promise(function(resolve, reject){
    console.log('............');
    resolve(); // 這是promise的一個機制,只有promise實例的狀態變爲resolved,纔會會觸發then回調函數
});

promise.then(function(){
    for(var i=0;i<100;i++) {
        console.log('c')
    }
})
.then(function(){
    console.log('b')
})
.then(function(){
    console.log('a')
})

那麼,爲什麼嵌套的回調函數這種JavaScript自帶實現異步機制不招人喜歡呢,因爲它的可讀性差,可維護性差;另一方面就是我們熟悉了jQuery的鏈式調用。所以,相比起來我們會更喜歡Promise的風格。

promise的3種狀態

上面提到了promise的 resolved 狀態,那麼,我們就來說一下promise的3種狀態,未完成(unfulfilled)、完成(fulfilled)、失敗(failed)。

在promise中我們使用resolved代表fulfilled,使用rejected表示fail。

ES6的Promise有哪些特性

  1. promise的狀態只能從 未完成->完成, 未完成->失敗 且狀態不可逆轉。

  2. promise的異步結果,只能在完成狀態時才能返回,而且我們在開發中是根據結果來選擇來選擇狀態的,然後根據狀態來選擇是否執行then()。

  3. 實例化的Promise內部會立即執行,then方法中的異步回調函數會在腳本中所有同步任務完成時纔會執行。因此,promise的異步回調結果最後輸出。示例代碼如下:

var promise = new Promise(function(resolve, reject) {
  console.log('Promise instance');
  resolve();
});

promise.then(function() {
  console.log('resolved result');
});
for(var i=0;i<100;i++) {
console.log(i);
/*
Promise instance
1
2
3
...
99
100
resolved result
*/

上面的代碼執行輸出結果的先後順序,曾經有人拿到這樣一個面試題問過我,所以,這個問題還是要注意的。

resolve中可以接受另一個promise實例

resolve中接受另一個另一個對象的實例後,resolve本實例的返回狀態將會有被傳入的promise的返回狀態來取代。

reject狀態替換實例,代碼如下:

const p1 = new Promise(function (resolve, reject) {
    cosole.log('2秒之後,調用返回p1的reject給p2');
    setTimeout(reject, 3000, new Error('fail'))
})

const p2 = new Promise(function (resolve, reject) {
    cosole.log('1秒之後,調用p1');
    setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))

// fail

resolve狀態替換實例,代碼如下:

const p1 = new Promise(function (resolve, reject) {
    cosole.log('2秒之後,調用返回p1的resolve給p2');
    setTimeout(resolve, 3000, 'success')
})

const p2 = new Promise(function (resolve, reject) {
    cosole.log('1秒之後,調用p1');
    setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))

// success

注意:promise實例內部的resolve也執行的是異步回調,所以不管resolve放的位置靠前還是靠後,都要等內部的同步函數執行完畢,纔會執行resolve異步回調。

new Promise((resolve, reject) => {
    console.log(1);
    resolve(2);
    console.log(3);
}).then(result => {
    console.log(result);
});
/*
1
3
2
*/

簡單的介紹結束了,接下來開始來點乾貨,正式擼代碼了。

1. 基本用法

首先看完上面的內容,我們應該瞭解基本的Promise使用了,那麼首先來了解下兼容性。

1. 兼容性

查看caniuse

查兼容性 基本上 主流瀏覽器支持沒有問題。

IE不兼容 問題,本文不予以處理,出門左轉,找谷哥。具體查看 babel,或者 自己實現一個Promise

2. ajax XMLHttpRequest封裝

//get 請求封裝
function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

2. Promse API

Promise API 分爲 :MDN

這裏不大段羅列API 只拿then來深入聊聊。(目錄結構是告訴分爲靜態方法及prototype上的方法,具體不同參考JavaScript原型鏈)

1.靜態方法

2.prototype上方法

  1. Promise.prototype.then() 來分析
首先來看看 `Promise.prototype.then()`返回一個`Promise`,但`Promise`內部有返回值,且 返回值,可以是個值,也可能就是一個新`Promise`

具體規則如下:

- *如果then中的回調函數返回一個值,那麼then返回的Promise將會成爲接受狀態,並且將返回的值作爲接受狀態的回調函數的參數值。*
- *如果then中的回調函數拋出一個錯誤,那麼then返回的Promise將會成爲拒絕狀態,並且將拋出的錯誤作爲拒絕狀態的回調函數的參數值。*
- *如果then中的回調函數返回一個已經是接受狀態的Promise,那麼then返回的Promise也會成爲接受狀態,並且將那個Promise的接受狀態的回調函數的參數值作爲該被返回的Promise的接受狀態回調函數的參數值。*
- *如果then中的回調函數返回一個已經是拒絕狀態的Promise,那麼then返回的Promise也會成爲拒絕狀態,並且將那個Promise的拒絕狀態的回調函數的參數值作爲該被返回的Promise的拒絕狀態回調函數的參數值。*
- *如果then中的回調函數返回一個未定狀態(pending)的Promise,那麼then返回Promise的狀態也是未定的,並且它的終態與那個Promise的終態相同;同時,它變爲終態時調用的回調函數參數與那個Promise變爲終態時的回調函數的參數是相同的。*

**上面是官方規則,神馬,具體白話就是 核心是 返回參數及返回promise的狀態**

參考:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#%E8%BF%94%E5%9B%9E%E5%80%BC)

是不是 覺得很暈,沒關係,可以先看 下一節,看完後,再回過來看具體的說明
/*then 回調中,
	1. 返回是return function,則返回一個Promise 【參見對比3代碼】
	2. 不是一個function,則 then 將創建一個沒有經過回調函數處理的新 Promise 對象,這個新 Promise 只是簡單地接受調用這個 then 的原 Promise 的終態作爲它的終態。(MDN中解釋)【參見對比1代碼】
	3. 返回一個function,但沒有return ,則相當於 then(null)
  */
//對比1 穿透問題  返回是'foo' 而不是 'bar'
Promise.resolve('foo')
    .then(Promise.resolve('bar'))
    .then(function(result){
    	console.log(result)
	})


//對比2  打印undefined
Promise.resolve('foo')
    .then(function(){Promise.resolve('bar')})
    .then(function(result){
        console.log(result)
    })


//對比3  返回 'bar'
Promise.resolve('foo')
    .then(function() {
        return Promise.resolve('bar')
    }).then(function(result) {
        console.log(result)
    })

3. Prmise 鏈式調用——重點(難點)

鏈式調用

  1.   核心就是 then catch 等方法返回一個Promise
  2.   鏈式 調用數據傳遞(注意)

1. 值傳遞問題

簡單例子

   //正常狀態
   const promise1 = new Promise((resolve, reject) => {
       resolve('0000')//
   })
   promise1.then(result => {
       console.log(result) //0000
   	   return '1111';//類似於 return Promise.resolve('1111'); 參數是data,promise 狀態時 resolve
   }).then(data => {
       console.log(data) // 1111
   })

一個實際的例子:(拿來大神的例子JavaScript Promise:簡介

//step 0
get('story.json').then(function(response) {
  console.log("Success!", response);
})
//step 1
//這裏的 response 是 JSON,但是我們當前收到的是其純文本。也可以設置XMLHttpRequest.responseType =json
get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})
//step 2
//由於 JSON.parse() 採用單一參數並返回改變的值,因此我們可以將其簡化爲:
get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})
//step 3
function getJSON(url) {
  return get(url).then(JSON.parse);
}
//getJSON() 仍返回一個 promise,該 promise 獲取 URL 後將 response 解析爲 JSON。

2. 異步操作隊列

上面至今是return 值,直接調用 下一下then就OK了。

但如果return Promise,則?

Promise.resolve(111).then(function(d){
	console.log(d);
	return Promise.resolve(d+111);//返回promise
}).then(function(d2){
	console.log(d2);
})
// 111,222

3. 鏈式調用異常處理

參見後文,異常處理。

4. 並行問題forEach處理

上面是多個鏈式調用,下面聊聊 並行處理

當多個異步並行執行時,每個異步代碼執行時間不定,所以多個異步執行結束時間無法確定(無法確定結束完時間)。

所以需要特殊處理。

//forEach 順便無法保證
var arrs = [1,2,3,4];
var p = function(d){
	return new Promise((resolve)=>{
       setTimeout(()=>{
			resolve(d);
		},Math.random()*1000);//因爲異步執行時間無法確認
    });
};
arrs.forEach(function(arr){
  p(arr).then((d)=>{
    console.log(d);
  })
});
//使用 Promise.all 來讓返回有序
var arrs = [1,2,3,4];
var p = function(d){
	return new Promise((resolve)=>{
       setTimeout(()=>{
			resolve(d);
		},Math.random()*1000);//因爲異步執行時間無法確認
    });
};
var ps = [];
arrs.forEach(function(arr){
  ps.push(p(arr));
});
Promise.all(ps).then(values=>{
  console.log(values);//[1,2,3,4]
})

5. 基本實現原理—實現一個簡單Promise

自己手擼一個簡單的Promise

1. 版本1—極簡實現

//版本1 極簡實現
function Promise1(fn) {
    var value = null,
        callbacks = [];  //callbacks爲數組,因爲可能同時有很多個回調

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;//支持鏈式調用 Promise.then().then
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}
//Test 對上面實現,寫一個簡單的測試
new Promise1(function(resolve){
    setTimeout(function(){
        resolve(1);
    },100);
}).then(function(d){
    console.log(d);
})
//1

2. 版本2—加入延時機制

//上面版本1 可能導致問題
//在then註冊回調之前,resolve就已經執行了
new Promise1(function(resolve){
    console.log(0)
	resolve(1);
}).then(function(d){
   console.log(d);
})
// 1 不會打印
//版本2 解決
function Promise1(fn) {
    var value = null,
        callbacks = [];  //callbacks爲數組,因爲可能同時有很多個回調

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;//支持鏈式調用 Promise.then().then
    };

    function resolve(value) {
       setTimeout(function(){
        callbacks.forEach(function (callback) {
            callback(value);
        }),0});
    }

    fn(resolve);
}

3. 版本3—狀態

Promise有三種狀態pendingfulfilledrejected ,且狀態變化時單向的。

具體細節就是 在then,resolve中加狀態判斷,具體代碼略

4. Promises/A+

具體 Promise實現有一套官方規範,具體參見Promises/A+

6. finnaly 實現

//版本一 finnaly 表示,不管resolve,reject 都執行
   Promise.prototype.finally = function (callback) {
     let P = this.constructor;
     return this.then(
       value  => P.resolve(callback()).then(() => value),
       reason => P.resolve(callback()).then(() => { throw reason })
     );
   };
//版本二
Promise.prototype.finally = function (callback) {
     return this.then(//這個 必須是this.then 而不是 Promise.prototype.then
       value  => Promise.resolve(callback()).then(() => value),
       reason => Promise.resolve(callback()).then(() => { throw reason })
     );
   };

版本一 版本二 兩種不同的寫法,各有利弊。具體參見 JavaScript原型鏈

//test
Promise.resolve(1).finally((d)=>{console.log(d)})
Promise.reject(1).finally((d)=>{console.log(d)})

6. 異常處理

異常分類:

  1. 同步異常
  2. 異步異常 無法try-catch 得到
  3. 多層Promise嵌套,獲異常取具體的一個promise異常,而不是全部

0. try-catch 無法捕獲異步異常

因爲異步的執行上下文 與try-catch 不是同一個,所以無法捕獲

//一個簡單例子
try{
  Promise.reject(2)
}catch(e){
 console.log(11111111)
}
//VM1279:2 Uncaught (in promise) 2

1. Promise 異常處理基本套路

基本處理異常中,有兩種方案then(undefined, func)catch()

then(undefined, func)catch()不同,具體參見代碼方案3

//方案1 使用 Promise.prototype.catch()來catch
const promise1 = new Promise((resolve, reject) => {
    reject('no')//
})
promise1.then(result => {
    console.log(result) // 永遠不會執行
}).catch(error => {
    console.log(error) // no
})
//方案2 使用 Promise.prototype.then()中第二個參數 來處理
const promise1 = new Promise((resolve, reject) => {
    reject('no')//
})
promise1.then(result => {
    console.log(result) // 永遠不會執行
},error => {
    console.log(error) // no
})
//方案2  (方案1  方案2 對比)
var promise2 = new Promise((resolve, reject) => {
    resolve('yes')//
})
promise2.then(result => {
    throw new Error('then');
    console.log(result)
},error => {
    console.log('1111',error) // no
}).catch(error=>{
   console.log('2222',error)// 最終 err在此處被捕獲,而不是 then 中
})

2. 異常不同分類

Promise可能遇到的異常種類

//1.異常 reject()
const promise1 = new Promise((resolve, reject) => {
    reject('no')//
})
promise1.then(result => {
    console.log(result) // 永遠不會執行
}).catch(error => {
    console.log(error) // no
})

//2.異常 顯示throw
const promise1 = new Promise((resolve, reject) => {
    throw Error('no')
})
promise1.then(result => {
    console.log(result) // 永遠不會執行
}).catch(error => {
    console.log(error) //
})
//3.執行異常
const promise1 = new Promise((resolve, reject) => {
    aaaa;
})
promise1.then(result => {
    console.log(result) // 永遠不會執行
}).catch(error => {
    console.log(error) //
})

3. 異常鏈式調用

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

上述代碼的流程圖形式:

流程圖形式

// promise鏈式調用,catch住異常後,後面就不會處理異常了
Promise.reject().then(()=>{
  console.log(2222);
},(err)=>{
	console.log(333,err)
	return err})
.catch((err)=>{
  console.log(1111,err);
})
//333 undefined  ,沒有打印 1111
//如果 在鏈式調用中,then 第二個參數 catch住了異常,沒有return Promise.reject()則後續鏈式調用返回rosolve狀態pormise
Promise.reject()
   .then(()=>{
      console.log(111);
    },(err)=>{
        console.log(111,err) //reject
        return err;
    }).then((data)=>{
        console.log(222,data)//resolve 執行
    },(err)=>{
      console.log(222,err); //未執行
    })
//4444 沒有執行 1111

4. 如何停止一個Promise鏈

//簡化一個模型,
new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!" ——出現錯誤後,沒有必要執行後續代碼
  })//但代碼,是無論return||throw,都會執行後續catch||then,async 可以解決
  .catch()
  .then()
  .then()
  .catch()
  .then()
//網上的一個解決方案,但後續回調都無法被GCC回收;
//其實本質是返回一個無狀態的Promise,讓其永遠處於pending狀態
new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
   return new Promise(function(){})
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

其實 Promise異常,麻煩再鏈式調用,異常處理位置真不好處理。

5. 異常丟失

很多情況下,promise無法捕獲異常

場景1 macrotask 隊列中拋出異常:

//場景1
//永遠不要在 macrotask 隊列中拋出異常,因爲 macrotask 隊列脫離了運行上下文環境,異常無法被當前作用域捕獲。
function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
             throw Error('用戶不存在')
        })
    })
}

fetch().then(result => {
    console.log('請求處理', result) // 永遠不會執行
}).catch(error => {
    console.log('請求處理異常', error) // 永遠不會執行
})

// 程序崩潰
// Uncaught Error: 用戶不存在

/*
    參考
    作者:黃子毅
    鏈接:https://www.jianshu.com/p/78dfb38ac3d7
    來源:簡書
    簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。
*/
//解決場景1 怎麼解決,因爲setTimeout 是macrotask任務,執行上下文完全不同
/**
	如何解決?
	調用reject
*/
function fetch() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('收斂一些')
        })
    })
}
fetch().then((resolve, reject) => {
    console.log('resolve');
}).catch(error => {
    console.log('捕獲異常', error) // 捕獲異常 收斂一些
})

場景二 Promise 狀態只能改變一次

 //異常丟失
   const promise2 = new Promise((resolve, reject) => {
       reject('no')
       console.log('reject after')
     throw Error('no') //異常丟失
   })
   promise1.then(result => {
       console.log(result) // 永遠不會執行
   }).catch(error => {
       console.log('err',error) // no
   }).catch(error => {
       console.log('err2',error) // 也無法捕獲異常
   })

7.async

async是 Promise 更高一層的封裝,具體參見「前端進階」完全喫透async/await,深入JavaScript異步

個人博客

更多前端技術文章美術設計wordpress插件、優化教程學習筆記盡在我的個人博客喵容 - 和你一起描繪生活,歡迎一起交流學習,一起進步:https://www.miaoroom.com

站內文章推薦:

「前端進階」史上最全的前端學習路線

「不要重複造輪子系列」 前端常用插件、工具類庫彙總

如何保障前端項目代碼質量

記錄一次基於vue、typescript、pwa的項目由開發到部署

小程序挖坑之路

原文鏈接:「前端進階」完全喫透Promise,深入JavaScript異步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章