JavaScript非同步處理的那些事兒

正偉_發表於2018-11-12

前言

原文

之前總結了關於 JavaScript 非同步的 事件迴圈與訊息佇列 機制以及 ES6 帶來的 微任務與巨集任務 的知識。傳送門

下面是關於JS非同步處理的各種方案:

callback >> ES6 Primise >> async/await
複製程式碼

沒有非同步處理

先看一段程式碼:

// 假設有一個耗時的非同步請求 ajax,在 2 秒後列印日誌

function ajax () {
  // do something...
  setTimeout(() => {
    console.log('Hello, Zavier Tang!')
  }, 2000)
}
ajax()
// do something...
console.log('The end.')

// The end.
// Hello, Zavier Tang!
複製程式碼

這裡模擬了一個簡單的非同步網路請求,並在 2 秒後列印 "Hello, Zavier Tang!",然後做一些處理("do something")後,列印結束 "The end."。

結果是先列印的 "The end."。

顯然這並不是我們要的結果,"非同步請求" ajax 中的 setTimeout 並沒有阻塞程式碼的執行,而是直接執行了 console.log()

非同步的解決方案

1. 傳統(可怕)的 callback 回撥地獄

同樣是上面的非同步網路請求,這裡使用 callback 回撥函式的方式解決 JavaScript 同步帶來的問題。

function ajax (fn) {
  // do something...
  setTimeout(() => {
    console.log('Hello, Zavier Tang!')
    fn()
  }, 2000)
}
ajax(() => {
  // do something...
  console.log('The end.')
})

// Hello, Zavier Tang!
// The end.
複製程式碼

這裡我們直接把非同步請求之後要做的一些操作做為回撥函式,傳遞到 ajax 中去,並在非同步請求結束後執行回撥函式裡的操作。

問題似乎已經解決了??但是,如果有多個非同步請求,並且在每個非同步請求完成後都執行一些操作,那程式碼就會像下面這樣:

function ajax (fn) {
  // do something...
  setTimeout(() => {
    console.log('Hello, Zavier Tang!')
    fn()
  }, 500)
}

ajax(() => {
  // do something...
  console.log('The end 1.')
  ajax(() => {
    // do something...
    console.log('The end 2.')
    ajax(() => {
      // do something...
      console.log('The end 3.')
      ajax(() => {
        // do something...
        console.log('The end 4.')
        ajax(() => {
          // do something...
          console.log('The end 5.')
          // ......
          // ......
        })
      })
    })
  })
})

// Hello, Zavier Tang!
// The end 1.
// Hello, Zavier Tang!
// The end 2.
// Hello, Zavier Tang!
// The end 3.
// Hello, Zavier Tang!
// The end 4.
// Hello, Zavier Tang!
// The end 5.
複製程式碼

看起來很嚇人!!

如果是這樣:請求1結束後進行請求2,然後還有請求3、請求4、請求5,並且請求2還可能依賴於請求1的資料,請求3依賴於請求2的資料,不停的一層一層巢狀,對於錯誤的處理也極不方便,所以稱之為 callback 回撥地獄。

2. 下一代非同步解決方案:Promise

ES6 的 Promise 被稱為 JS 非同步的下一代解決方案。

關於 Promise:

  • 用於非同步計算。
  • 可以將非同步操作佇列化,按照期望的順序執行,返回符合預期的結果。
  • 可以在物件之間傳遞和操作 Promise,方便處理佇列。

接下來使用 Promise 處理非同步:

function ajax (word) {
  return new Promise((resolve) => {
    // do something...
    setTimeout(() => {
      resolve('Hello ' + word)
    }, 500)
  })
}

ajax('請求1')
  .then((word) => {
    console.log(word)
    console.log('The end 1.')
    return ajax('請求2')
  })
  .then((word) => {
    console.log(word)
    console.log('The end 2.')
    return ajax('請求3')
  })
  .then((word) => {
    console.log(word)
    console.log('The end 3.')
  })
  // .catch(() => {})

// Hello 請求1
// The end 1.
// Hello 請求2
// The end 2.
// Hello 請求3
// The end 3.
複製程式碼

上面還是連續的非同步請求,每次請求後列印日誌。這樣看起來比上面的回撥地獄舒服多了,每個請求通過鏈式的呼叫,在最後可以對所有的請求進行錯誤處理。

但,似乎還並不是特別優雅。

3. 終極解決方案:async/await

關於 async/await 的簡介:

  • async/await 是寫非同步程式碼的新方式,不同於以前的 callback 回撥函式和 Promise。
  • async/await 是基於 Promise 實現的,不能用於普通的回撥函式。
  • async/await 與 Promise 一樣,是非阻塞的。
  • async/await 使得非同步程式碼看起來像同步程式碼。

體驗一下神奇的 async/await:

// 接上面的程式碼
async function doAsync () {
  const word1 = await ajax('請求1')
  console.log(word1)
  console.log('The end 1')

  const word2 = await ajax('請求2')
  console.log(word2)
  console.log('The end 2')

  const word3 = await ajax('請求3')
  console.log(word3)
  console.log('The end 3')
}
doAsync()

// Hello 請求1
// The end 1
// Hello 請求2
// The end 2
// Hello 請求3
// The end 3
複製程式碼

這樣看起來,更優雅了。沒有任何括號,也沒有 callback,沒有 then,直接申明 async,使用 await 等待非同步執行完成,看起來也更像是同步的程式碼。

總結

JavaScript 的非同步編寫方式,從 callback 回撥函式到 Promise ,再到 async/await,只能說。。。

技術發展太快啦,趕緊學習吧!

相關文章