關於es6--promise

看了很多關於promise的文章,此篇文章做以總結。
由於Javascript是一種單線程的語言,所有的代碼必須按照所謂的“自上而下”的順序來執行。本特性帶來的問題就是,一些將來的、未知的操作,必須異步實現。promise就是一個比較常見的異步解決方案。

Promise有以下兩個特點:
(1)狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。
(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對Promise對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
有了Promise對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加容易。

首先,看一個例子,new一個promise

var p = new Promise(function(resolve, reject){
  //做一些異步操作
  setTimeout(function(){
    console.log('執行完成');
    resolve('成功的數據');
  }, 2000);
});

Promise的構造函數接收一個參數,是函數,並且傳入兩個參數:resolve,reject,分別表示異步操作執行成功後的回調函數和異步操作執行失敗後的回調函數,按照標準來講,resolve是將Promise的狀態置爲fullfiled,reject是將Promise的狀態置爲rejected。

在上面的代碼中,我們執行了一個異步操作,也就是setTimeout,2秒後,輸出“執行完成”,並且調用resolve方法。

運行代碼,會在2秒後輸出“執行完成”。注意!我只是new了一個對象,並沒有調用它,我們傳進去的函數就已經執行了,這是需要注意的一個細節。所以我們用Promise的時候一般是包在一個函數中,在需要的時候去運行這個函數,如:

function runAsync(){
  var p = new Promise(function(resolve, reject){
    //做一些異步操作
    setTimeout(function(){
        console.log('執行完成');
        resolve('隨便什麼數據');
    }, 2000);
  });
  return p;            
}
runAsync()

在上面的代碼中,我們包裝好的函數最後會return一個promise對象,也就是說promise是一個對象,接下來就是promise的一些方法了:then/ catch /all/ race等
首先了解一下沒有promise的回調地獄

setTimeout(function(){
  left(function(){
    setTimeout(function(){
      left(function(){
        setTimeout(function(){
          left();
        },2000);
      });
    }, 2000);
  });
}, 2000);

以上代碼就是傳說中的回調地獄,如果有多層業務邏輯嵌套的話,不僅會使代碼閱讀困難,而且後面維護起來也是難點。
之後在ES6,Promise就應運而生。
用then來進行回調操作(鏈式操作)

function runAsync1(){
  var p = new Promise(function(resolve, reject){
    //做一些異步操作
    setTimeout(function(){
      console.log('異步任務1執行完成');
      resolve('隨便什麼數據1');
    }, 1000);
  });
  return p;            
}
function runAsync2(){
  var p = new Promise(function(resolve, reject){
    //做一些異步操作
    setTimeout(function(){
      console.log('異步任務2執行完成');
      resolve('隨便什麼數據2');
    }, 2000);
  });
  return p;           
  }
function runAsync3(){
  var p = new Promise(function(resolve, reject){
    //做一些異步操作
    setTimeout(function(){
      console.log('異步任務3執行完成');
      resolve('隨便什麼數據3');
    }, 2000);
  });
  return p;            
}
runAsync1()
.then(function(data){
  console.log(data);
  return runAsync2();
})
.then(function(data){
  console.log(data);
  return runAsync3();
})
.then(function(data){
  console.log(data);
});

這樣能夠按順序,每隔兩秒輸出每個異步回調中的內容,在runAsync2中傳給resolve的數據,能在接下來的then方法中拿到。運行結果

clipboard.png
在then方法中,你也可以直接return數據而不是Promise對象,在後面的then中就可以接收到數據了,比如我們把上面的代碼修改成這樣:

runAsync1()
.then(function(data){
  console.log(data);
  return runAsync2();
})
.then(function(data){
  console.log(data);
  return '直接返回數據';  //這裏直接返回數據
})
.then(function(data){
  console.log(data);
});

結果如下:

clipboard.png

接下來說一下reject的用法:
reject的作用就是把Promise的狀態置爲rejected,這樣我們在then中就能捕捉到,然後執行“失敗”情況的回調。看下面的代碼。

function getNumber(){
  var p = new Promise(function(resolve, reject){
    //做一些異步操作
    setTimeout(function(){
      var num = Math.ceil(Math.random()*10); //生成1-10的隨機數
      if(num<=5){
        resolve(num);
      }
      else{
        reject('數字太大了');
      }
    }, 2000);
  });
  return p;            
}

getNumber()
.then(
    function(data){
      console.log('resolved');
      console.log(data);
    }, 
    function(reason, data){
      console.log('rejected');
      console.log(reason);
    }
);

getNumber函數用來異步獲取一個數字,2秒後執行完成,如果數字小於等於5,我們認爲是“成功”了,調用resolve修改Promise的狀態。否則我們認爲是“失敗”了,調用reject並傳遞一個參數,作爲失敗的原因。

運行getNumber並且在then中傳了兩個參數,then方法可以接受兩個參數,第一個對應resolve的回調,第二個對應reject的回調。所以我們能夠分別拿到他們傳過來的數據。多次運行這段代碼,你會隨機得到下面兩種結果:

clipboard.png
注意,Promise.reject()方法的參數,會原封不動地作爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。
catch方法
catch的用法和then的第二個參數一樣,用來指定reject的回調

getNumber()
.then(function(data){
  console.log('resolved');
  console.log(data);
})
.catch(function(reason){
  console.log('rejected');
  console.log(reason);
});

效果和寫在then的第二個參數裏面一樣。不過它還有另外一個作用:在執行resolve的回調(也就是上面then中的第一個參數)時,如果拋出異常了(代碼出錯了),那麼並不會報錯卡死js,而是會進到這個catch方法中。請看下面的代碼:

getNumber()
.then(function(data){
  console.log('resolved');
  console.log(data);
  console.log(somedata); //此處的somedata未定義
})
.catch(function(reason){
  console.log('rejected');
  console.log(reason);
});

在resolve的回調中,我們console.log(somedata);而somedata這個變量是沒有被定義的。如果我們不用Promise,代碼運行到這裏就直接在控制檯報錯了,不往下運行了。但是在這裏,會得到這樣的結果:

clipboard.png
關於catch
1.catch是用於指定發生錯誤時的回調函數。(建議不要在then的第二個參數寫rejected狀態,總是使用catch)
2.catch()使回調報錯時不會卡死js而是會繼續往下執行。
3.Promise 對象的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤總是會被下一個catch語句捕獲。
這裏要注意,不管是then或者catch返回的都是一個新的Promise實例!而每個Primise實例都有最原始的Pending(進行中)到Resolve(已完成),或者Pending(進行中)到Reject(已失敗)的過程。

all的用法
Promise的all方法提供了並行執行異步操作的能力,並且在所有異步操作執行完後才執行回調。我們仍舊使用上面定義好的runAsync1、runAsync2、runAsync3這三個函數,看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
  console.log(results);
});

用Promise.all來執行,all接收一個數組參數,裏面的值最終都算返回Promise對象。這樣,三個異步操作的並行執行的,等到它們都執行完後纔會進到then裏面。那麼,三個異步操作返回的數據哪裏去了呢?都在then裏面呢,all會把所有異步操作的結果放進一個數組中傳給then,就是上面的results。所以上面代碼的輸出結果就是:「誰跑的慢,以誰爲準執行回調」

clipboard.png
有了all,你就可以並行執行多個異步操作,並且在一個回調中處理所有的返回數據,有一個場景是很適合用這個的,一些遊戲類的素材比較多的應用,打開網頁時,預先加載需要用到的各種資源如圖片、flash以及各種靜態文件。所有的都加載完後,我們再進行頁面的初始化。(個人感覺很想&&符號鏈接)
race的用法
all方法的效果實際上是「誰跑的慢,以誰爲準執行回調」,那麼相對的就有另一個方法「誰跑的快,以誰爲準執行回調」,這就是race方法,這個詞本來就是賽跑的意思。race的用法與all一樣,我們把上面runAsync1的延時改爲1秒來看一下:

Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
  console.log(results);
});

這三個異步操作同樣是並行執行的。結果你應該可以猜到,1秒後runAsync1已經執行完了,此時then裏面的就執行了。結果是這樣的:

clipboard.png
在then裏面的回調開始執行時,runAsync2()和runAsync3()並沒有停止,仍舊再執行。於是再過1秒後,輸出了他們結束的標誌,resolve/reject一旦輸出不會改變。(感覺就是||符號操作~~~)

還有一些關於promise的面試題。感興趣的可以看一下:https://mp.weixin.qq.com/s/Wv...
以上感謝:王漢炎 枸杞辣條 IT平頭哥聯盟

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