來源: ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Asynchronous Programmingjavascript
譯者:飛龍html
協議:CC BY-NC-SA 4.0java
自豪地採用谷歌翻譯git
孰能濁以澄?靜之徐清;github
孰能安以久?動之徐生。apache
老子,《道德經》編程
計算機的核心部分稱爲處理器,它執行構成咱們程序的各個步驟。 到目前爲止,咱們看到的程序都是讓處理器忙碌,直到他們完成工做。 處理數字的循環之類的東西,幾乎徹底取決於處理器的速度。數組
可是許多程序與處理器以外的東西交互。 例如,他們可能經過計算機網絡進行通訊或從硬盤請求數據 - 這比從內存獲取數據要慢不少。promise
當發生這種事情時,讓處理器處於閒置狀態是可恥的 - 在此期間能夠作一些其餘工做。 某種程度上,它由你的操做系統處理,它將在多個正在運行的程序之間切換處理器。 可是,咱們但願單個程序在等待網絡請求時能作一些事情,這並無什麼幫助。瀏覽器
在同步編程模型中,一次只發生一件事。 當你調用執行長時間操做的函數時,它只會在操做完成時返回,而且能夠返回結果。 這會在你執行操做的時候中止你的程序。
異步模型容許同時發生多個事件。 當你開始一個動做時,你的程序會繼續運行。 當動做結束時,程序會受到通知並訪問結果(例如從磁盤讀取的數據)。
咱們可使用一個小例子來比較同步和異步編程:一個從網絡獲取兩個資源而後合併結果的程序。
在同步環境中,只有在請求函數完成工做後,它才返回,執行此任務的最簡單方法是逐個建立請求。 這有一個缺點,僅當第一個請求完成時,第二個請求才會啓動。 所花費的總時間至少是兩個響應時間的總和。
在同步系統中解決這個問題的方法是啓動額外的控制線程。 線程是另外一個正在運行的程序,它的執行可能會交叉在操做系統與其餘程序當中 - 由於大多數現代計算機都包含多個處理器,因此多個線程甚至可能同時運行在不一樣的處理器上。 第二個線程能夠啓動第二個請求,而後兩個線程等待它們的結果返回,以後它們從新同步來組合它們的結果。
在下圖中,粗線表示程序正常花費運行的時間,細線表示等待網絡所花費的時間。 在同步模型中,網絡所花費的時間是給定控制線程的時間線的一部分。 在異步模型中,從概念上講,啓動網絡操做會致使時間軸中出現分裂。 啓動該動做的程序將繼續運行,而且該動做將與其同時發生,並在程序結束時通知該程序。
另外一種描述差別的方式是,等待動做完成在同步模型中是隱式的,而在異步模型中,在咱們的控制之下,它是顯式的。
異步性是個雙刃劍。 它能夠生成不適合直線控制模型的程序,但它也可使直線控制的程序更加笨拙。 本章後面咱們會看到一些方法來解決這種笨拙。
兩種重要的 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
在後面的章節中,咱們將看到在事件循環中運行的,各類其餘類型的事件。
當你的程序同步運行時,除了那些程序自己所作的外,沒有發生任何狀態變化。 對於異步程序,這是不一樣的 - 它們在執行期間可能會有空白,這個時候其餘代碼能夠運行。
咱們來看一個例子。 咱們烏鴉的愛好之一是計算整個村莊每一年孵化的雛雞數量。 鳥巢將這一數量存儲在他們的存儲器中。 下面的代碼嘗試枚舉給定年份的全部鳥巢的計數。
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}`) }\n`; })); 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("\n"); }
像這樣的錯誤很容易作出來,特別是在使用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); } });