這個問題作者認為是所有從後端轉向前端開發的程式員,都會遇到的第一問題。JS前端編程與後端編程最大的不同,就是它的非同步機制,同時這也是它的核心機制。 為了更好地說明如何返回非同步調用的結果,先看三個嘗試非同步調用的示例吧。 示例一:調用一個後端介面,返回介面返回的內容 function foo() { v ...
這個問題作者認為是所有從後端轉向前端開發的程式員,都會遇到的第一問題。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