摘要
本文介紹了基於 XMLHttpRequest、Promise、async/await 等三種非同步網路請求的寫法,其中async/await 寫法允許我們以類似於同步的方式編寫非同步程式,擺脫繁瑣的回撥函式。
一、背景
為了應對越來越多的測試需求,減少重複性的工作,有道智慧硬體測試組基於 electron 開發了一系列測試提效工具。
electron 的程式語言是js,因為大家都不是專業的前端,對js不太熟悉,在編寫程式時踩了不少坑。尤其是js中的事件和網路請求,這些涉及到非同步程式設計的地方很容易出錯。
隨著工具的快速開發迭代,程式碼中出現了越來越多的巢狀的回撥函式,工具崩潰的機率也越來越大。為了解決這些問題,我們用 async/await 對這些回撥函式進行了重構,使得程式碼量下降,程式碼的可讀性和可理解性都有了大幅度提高。
本文介紹了基於 XMLHttpRequest、Promise、async/await 等三種非同步網路請求的寫法,其中 async/await 寫法允許我們以類似於同步的方式編寫非同步程式,擺脫繁瑣的回撥函式。
二、前言
在js中如果只是發起單個網路請求還不算複雜,用fetch、axios或者直接用XMLHttpRequest就能滿足要求。
但若是多個請求按順序拉取資料那寫起來就很麻煩了?,因為js中的網路請求都是非同步的,想要順序執行最常見寫法就是在回撥函式中發起下一個請求,如下面這些程式碼:
const requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch('https://xxx.yyy.com/api/zzz/', requestOptions)
.then(response => response.json())
.then(data => {
fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
.then(response => response.json())
.then(data => {
console.log(data)
})
.catch(error => console.error('error', error));
})
.catch(error => console.error('error', error));
假設我需要經過兩步獲取一個資料,如從https://xxx.yyy.com/api/zzz/獲取一個資料物件data,通過data.id得到我要獲取資料的序號,之後再發一次請求得到想要的資料。
用回撥函式的方式就類似於上面這樣,太繁瑣了,而且容易出錯,並且一旦邏輯複雜就不好改啦。
接下來梳理一下js的幾種網路請求方式,擺脫回撥地獄,希望對遇到類似問題的小夥伴有所幫助。
(一)XMLHttpRequest
首先是XMLHttpRequest,初學前端時大名鼎鼎的Ajax主要指的就是它。通過XMLHttpRequest物件建立網路請求的套路如下:
// 假設訪問http://localhost:3000/user返回json物件{"name":"YouDao"}
const xhr = new XMLHttpRequest();
const url = 'http://localhost:3000/user'
xhr.onreadystatechange = function(){
if (this.readyState == 4 && this.status == 200){
const json=JSON.parse(xhr.responseText)
const name=json.name
console.log(name)
}
}
xhr.open('GET',url)
xhr.send()
這段程式碼首先建立一個XMLHttpRequest物件xhr,然後給xhr.onreadystatechange新增readystatechange事件的回撥函式,之後xhr.open('GET',url)初始化請求,最後由xhr.send()傳送請求。
請求傳送後,程式會繼續執行不會阻塞,這也是非同步呼叫的好處。當瀏覽器收到響應時就會進入xhr.onreadystatechange的回撥函式中去。在整個請求過程中,xhr.onreadystatechange會觸發四次,每次readyState都會自增,從1一直到4,只有到了最後階段也就是readyState為4時才能得到最終的響應資料。到達第四階段後還要根據status判斷響應的狀態碼是否正常,通常響應碼為200說明請求沒有遇到問題。這段程式碼最終會在控制檯上會打出YouDao。
可以看出,通過XMLHttpRequest處理請求的話,首先要針對每個請求建立一個XMLHttpRequest物件,然後還要對每個物件繫結readystatechange事件的回撥函式,若是多個請求串起來,想想就很麻煩。
(二)Promise
Promise是在 ECMAScript 2015 引入的,如果一個事件依賴於另一個事件返回的結果,那麼使用回撥會使程式碼變得很複雜。Promise物件提供了檢查操作失敗或成功的一種模式。如果成功,則會返回另一個Promise。這使得回撥的書寫更加規範。
通過Promise處理的套路如下:
const promise = new Promise((resolve,reject)=>{
let condition = true;
if (condition) {
resolve("ok")
} else {
reject("failed")
}
}).then( msg => console.log(msg))
.catch( err => console.error(err))
.finally( _ =>console.log("finally"))
上面這段程式碼把整個處理過程串起來了,首先建立一個Promise物件,它的構造器接收一個函式,函式的第一個引數是沒出錯時要執行的函式resolve,第二個引數是出錯後要執行的函式reject。
resolve指執行成功後then裡面的回撥函式,reject指執行失敗後catch裡執行的回撥函式。最後的finally是不論成功失敗都會執行的,可以用來做一些收尾清理工作。
基於Promise的網路請求可以用axios庫或瀏覽器自帶的fetch實現。
axios庫建立請求的套路如下:
import axios from 'axios'
const url = 'http://xxx.yyy.com/'
axios.get(url)
.then(data => console.log(data))
.catch(err => console.error(err))
我比較喜歡用fetch,fetch是用來代替XMLHttpRequest的瀏覽器API,它不需要導庫,fetch建立請求的方式和axios類似,在開頭已經展示過了就不重複寫了。
雖然Promise把回撥函式的編寫方式簡化了一些,但還是沒有擺脫回撥地獄,多個請求串起來的話就會像我開頭寫的那樣,在then裡面建立新的Promise,最終變成Promise地獄。
(三)async/await
async/await是在 ECMAScript 2017 引入的,可以簡化Promise的寫法,使得程式碼中的非同步函式呼叫可以按順序執行,易於理解。
下面就用開頭的那個例子說明吧:
直接用fetch獲取資料:
const requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch('https://xxx.yyy.com/api/zzz/', requestOptions)
.then(response => response.json())
.then(data => {
fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
.then(response => response.json())
.then(data => {
console.log(data)
})
.catch(error => console.error('error', error));
})
.catch(error => console.error('error', error));
用async/await改寫後:
async function demo() {
const requestOptions = {
method: 'GET',
redirect: 'follow'
};
const response = await fetch('https://xxx.yyy.com/api/zzz/', requestOptions);
const data = await response.json()
const response1 = await fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
const data1 = await response1.json()
console.log(data1)
}
demo().catch(error => console.error('error',error))
改寫後的程式碼是不是就很清楚了,沒有那麼多的then跟在後面了,這樣如果有一連串的網路請求也不用怕了。
當async放在一個函式的宣告前時,這個函式就是一個非同步函式,呼叫該函式會返回一個Promise。
await用於等待一個Promise物件,它只能在非同步函式中使用,await表示式會暫停當前非同步函式的執行,等待 Promise 處理完成。
這樣如果想讓一連串的非同步函式呼叫順序執行,只要把被呼叫的這些函式放到一個用async修飾的函式中,呼叫前加上await就能讓這些函式乖乖地順序執行了。
結語
通過本文的梳理,相信你已經知道怎樣避免回撥地獄了。不過需要注意的是 Promise 是2015年加入語言規範的,而 async/await 是2017年才加入到語言規範的,如果你的專案比較老或者是必須要相容老版本的瀏覽器(如IE6?),那就需要用別的方式來解決回撥地獄了。
對於 electron 只要你用的是近幾年的版本都是支援的,electron 可以當成是 chromium 和 node.js 的結合體,特別適合用來寫跨平臺的工具類桌面應用程式。