非同步解決方案----Promise與Await

浪裡行舟發表於2018-06-14

前言

非同步程式設計模式在前端開發過程中,顯得越來越重要。從最開始的XHR到封裝後的Ajax都在試圖解決非同步程式設計過程中的問題。隨著ES6新標準的到來,處理非同步資料流又有了新的方案。我們都知道,在傳統的ajax請求中,當非同步請求之間的資料存在依賴關係的時候,就可能產生很難看的多層回撥,俗稱'回撥地獄'(callback hell),這卻讓人望而生畏,Promise的出現讓我們告別回撥函式,寫出更優雅的非同步程式碼。在實踐過程中,卻發現Promise並不完美,Async/Await是近年來JavaScript新增的最革命性的的特性之一,Async/Await提供了一種使得非同步程式碼看起來像同步程式碼的替代方法。接下來我們介紹這兩種處理非同步程式設計的方案。

一、Promise的原理與基本語法

1.Promise的原理

Promise 是一種對非同步操作的封裝,可以通過獨立的介面新增在非同步操作執行成功、失敗時執行的方法。主流的規範是 Promises/A+。

Promise中有幾個狀態

  • pending: 初始狀態, 非 fulfilled 或 rejected;

  • fulfilled: 成功的操作,為表述方便,fulfilled 使用 resolved 代替;

  • rejected: 失敗的操作。

非同步解決方案----Promise與Await
pending可以轉化為fulfilled或rejected並且只能轉化一次,也就是說如果pending轉化到fulfilled狀態,那麼就不能再轉化到rejected。並且fulfilled和rejected狀態只能由pending轉化而來,兩者之間不能互相轉換。

2.Promise的基本語法

  • Promise例項必須實現then這個方法

  • then()必須可以接收兩個函式作為引數

  • then()返回的必須是一個Promise例項

<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//如果低版本瀏覽器不支援Promise,通過cdn這種方式
      <script type="text/javascript">
        function loadImg(src) {
            var promise = new Promise(function (resolve, reject) {
                var img = document.createElement('img')
                img.onload = function () {
                    resolve(img)
                }
                img.onerror = function () {
                    reject('圖片載入失敗')
                }
                img.src = src
            })
            return promise
        }
        var src = 'https://www.imooc.com/static/img/index/logo_new.png'
        var result = loadImg(src)
        result.then(function (img) {
            console.log(1, img.width)
            return img
        }, function () {
            console.log('error 1')
        }).then(function (img) {
            console.log(2, img.height)
        })
     </script>
複製程式碼

二、Promise多個串聯操作

Promise還可以做更多的事情,比如,有若干個非同步任務,需要先做任務1,如果成功後再做任務2,任何任務失敗則不再繼續並執行錯誤處理函式。要序列執行這樣的非同步任務,不用Promise需要寫一層一層的巢狀程式碼。

有了Promise,我們只需要簡單地寫job1.then(job2).then(job3).catch(handleError); 其中job1、job2和job3都是Promise物件。

比如我們想實現第一個圖片載入完成後,再載入第二個圖片,如果其中有一個執行失敗,就執行錯誤函式:

       var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
        var result1 = loadImg(src1) //result1是Promise物件
        var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
        var result2 = loadImg(src2) //result2是Promise物件
        result1.then(function (img1) {
            console.log('第一個圖片載入完成', img1.width)
            return result2  // 鏈式操作
        }).then(function (img2) {
            console.log('第二個圖片載入完成', img2.width)
        }).catch(function (ex) {
            console.log(ex)
        })
複製程式碼

這裡需注意的是:then 方法可以被同一個 promise 呼叫多次,then 方法必須返回一個 promise 物件。上例中result1.then如果沒有明文返回Promise例項,就預設為本身Promise例項即result1,result1.then返回了result2例項,後面再執行.then實際上執行的是result2.then

三、Promise常用方法

除了序列執行若干非同步任務外,Promise還可以並行執行非同步任務

試想一個頁面聊天系統,我們需要從兩個不同的URL分別獲得使用者的個人資訊和好友列表,這兩個任務是可以並行執行的,用Promise.all()實現如下:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 獲得一個Array: ['P1', 'P2']
});
複製程式碼

有些時候,多個非同步任務是為了容錯。比如,同時向兩個URL讀取使用者的個人資訊,只需要獲得先返回的結果即可。這種情況下,用Promise.race()實現:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});
複製程式碼

由於p1執行較快,Promise的then()將獲得結果'P1'。p2仍在繼續執行,但執行結果將被丟棄。

總結:Promise.all接受一個promise物件的陣列,待全部完成之後,統一執行success;

Promise.race接受一個包含多個promise物件的陣列,只要有一個完成,就執行success

接下來我們對上面的例子做下修改,加深對這兩者的理解:

     var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
     var result1 = loadImg(src1)
     var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
     var result2 = loadImg(src2)
     Promise.all([result1, result2]).then(function (datas) {
         console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
         console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
     })
     Promise.race([result1, result2]).then(function (data) {
         console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
     })
複製程式碼

如果我們組合使用Promise,就可以把很多非同步任務以並行和序列的方式組合起來執行

四、Async/Await簡介與用法

非同步操作是 JavaScript 程式設計的麻煩事,很多人認為async函式是非同步操作的終極解決方案。

1、Async/Await簡介

  • async/await是寫非同步程式碼的新方式,優於回撥函式和Promise。

  • async/await是基於Promise實現的,它不能用於普通的回撥函式。

  • async/await與Promise一樣,是非阻塞的。

  • async/await使得非同步程式碼看起來像同步程式碼,再也沒有回撥函式。但是改變不了JS單執行緒、非同步的本質。

2、Async/Await的用法

  • 使用await,函式必須用async標識

  • await後面跟的是一個Promise例項

  • 需要安裝babel-polyfill,安裝後記得引入 //npm i --save-dev babel-polyfill

   function loadImg(src) {
            const promise = new Promise(function (resolve, reject) {
                const img = document.createElement('img')
                img.onload = function () {
                    resolve(img)
                }
                img.onerror = function () {
                    reject('圖片載入失敗')
                }
                img.src = src
            })
            return promise
        }
     const src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
     const src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
     const load = async function(){
        const result1 = await loadImg(src1)
        console.log(result1)
        const result2 = await loadImg(src2)
        console.log(result2) 
     }
     load()
複製程式碼

當函式執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再接著執行函式體內後面的語句。

五、Async/Await錯誤處理

await 命令後面的 Promise 物件,執行結果可能是 rejected,所以最好把 await 命令放在 try...catch 程式碼塊中。try..catch錯誤處理也比較符合我們平常編寫同步程式碼時候處理的邏輯

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}
複製程式碼

六、為什麼Async/Await更好?

Async/Await較Promise有諸多好處,以下介紹其中三種優勢:

1. 簡潔

使用Async/Await明顯節約了不少程式碼。我們不需要寫.then,不需要寫匿名函式處理Promise的resolve值,也不需要定義多餘的data變數,還避免了巢狀程式碼。

2. 中間值

你很可能遇到過這樣的場景,呼叫promise1,使用promise1返回的結果去呼叫promise2,然後使用兩者的結果去呼叫promise3。你的程式碼很可能是這樣的:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1)
        .then(value2 => {        
          return promise3(value1, value2)
        })
    })
}
複製程式碼

使用async/await的話,程式碼會變得異常簡單和直觀

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}
複製程式碼

3.條件語句

下面示例中,需要獲取資料,然後根據返回資料決定是直接返回,還是繼續獲取更多的資料。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}
複製程式碼

程式碼巢狀(6層)可讀性較差,它們傳達的意思只是需要將最終結果傳遞到最外層的Promise。使用async/await編寫可以大大地提高可讀性:

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}
複製程式碼

參考文章

Async/Await替代Promise的6個理由

前端的非同步解決方案之Promise和Await/Async

廖雪峰的Javascript教程

[譯] Promises/A+ 規範

async 函式的含義和用法

相關文章