這個問題作者認為是所有從后端轉向前端開發的程式員,都會遇到的第一問題,JS前端編程與后端編程最大的不同,就是它的異步機制,同時這也是它的核心機制,
為了更好地說明如何回傳異步呼叫的結果,先看三個嘗試異步呼叫的示例吧,
示例一:呼叫一個后端介面,回傳介面回傳的內容
function foo() {
var result
$.ajax({
url: "...",
success: function(response) {
result = response
}
});
return result // 回傳:undefined
}
函式foo嘗試呼叫一個介面并回傳其內容,但每次執行都只會回傳undefiend,
示例二:使用Promise的then方法,同樣是呼叫介面然后回傳內容
function foo() {
var result
fetch(url).then(function(response) {
result = response
})
return result // 回傳:undefined
}
與上一個示例的呼叫一樣,也只會回傳undefined,
示例三:讀取本地檔案,然后回傳其內容
function foo() {
var result
fs.readFile("path/to/file", function(err, response) {
result = response
})
return result // 回傳:undefined
}
毫無意外這個示例的呼叫結果也是undefined,
為什么?
因為這三個示例涉及的三個操作————ajax、fetch、readFile都是異步操作,從操作指令發出,到拿到結果,這中間有一個時間間隔,無論你的機器性能多么強勁,這個間隔也無法完全抹掉,這是由JS的主執行緒是單執行緒而決定的,JS代碼執行到一定位置的時候,它不能等待,等待意味著用戶界面的卡頓,這是用戶不能容忍的,JS采用異步執行緒優化該場景,當主執行緒中有異步操作發起時,主執行緒不會阻塞,會繼續向下執行;當異步操作有資料回傳時,異步執行緒會主動通知主執行緒:“Hi,老大,資料來了,現在要用嗎?”
“好的!馬上給我,”
這樣異步執行緒把異步代碼推給主執行緒,異步代碼才得以執行,對于上面三個示例而言,result = response就是它們的異步代碼,
下面作者畫一張輔助理解這種機制吧:

當異步執行緒準備好資料的時候,主執行緒也不是馬上就能處理,只有當主執行緒有空閑了,并且前面沒有排隊等待處理的資料了,新的異步資料才能得以處理,
在了解了JS的異步機制以后,下面看前面三個示例如何正確改寫,
回呼函式:最古老的異步結果回傳方式
先看示例一,使用回呼函式改寫:
function foo(callback) {
$.ajax({
url: "...",
success: function(response) {
callback(response)
}
});
// return result // 回傳:undefined
}
在呼叫函式foo的時候,事先傳遞進來一個callback,當ajax操作取到介面資料的時候,將資料傳遞給callback,由callback自行處理,
這種基于回呼的解決方案,雖然“巧妙”地解決了問題,但在存在多層異步回呼的復雜專案中,往往由于一個操作依賴于多個異步資料而造成“回呼噩夢”,
ES2015:使用Promise物件與then方法鏈式呼叫
第二種改進的方案,不使用回呼函式,而是使用ES2015中新增的Promise及其then方法,下面以示例二進行改造:
function foo() {
return new Promise(function(resolve, reject) {
fetch(url).then(function(response) {
resolve(response)
})
})
}
foo().then(function(res){
console.log(res)
})..catch(function(err) {
//
})
foo回傳一個Promise物件,注意,Promise僅是一個可能承載正確資料的容器,它并不是資料,在使用它的,需要呼叫它的then方法才能取得資料(在有資料回傳的時候),與then同時存在的另一個有用的方法是catch,它用于捕捉異步操作可能出現的例外,處理可能的錯誤對加強魯棒性至關重要,這個catch方法不容忽視,
注意:示例中的fetch方法作者沒有給出具體實作,它在這里是作為一個回傳Promise物件的異步操作被對待的,也因此我們看到了,在這個方法被呼叫后回傳的物件上,也可以緊跟著呼叫then方法(第3行),
但是,這種使用Promise的解決方案就完美了嗎,就沒有問題了嗎?顯然不是的,
ES2017:使用async/await語法關鍵字
過多的“緊隨”風格的then方法呼叫及catch方法呼叫,讓代碼的前后邏輯不清晰;當我們閱讀這樣的代碼時,并不是從上向下瀑布式閱讀的,而是時而上、時而下跳動著閱讀的,這很不舒服,不僅閱讀時不舒服,撰寫時也很難以用一種像后端編程那樣的從上向下的簡潔的邏輯組織代碼,
下面開始開始使用ES2017標準中提供async/await語法關鍵字,對示例三進行改寫:
function foo() {
return new Promise(function(resolve, reject) {
fs.readFile("path/to/file", function(err, response) {
resolve(response)
})
})
}
(async function(){
const res = await foo().catch(console.log)
console.log(res)
})()
基于async/await語法關鍵字的方案,是使用Promise的方案的升級版,在這個方案中也使用了Promise,第8行第11行,這是一個IIFE(立即呼叫函式運算式),之所以要用一個只使用一次的臨時匿名函式將第9行第10行的代碼包裹起來,是因為await必須用在一個被async關鍵字修飾的函式或方法中,只能直接用到頂層的檔案作用域或模塊作用域下,
使用這種方案的優化是,代碼可以像后端編程那樣從上向下寫,結構可以很清晰,這也是一種被稱為“異步轉同步”的JS編程范式,在前端開發中已被普遍接受,
注意,“異步轉同步”并沒有真正改變異步代碼,異步代碼仍然是異步代碼,它們仍然會在異步執行緒中先默默地執行,等有資料回傳了再通知主執行緒處理,當我們使用這種編程模式的時候,一定不要在主執行緒上去await一個Promise,可以發起異步操作,讓異步操作像葡萄一樣掛在主執行緒上,但不能等待它們回傳了再往下執行,
jQuery的Deferred Object(延遲物件)
先看一段Promise+then方法風格的jQuery代碼:
$.ajax({
url: "test.html",
context: document.body
}).done(function() {
$(this).addClass("done")
});
第4行,這里的done方法是jQuery自行實作的,$.ajax方法回傳的是一個DeferredObject(延遲物件),這個物件上有done方法,這個方法與Promise的then類似,
jQuery成名在前,在ES2015標準誕生之前,jQuery的DeferredObject就已經被定義了,Promise本身并沒有神奇的地方,它可以發揮作用,主要依賴的是在JS中,Object是參考物件,繼承于Object原型的Promise也是參考物件,當異步操作發起時,只有一個“空”的Promise被創建了,但是它的參考被保持了;當資料回來的時候,資料再被“裝填”進這個物件,這樣通過先前持有的參考,異步代碼便可以訪問到物件上攜帶的資料,
Promise的勝利,更多是編程思想上的勝利,Promise的成功,也是編程思想上的成功,所有一種語言中編程思想上的成功,在其他語言中都可以被學習和借鑒,事實上在后端編程中,這種偽裝成同步代碼風格的異步編程思想也極其普遍,它們擁有一個共同的名字,叫協程,
小結
在JS中處理異步呼叫的結果,最佳實踐就是“異步轉同步”:使用Promise + async/await語法關鍵字,在這里async總是與await成對出現,一個async函式總是回傳一個Promise,一個await關鍵字總是在嘗試“解開”一個Promise,結局要么等到有價值的資料,要么異步出現異步,什么也沒有等到,為了避免出現例外,影響主執行緒的正常運行,一般要用catch規避例外,
著作權歸LIYI所有 基于CC BY-SA 4.0協議 原文鏈接:https://yishulun.com/posts/2022/33.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/540903.html
標籤:JavaScript
