摘要:回調異步編程的一種方法是使執行慢動作的函數接受額外的參數,即回調函數。執行異步工作的函數通常會在完成工作之前返回,安排回調函數在完成時調用。它注冊了一個回調函數,當解析并產生一個值時被調用。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Asynchronous Programming
譯者:飛龍
協議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
孰能濁以澄?靜之徐清;
孰能安以久?動之徐生。
老子,《道德經》
計算機的核心部分稱為處理器,它執行構成我們程序的各個步驟。 到目前為止,我們看到的程序都是讓處理器忙碌,直到他們完成工作。 處理數字的循環之類的東西,幾乎完全取決于處理器的速度。
但是許多程序與處理器之外的東西交互。 例如,他們可能通過計算機網絡進行通信或從硬盤請求數據 - 這比從內存獲取數據要慢很多。
當發生這種事情時,讓處理器處于閑置狀態是可恥的 - 在此期間可以做一些其他工作。 某種程度上,它由你的操作系統處理,它將在多個正在運行的程序之間切換處理器。 但是,我們希望單個程序在等待網絡請求時能做一些事情,這并沒有什么幫助。
異步在同步編程模型中,一次只發生一件事。 當你調用執行長時間操作的函數時,它只會在操作完成時返回,并且可以返回結果。 這會在你執行操作的時候停止你的程序。
異步模型允許同時發生多個事件。 當你開始一個動作時,你的程序會繼續運行。 當動作結束時,程序會受到通知并訪問結果(例如從磁盤讀取的數據)。
我們可以使用一個小例子來比較同步和異步編程:一個從網絡獲取兩個資源然后合并結果的程序。
在同步環境中,只有在請求函數完成工作后,它才返回,執行此任務的最簡單方法是逐個創建請求。 這有一個缺點,僅當第一個請求完成時,第二個請求才會啟動。 所花費的總時間至少是兩個響應時間的總和。
在同步系統中解決這個問題的方法是啟動額外的控制線程。 線程是另一個正在運行的程序,它的執行可能會交叉在操作系統與其他程序當中 - 因為大多數現代計算機都包含多個處理器,所以多個線程甚至可能同時運行在不同的處理器上。 第二個線程可以啟動第二個請求,然后兩個線程等待它們的結果返回,之后它們重新同步來組合它們的結果。
在下圖中,粗線表示程序正常花費運行的時間,細線表示等待網絡所花費的時間。 在同步模型中,網絡所花費的時間是給定控制線程的時間線的一部分。 在異步模型中,從概念上講,啟動網絡操作會導致時間軸中出現分裂。 啟動該動作的程序將繼續運行,并且該動作將與其同時發生,并在程序結束時通知該程序。
另一種描述差異的方式是,等待動作完成在同步模型中是隱式的,而在異步模型中,在我們的控制之下,它是顯式的。
異步性是個雙刃劍。 它可以生成不適合直線控制模型的程序,但它也可以使直線控制的程序更加笨拙。 本章后面我們會看到一些方法來解決這種笨拙。
兩種重要的 JavaScript 編程平臺(瀏覽器和 Node.js)都可能需要一段時間的異步操作,而不是依賴線程。 由于使用線程進行編程非常困難(理解程序在同時執行多個事情時所做的事情要困難得多),這通常被認為是一件好事。
烏鴉科技大多數人都知道烏鴉非常聰明。 他們可以使用工具,提前計劃,記住事情,甚至可以互相溝通這些事情。
大多數人不知道的是,他們能夠做一些事情,并且對我們隱藏得很好。我聽說一個有聲望的(但也有點古怪的)專家 corvids 認為,烏鴉技術并不落后于人類的技術,并且正在迎頭趕上。
例如,許多烏鴉文明能夠構建計算設備。 這些并不是電子的,就像人類的計算設備一樣,但是它們操作微小昆蟲的行動,這種昆蟲是與白蟻密切相關的物種,它與烏鴉形成了共生關系。 鳥類為它們提供食物,對之對應,昆蟲建立并操作復雜的殖民地,在其內部的生物的幫助下進行計算。
這些殖民地通常位于大而久遠的鳥巢中。 鳥類和昆蟲一起工作,建立一個球形粘土結構的網絡,隱藏在巢的樹枝之間,昆蟲在其中生活和工作。
為了與其他設備通信,這些機器使用光信號。 鳥類在特殊的通訊莖中嵌入反光材料片段,昆蟲校準這些反光材料將光線反射到另一個鳥巢,將數據編碼為一系列快速閃光。 這意味著只有具有完整視覺連接的巢才能溝通。
我們的朋友 corvid 專家已經繪制了 Rh?ne 河畔的 Hières-sur-Amby 村的烏鴉鳥巢網絡。 這張地圖顯示了鳥巢及其連接。
在一個令人震驚的趨同進化的例子中,烏鴉計算機運行 JavaScript。 在本章中,我們將為他們編寫一些基本的網絡函數。
回調異步編程的一種方法是使執行慢動作的函數接受額外的參數,即回調函數。動作開始,當它結束時,使用結果調用回調函數。
例如,在 Node.js 和瀏覽器中都可用的setTimeout函數,等待給定的毫秒數(一秒為一千毫秒),然后調用一個函數。
setTimeout(() => console.log("Tick"), 500);
等待通常不是一種非常重要的工作,但在做一些事情時,例如更新動畫或檢查某件事是否花費比給定時間更長的時間,可能很有用。
使用回調在一行中執行多個異步操作,意味著你必須不斷傳遞新函數來處理操作之后的計算延續。
大多數烏鴉鳥巢計算機都有一個長期的數據存儲器,其中的信息刻在小樹枝上,以便以后可以檢索。雕刻或查找一段數據需要一些時間,所以長期存儲的接口是異步的,并使用回調函數。
存儲器按照名稱存儲 JSON 編碼的數據片段。烏鴉可以存儲它隱藏食物的地方的信息,其名稱為"food caches",它可以包含指向其他數據片段的名稱數組,描述實際的緩存。為了在 Big Oak 鳥巢的存儲器中查找食物緩存,烏鴉可以運行這樣的代碼:
import {bigOak} from "./crow-tech"; bigOak.readStorage("food caches", caches => { let firstCache = caches[0]; bigOak.readStorage(firstCache, info => { console.log(info); }); });
(所有綁定名稱和字符串都已從烏鴉語翻譯成英語。)
這種編程風格是可行的,但縮進級別隨著每個異步操作而增加,因為你最終會在另一個函數中。 做更復雜的事情,比如同時運行多個動作,會變得有點笨拙。
烏鴉鳥巢計算機為使用請求-響應對進行通信而構建。 這意味著一個鳥巢向另一個鳥巢發送消息,然后它立即返回一個消息,確認收到,并可能包括對消息中提出的問題的回復。
每條消息都標有一個類型,它決定了它的處理方式。 我們的代碼可以為特定的請求類型定義處理器,并且當這樣的請求到達時,調用處理器來產生響應。
"./crow-tech"模塊所導出的接口為通信提供基于回調的函數。 鳥巢擁有send方法來發送請求。 它接受目標鳥巢的名稱,請求的類型和請求的內容作為它的前三個參數,以及一個用于調用的函數,作為其第四個和最后一個參數,當響應到達時調用。
bigOak.send("Cow Pasture", "note", "Let"s caw loudly at 7PM", () => console.log("Note delivered."));
但為了使鳥巢能夠接收該請求,我們首先必須定義名為"note"的請求類型。 處理請求的代碼不僅要在這臺鳥巢計算機上運行,而且還要運行在所有可以接收此類消息的鳥巢上。 我們只假定一只烏鴉飛過去,并將我們的處理器代碼安裝在所有的鳥巢中。
import {defineRequestType} from "./crow-tech"; defineRequestType("note", (nest, content, source, done) => { console.log(`${nest.name} received note: ${content}`); done(); });
defineRequestType函數定義了一種新的請求類型。該示例添加了對"note"請求的支持,它只是向給定的鳥巢發送備注。我們的實現調用console.log,以便我們可以驗證請求到達。鳥巢有name屬性,保存他們的名字。
給handler的第四個參數done,是一個回調函數,它在完成請求時必須調用。如果我們使用了處理器的返回值作為響應值,那么這意味著請求處理器本身不能執行異步操作。執行異步工作的函數通常會在完成工作之前返回,安排回調函數在完成時調用。所以我們需要一些異步機制 - 在這種情況下是另一個回調函數 - 在響應可用時發出信號。
某種程度上,異步性是傳染的。任何調用異步的函數的函數,本身都必須是異步的,使用回調或類似的機制來傳遞其結果。調用回調函數比簡單地返回一個值更容易出錯,所以以這種方式構建程序的較大部分并不是很好。
Promise當這些概念可以用值表示時,處理抽象概念通常更容易。 在異步操作的情況下,你不需要安排將來某個時候調用的函數,而是返回一個代表這個未來事件的對象。
這是標準類Promise的用途。 Promise是一種異步行為,可以在某個時刻完成并產生一個值。 當值可用時,它能夠通知任何感興趣的人。
創建Promise的最簡單方法是調用Promise.resolve。 這個函數確保你給它的值包含在一個Promise中。 如果它已經是Promise,那么僅僅返回它 - 否則,你會得到一個新的Promise,并使用你的值立即結束。
let fifteen = Promise.resolve(15); fifteen.then(value => console.log(`Got ${value}`)); // → Got 15
為了獲得Promise的結果,可以使用它的then方法。 它注冊了一個回調函數,當Promise解析并產生一個值時被調用。 你可以將多個回調添加到單個Promise中,即使在Promise解析(完成)后添加它們,它們也會被調用。
但那不是then方法所做的一切。 它返回另一個Promise,它解析處理器函數返回的值,或者如果返回Promise,則等待該Promise,然后解析為結果。
將Promise視為一種手段,將值轉化為異步現實,是有用處的。 一個正常的值就在那里。promised 的值是未來可能存在或可能出現的值。 根據Promise定義的計算對這些包裝值起作用,并在值可用時異步執行。
為了創建Promise,你可以將Promise用作構造器。 它有一個有點奇怪的接口 - 構造器接受一個函數作為參數,它會立即調用,并傳遞一個函數來解析這個Promise。 它以這種方式工作,而不是使用resolve方法,這樣只有創建Promise的代碼才能解析它。
這就是為readStorage函數創建基于Promise的接口的方式。
function storage(nest, name) { return new Promise(resolve => { nest.readStorage(name, result => resolve(result)); }); } storage(bigOak, "enemies") .then(value => console.log("Got", value));
這個異步函數返回一個有意義的值。 這是Promise的主要優點 - 它們簡化了異步函數的使用。 基于Promise的函數不需要傳遞回調,而是類似于常規函數:它們將輸入作為參數并返回它們的輸出。 唯一的區別是輸出可能還不可用。
故障譯者注:這段如果有配套代碼會更容易理解,但是沒有,所以湊合看吧。
常規的 JavaScript 計算可能會因拋出異常而失敗。 異步計算經常需要類似的東西。 網絡請求可能會失敗,或者作為異步計算的一部分的某些代碼,可能會引發異常。
異步編程的回調風格中最緊迫的問題之一是,確保將故障正確地報告給回調函數,是非常困難的。
一個廣泛使用的約定是,回調函數的第一個參數用于指示操作失敗,第二個參數包含操作成功時生成的值。 這種回調函數必須始終檢查它們是否收到異常,并確保它們引起的任何問題,包括它們調用的函數所拋出的異常,都會被捕獲并提供給正確的函數。
Promise使這更容易。可以解決它們(操作成功完成)或拒絕(故障)。只有在操作成功時,才會調用解析處理器(使用then注冊),并且拒絕會自動傳播給由then返回的新Promise。當一個處理器拋出一個異常時,這會自動使then調用產生的Promise被拒絕。因此,如果異步操作鏈中的任何元素失敗,則整個鏈的結果被標記為拒絕,并且不會調用失敗位置之后的任何常規處理器。
就像Promise的解析提供了一個值,拒絕它也提供了一個值,通常稱為拒絕的原因。當處理器中的異常導致拒絕時,異常值將用作原因。同樣,當處理器返回被拒絕的Promise時,拒絕流入下一個Promise。Promise.reject函數會創建一個新的,立即被拒絕的Promise。
為了明確地處理這種拒絕,Promise有一個catch方法,用于注冊一個處理器,當Promise被拒絕時被調用,類似于處理器處理正常解析的方式。 這也非常類似于then,因為它返回一個新的Promise,如果它正常解析,它將解析原始Promise的值,否則返回catch處理器的結果。 如果catch處理器拋出一個錯誤,新的Promise也被拒絕。
作為簡寫,then還接受拒絕處理器作為第二個參數,因此你可以在單個方法調用中,裝配這兩種的處理器。
傳遞給Promise構造器的函數接收第二個參數,并與解析函數一起使用,它可以用來拒絕新的Promise。
通過調用then和catch創建的Promise值的鏈條,可以看作異步值或失敗沿著它移動的流水線。 由于這種鏈條通過注冊處理器來創建,因此每個鏈條都有一個成功處理器或與其關聯的拒絕處理器(或兩者都有)。 不匹配結果類型(成功或失敗)的處理器將被忽略。 但是那些匹配的對象被調用,并且它們的結果決定了下一次會出現什么樣的值 -- 返回非Promise值時成功,當它拋出異常時拒絕,并且當它返回其中一個時是Promise的結果。
就像環境處理未捕獲的異常一樣,JavaScript 環境可以檢測未處理Promise拒絕的時候,并將其報告為錯誤。
網絡是困難的偶爾,烏鴉的鏡像系統沒有足夠的光線來傳輸信號,或者有些東西阻擋了信號的路徑。 信號可能發送了,但從未收到。
事實上,這只會導致提供給send的回調永遠不會被調用,這可能會導致程序停止,而不會注意到問題。 如果在沒有得到回應的特定時間段內,請求會超時并報告故障,那就很好。
通常情況下,傳輸故障是隨機事故,例如汽車的前燈會干擾光信號,只需重試請求就可以使其成功。 所以,當我們處理它時,讓我們的請求函數在放棄之前自動重試發送請求幾次。
而且,既然我們已經確定Promise是一件好事,我們也會讓我們的請求函數返回一個Promise。 對于他們可以表達的內容,回調和Promise是等同的。 基于回調的函數可以打包,來公開基于Promise的接口,反之亦然。
即使請求及其響應已成功傳遞,響應也可能表明失敗 - 例如,如果請求嘗試使用未定義的請求類型或處理器,會引發錯誤。 為了支持這個,send和defineRequestType遵循前面提到的慣例,其中傳遞給回調的第一個參數是故障原因,如果有的話,第二個參數是實際結果。
這些可以由我們的包裝翻譯成Promise的解析和拒絕。
class Timeout extends Error {} function request(nest, target, type, content) { return new Promise((resolve, reject) => { let done = false; function attempt(n) { nest.send(target, type, content, (failed, value) => { done = true; if (failed) reject(failed); else resolve(value); }); setTimeout(() => { if (done) return; else if (n < 3) attempt(n + 1); else reject(new Timeout("Timed out")); }, 250); } attempt(1); }); }
因為Promise只能解析(或拒絕)一次,所以這個是有效的。 第一次調用resolve或reject會決定Promise的結果,并且任何進一步的調用(例如請求結束后到達的超時,或在另一個請求結束后返回的請求)都將被忽略。
為了構建異步循環,對于重試,我們需要使用遞歸函數 - 常規循環不允許我們停止并等待異步操作。 attempt函數嘗試發送請求一次。 它還設置了超時,如果 250 毫秒后沒有響應返回,則開始下一次嘗試,或者如果這是第四次嘗試,則以Timeout實例為理由拒絕該Promise。
每四分之一秒重試一次,一秒鐘后沒有響應就放棄,這絕對是任意的。 甚至有可能,如果請求確實過來了,但處理器花費了更長時間,請求將被多次傳遞。 我們會編寫我們的處理器,并記住這個問題 - 重復的消息應該是無害的。
總的來說,我們現在不會建立一個世界級的,強大的網絡。 但沒關系 - 在計算方面,烏鴉沒有很高的預期。
為了完全隔離我們自己的回調,我們將繼續,并為defineRequestType定義一個包裝器,它允許處理器返回一個Promise或明確的值,并且連接到我們的回調。
function requestType(name, handler) { defineRequestType(name, (nest, content, source, callback) => { try { Promise.resolve(handler(nest, content, source)) .then(response => callback(null, response), failure => callback(failure)); } catch (exception) { callback(exception); } }); }
如果處理器返回的值還不是Promise,Promise.resolve用于將轉換為Promise。
請注意,處理器的調用必須包裝在try塊中,以確保直接引發的任何異常都會被提供給回調函數。 這很好地說明了使用原始回調正確處理錯誤的難度 - 很容易忘記正確處理類似的異常,如果不這樣做,故障將無法報告給正確的回調。Promise使其大部分是自動的,因此不易出錯。
Promise的集合每臺鳥巢計算機在其neighbors屬性中,都保存了傳輸距離內的其他鳥巢的數組。 為了檢查當前哪些可以訪問,你可以編寫一個函數,嘗試向每個鳥巢發送一個"ping"請求(一個簡單地請求響應的請求),并查看哪些返回了。
在處理同時運行的Promise集合時,Promise.all函數可能很有用。 它返回一個Promise,等待數組中的所有Promise解析,然后解析這些Promise產生的值的數組(與原始數組的順序相同)。 如果任何Promise被拒絕,Promise.all的結果本身被拒絕。
requestType("ping", () => "pong"); function availableNeighbors(nest) { let requests = nest.neighbors.map(neighbor => { return request(nest, neighbor, "ping") .then(() => true, () => false); }); return Promise.all(requests).then(result => { return nest.neighbors.filter((_, i) => result[i]); }); }
當一個鄰居不可用時,我們不希望整個組合Promise失敗,因為那時我們仍然不知道任何事情。 因此,在鄰居集合上映射一個函數,將它們變成請求Promise,并附加處理器,這些處理器使成功的請求產生true,拒絕的產生false。
在組合Promise的處理器中,filter用于從neighbors數組中刪除對應值為false的元素。 這利用了一個事實,filter將當前元素的數組索引作為其過濾函數的第二個參數(map,some和類似的高階數組方法也一樣)。
網絡泛洪鳥巢僅僅可以鄰居通信的事實,極大地減少了這個網絡的實用性。
為了將信息廣播到整個網絡,一種解決方案是設置一種自動轉發給鄰居的請求。 然后這些鄰居轉發給它們的鄰居,直到整個網絡收到這個消息。
import {everywhere} from "./crow-tech"; everywhere(nest => { nest.state.gossip = []; }); function sendGossip(nest, message, exceptFor = null) { nest.state.gossip.push(message); for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "gossip", message); } } requestType("gossip", (nest, message, source) => { if (nest.state.gossip.includes(message)) return; console.log(`${nest.name} received gossip "${ message}" from ${source}`); sendGossip(nest, message, source); });
為了避免永遠在網絡上發送相同的消息,每個鳥巢都保留一組已經看到的閑話字符串。 為了定義這個數組,我們使用everywhere函數(它在每個鳥巢上運行代碼)向鳥巢的狀態對象添加一個屬性,這是我們將保存鳥巢局部狀態的地方。
當一個鳥巢收到一個重復的閑話消息,它會忽略它。每個人都盲目重新發送這些消息時,這很可能發生。 但是當它收到一條新消息時,它會興奮地告訴它的所有鄰居,除了發送消息的那個鄰居。
這將導致一條新的閑話通過網絡傳播,如在水中的墨水一樣。 即使一些連接目前不工作,如果有一條通往指定鳥巢的替代路線,閑話將通過那里到達它。
這種網絡通信方式稱為泛洪 - 它用一條信息充滿網絡,直到所有節點都擁有它。
我們可以調用sendGossip看看村子里的消息流。
sendGossip(bigOak, "Kids with airgun in the park");消息路由
如果給定節點想要與其他單個節點通信,泛洪不是一種非常有效的方法。 特別是當網絡很大時,這會導致大量無用的數據傳輸。
另一種方法是為消息設置節點到節點的傳輸方式,直到它們到達目的地。 這樣做的困難在于,它需要網絡布局的知識。 為了向遠方的鳥巢發送請求,有必要知道哪個鄰近的鳥巢更靠近其目的地。 以錯誤的方向發送它不會有太大好處。
由于每個鳥巢只知道它的直接鄰居,因此它沒有計算路線所需的信息。 我們必須以某種方式,將這些連接的信息傳播給所有鳥巢。 當放棄或建造新的鳥巢時,最好是允許它隨時間改變的方式。
我們可以再次使用泛洪,但不檢查給定的消息是否已經收到,而是檢查對于給定鳥巢來說,鄰居的新集合,是否匹配我們擁有的當前集合。
requestType("connections", (nest, {name, neighbors}, source) => { let connections = nest.state.connections; if (JSON.stringify(connections.get(name)) == JSON.stringify(neighbors)) return; connections.set(name, neighbors); broadcastConnections(nest, name, source); }); function broadcastConnections(nest, name, exceptFor = null) { for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "connections", { name, neighbors: nest.state.connections.get(name) }); } } everywhere(nest => { nest.state.connections = new Map; nest.state.connections.set(nest.name, nest.neighbors); broadcastConnections(nest, nest.name); });
該比較使用JSON.stringify,因為對象或數組上的==只有在兩者完全相同時才返回true,這不是我們這里所需的。 比較 JSON 字符串是比較其內容的一種簡單而有效的方式。
節點立即開始廣播它們的連接,它們應該立即為每個鳥巢提供當前網絡圖的映射,除非有一些鳥巢完全無法到達。
你可以用圖做的事情,就是找到里面的路徑,就像我們在第 7 章中看到的那樣。如果我們有一條通往消息目的地的路線,我們知道將它發送到哪個方向。
這個findRoute函數非常類似于第 7 章中的findRoute,它搜索到達網絡中給定節點的路線。 但不是返回整個路線,而是返回下一步。 下一個鳥巢將使用它的有關網絡的當前信息,來決定將消息發送到哪里。
function findRoute(from, to, connections) { let work = [{at: from, via: null}]; for (let i = 0; i < work.length; i++) { let {at, via} = work[i]; for (let next of connections.get(at) || []) { if (next == to) return via; if (!work.some(w => w.at == next)) { work.push({at: next, via: via || next}); } } } return null; }
現在我們可以建立一個可以發送長途信息的函數。 如果該消息被發送給直接鄰居,它將照常發送。 如果不是,則將其封裝在一個對象中,并使用"route"請求類型,將其發送到更接近目標的鄰居,這將導致該鄰居重復相同的行為。
function routeRequest(nest, target, type, content) { if (nest.neighbors.includes(target)) { return request(nest, target, type, content); } else { let via = findRoute(nest.name, target, nest.state.connections); if (!via) throw new Error(`No route to ${target}`); return request(nest, via, "route", {target, type, content}); } } requestType("route", (nest, {target, type, content}) => { return routeRequest(nest, target, type, content); });
我們現在可以將消息發送到教堂塔樓的鳥巢中,它的距離有四跳。
routeRequest(bigOak, "Church Tower", "note", "Incoming jackdaws!");
我們已經在原始通信系統的基礎上構建了幾層功能,來使其便于使用。 這是一個(盡管是簡化的)真實計算機網絡工作原理的很好的模型。
計算機網絡的一個顯著特點是它們不可靠 - 建立在它們之上的抽象可以提供幫助,但是不能抽象出網絡故障。所以網絡編程通常關于預測和處理故障。
async函數為了存儲重要信息,據了解烏鴉在鳥巢中復制它。 這樣,當一只鷹摧毀一個鳥巢時,信息不會丟失。
為了檢索它自己的存儲器中沒有的信息,鳥巢計算機可能會詢問網絡中其他隨機鳥巢,直到找到一個鳥巢計算機。
requestType("storage", (nest, name) => storage(nest, name)); function findInStorage(nest, name) { return storage(nest, name).then(found => { if (found != null) return found; else return findInRemoteStorage(nest, name); }); } function network(nest) { return Array.from(nest.state.connections.keys()); } function findInRemoteStorage(nest, name) { let sources = network(nest).filter(n => n != nest.name); function next() { if (sources.length == 0) { return Promise.reject(new Error("Not found")); } else { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); return routeRequest(nest, source, "storage", name) .then(value => value != null ? value : next(), next); } } return next(); }
因為connections 是一個Map,Object.keys不起作用。 它有一個key方法,但是它返回一個迭代器而不是數組。 可以使用Array.from函數將迭代器(或可迭代對象)轉換為數組。
即使使用Promise,這是一些相當笨拙的代碼。 多個異步操作以不清晰的方式鏈接在一起。 我們再次需要一個遞歸函數(next)來建模鳥巢上的遍歷。
代碼實際上做的事情是完全線性的 - 在開始下一個動作之前,它總是等待先前的動作完成。 在同步編程模型中,表達會更簡單。
好消息是 JavaScript 允許你編寫偽同步代碼。 異步函數是一種隱式返回Promise的函數,它可以在其主體中,以看起來同步的方式等待其他Promise。
我們可以像這樣重寫findInStorage:
async function findInStorage(nest, name) { let local = await storage(nest, name); if (local != null) return local; let sources = network(nest).filter(n => n != nest.name); while (sources.length > 0) { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); try { let found = await routeRequest(nest, source, "storage", name); if (found != null) return found; } catch (_) {} } throw new Error("Not found"); }
異步函數由function關鍵字之前的async標記。 方法也可以通過在名稱前面編寫async來做成異步的。 當調用這樣的函數或方法時,它返回一個Promise。 只要主體返回了某些東西,這個Promise就解析了。 如果它拋出異常,則Promise被拒絕。
findInStorage(bigOak, "events on 2017-12-21") .then(console.log);
在異步函數內部,await這個詞可以放在表達式的前面,等待解Promise被解析,然后才能繼續執行函數。
這樣的函數不再像常規的 JavaScript 函數一樣,從頭到尾運行。 相反,它可以在有任何帶有await的地方凍結,并在稍后恢復。
對于有意義的異步代碼,這種標記通常比直接使用Promise更方便。即使你需要做一些不適合同步模型的東西,比如同時執行多個動作,也很容易將await和直接使用Promise結合起來。
生成器函數暫停然后再次恢復的能力,不是異步函數所獨有的。 JavaScript 也有一個稱為生成器函數的特性。 這些都是相似的,但沒有Promise。
當用function*定義一個函數(在函數后面加星號)時,它就成為一個生成器。 當你調用一個生成器時,它將返回一個迭代器,我們在第 6 章已經看到了它。
function* powers(n) { for (let current = n;; current *= n) { yield current; } } for (let power of powers(3)) { if (power > 50) break; console.log(power); } // → 3 // → 9 // → 27
最初,當你調用powers時,函數在開頭被凍結。 每次在迭代器上調用next時,函數都會運行,直到它碰到yield表達式,該表達式會暫停它,并使得產生的值成為由迭代器產生的下一個值。 當函數返回時(示例中的那個永遠不會),迭代器就結束了。
使用生成器函數時,編寫迭代器通常要容易得多。 可以用這個生成器編寫group類的迭代器(來自第 6 章的練習):
Group.prototype[Symbol.iterator] = function*() { for (let i = 0; i < this.members.length; i++) { yield this.members[i]; } };
不再需要創建一個對象來保存迭代狀態 - 生成器每次yield時都會自動保存其本地狀態。
這樣的yield表達式可能僅僅直接出現在生成器函數本身中,而不是在你定義的內部函數中。 生成器在返回(yield)時保存的狀態,只是它的本地環境和它yield的位置。
異步函數是一種特殊的生成器。 它在調用時會產生一個Promise,當它返回(完成)時被解析,并在拋出異常時被拒絕。 每當它yield(await)一個Promise時,該Promise的結果(值或拋出的異常)就是await表達式的結果。
事件循環異步程序是逐片段執行的。 每個片段可能會啟動一些操作,并調度代碼在操作完成或失敗時執行。 在這些片段之間,該程序處于空閑狀態,等待下一個動作。
所以回調函數不會直接被調度它們的代碼調用。 如果我從一個函數中調用setTimeout,那么在調用回調函數時該函數已經返回。 當回調返回時,控制權不會回到調度它的函數。
異步行為發生在它自己的空函數調用堆棧上。 這是沒有Promise的情況下,在異步代碼之間管理異常很難的原因之一。 由于每個回調函數都是以幾乎為空的堆棧開始,因此當它們拋出一個異常時,你的catch處理程序不會在堆棧中。
try { setTimeout(() => { throw new Error("Woosh"); }, 20); } catch (_) { // This will not run console.log("Caught!"); }
無論事件發生多么緊密(例如超時或傳入請求),JavaScript 環境一次只能運行一個程序。 你可以把它看作在程序周圍運行一個大循環,稱為事件循環。 當沒有什么可以做的時候,那個循環就會停止。 但隨著事件來臨,它們被添加到隊列中,并且它們的代碼被逐個執行。 由于沒有兩件事同時運行,運行緩慢的代碼可能會延遲其他事件的處理。
這個例子設置了一個超時,但是之后占用時間,直到超時的預定時間點,導致超時延遲。
let start = Date.now(); setTimeout(() => { console.log("Timeout ran at", Date.now() - start); }, 20); while (Date.now() < start + 50) {} console.log("Wasted time until", Date.now() - start); // → Wasted time until 50 // → Timeout ran at 55
Promise總是作為新事件來解析或拒絕。 即使已經解析了Promise,等待它會導致你的回調在當前腳本完成后運行,而不是立即執行。
Promise.resolve("Done").then(console.log); console.log("Me first!"); // → Me first! // → Done
在后面的章節中,我們將看到在事件循環中運行的,各種其他類型的事件。
異步的 bug當你的程序同步運行時,除了那些程序本身所做的外,沒有發生任何狀態變化。 對于異步程序,這是不同的 - 它們在執行期間可能會有空白,這個時候其他代碼可以運行。
我們來看一個例子。 我們烏鴉的愛好之一是計算整個村莊每年孵化的雛雞數量。 鳥巢將這一數量存儲在他們的存儲器中。 下面的代碼嘗試枚舉給定年份的所有鳥巢的計數。
function anyStorage(nest, source, name) { if (source == nest.name) return storage(nest, name); else return routeRequest(nest, source, "storage", name); } async function chicks(nest, year) { let list = ""; await Promise.all(network(nest).map(async name => { list += `${name}: ${ await anyStorage(nest, name, `chicks in ${year}`) } `; })); return list; }
async name =>部分展示了,通過將單詞async放在它們前面,也可以使箭頭函數變成異步的。
代碼不會立即看上去有問題......它將異步箭頭函數映射到鳥巢集合上,創建一組Promise,然后使用Promise.all,在返回它們構建的列表之前等待所有Promise。
但它有嚴重問題。 它總是只返回一行輸出,列出響應最慢的鳥巢。
chicks(bigOak, 2017).then(console.log);
你能解釋為什么嗎?
問題在于+=操作符,它在語句開始執行時接受list的當前值,然后當await結束時,將list綁定設為該值加上新增的字符串。
但是在語句開始執行的時間和它完成的時間之間存在一個異步間隔。 map表達式在任何內容添加到列表之前運行,因此每個+ =操作符都以一個空字符串開始,并在存儲檢索完成時結束,將list設置為單行列表 - 向空字符串添加那行的結果。
通過從映射的Promise中返回行,并對Promise.all的結果調用join,可以輕松避免這種情況,而不是通過更改綁定來構建列表。 像往常一樣,計算新值比改變現有值的錯誤更少。
async function chicks(nest, year) { let lines = network(nest).map(async name => { return name + ": " + await anyStorage(nest, name, `chicks in ${year}`); }); return (await Promise.all(lines)).join(" "); }
像這樣的錯誤很容易做出來,特別是在使用await時,你應該知道代碼中的間隔在哪里出現。 JavaScript 的顯式異步性(無論是通過回調,Promise還是await)的一個優點是,發現這些間隔相對容易。
總結異步編程可以表示等待長時間運行的動作,而不需要在這些動作期間凍結程序。 JavaScript 環境通常使用回調函數來實現這種編程風格,這些函數在動作完成時被調用。 事件循環調度這樣的回調,使其在適當的時候依次被調用,以便它們的執行不會重疊。
Promise和異步函數使異步編程更容易。Promise是一個對象,代表將來可能完成的操作。并且,異步函數使你可以像編寫同步程序一樣編寫異步程序。
練習 跟蹤手術刀村里的烏鴉擁有一把老式的手術刀,他們偶爾會用于特殊的任務 - 比如說,切開紗門或包裝。 為了能夠快速追蹤到手術刀,每次將手術刀移動到另一個鳥巢時,將一個條目添加到擁有它和拿走它的鳥巢的存儲器中,名稱為"scalpel",值為新的位置。
這意味著找到手術刀就是跟蹤存儲器條目的痕跡,直到你發現一個鳥巢指向它本身。
編寫一個異步函數locateScalpel,它從它運行的鳥巢開始。 你可以使用之前定義的anyStorage函數,來訪問任意鳥巢中的存儲器。 手術刀已經移動了很長時間,你可能會認為每個鳥巢的數據存儲器中都有一個"scalpel"條目。
接下來,再次寫入相同的函數,而不使用async和await。
在兩個版本中,請求故障是否正確顯示為拒絕? 如何實現?
async function locateScalpel(nest) { // Your code here. } function locateScalpel2(nest) { // Your code here. } locateScalpel(bigOak).then(console.log); // → Butcher Shop構建Promise.all
給定Promise的數組,Promise.all返回一個Promise,等待數組中的所有Promise完成。 然后它成功,產生結果值的數組。 如果數組中的一個Promise失敗,這個Promise也失敗,故障原因來自那個失敗的Promise。
自己實現一個名為Promise_all的常規函數。
請記住,在Promise成功或失敗后,它不能再次成功或失敗,并且解析它的函數的進一步調用將被忽略。 這可以簡化你處理Promise的故障的方式。
function Promise_all(promises) { return new Promise((resolve, reject) => { // Your code here. }); } // Test code. Promise_all([]).then(array => { console.log("This should be []:", array); }); function soon(val) { return new Promise(resolve => { setTimeout(() => resolve(val), Math.random() * 500); }); } Promise_all([soon(1), soon(2), soon(3)]).then(array => { console.log("This should be [1, 2, 3]:", array); }); Promise_all([soon(1), Promise.reject("X"), soon(3)]) .then(array => { console.log("We should not get here"); }) .catch(error => { if (error != "X") { console.log("Unexpected failure:", error); } });
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105052.html
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關于指導電腦的書。在可控的范圍內編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數字存儲在內存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Introduction 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版技能分享會是一個活動,其中興趣相同的人聚在一起,針對他們所知的事情進行小型非正式的展示。所有接口均以路徑為中心。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: Skill-Sharing Website 譯者:飛龍 協議:CC BY-NC-SA 4...
摘要:在這樣的程序中,異步編程通常是有幫助的。最初是為了使異步編程簡單方便而設計的。在年設計時,人們已經在瀏覽器中進行基于回調的編程,所以該語言的社區用于異步編程風格。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Node.js 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)...
摘要:為了運行包裹的程序,可以將這些值應用于它們。在瀏覽器中,輸出出現在控制臺中。在英文版頁面上運行示例或自己的代碼時,會在示例之后顯示輸出,而不是在瀏覽器的控制臺中顯示。這被稱為條件執行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Program Structure 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...
摘要:在本例中,使用屬性指定鏈接的目標,其中表示超文本鏈接。您應該認為和元數據隱式出現在示例中,即使它們沒有實際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:JavaScript and the Browser 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...
閱讀 2565·2023-04-25 20:05
閱讀 2891·2023-04-25 17:56
閱讀 2204·2021-10-14 09:49
閱讀 2687·2019-08-29 15:10
閱讀 2926·2019-08-29 12:25
閱讀 421·2019-08-28 18:23
閱讀 762·2019-08-26 13:26
閱讀 1375·2019-08-23 18:21