js幾種網絡請求方式梳理——擺脱回調地獄

語言: CN / TW / HK

摘要

本文介紹了基於 XMLHttpRequest、Promise、async/await 等三種異步網絡請求的寫法,其中 async/await 寫法允許我們以類似於同步的方式編寫異步程序,擺脱繁瑣的回調函數。

youdao

背景

ydtech

為了應對越來越多的測試需求,減少重複性的工作,有道智能硬件測試組基於 electron 開發了一系列測試提效工具。

隨着工具的快速開發迭代,代碼中出現了越來越多的嵌套的回調函數,工具崩潰的機率也越來越大。為了解決這些問題, 我們用 async/await 對這些回調函數進行了重構, 使得代碼量下降,代碼的可讀性和可理解性都有了大幅度提高。

本文介紹了 基於 XMLHttpRequest、Promise、async/await 等三種異步網絡請求 的寫法,其中 async/await 寫法允許我們以類似於同步的方式編寫異步程序,擺脱繁瑣的回調函數。

youdao

前言

ydtech

在 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,第二個參數是出錯後要執行的函數r eject。

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:joy:),那就需要用別的方式來解決回調地獄了。

對於 electron 只要你用的是近幾年的版本都是支持的,electron 可以當成是 chromium 和 node.js 的結合體,特別適合用來寫跨平台的工具類桌面應用程序。

往期推薦