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