1. 程式人生 > >「前端進階」完全吃透Promise,深入JavaScript非同步

「前端進階」完全吃透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