<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 十一、異步編程 > 原文:[Asynchronous Programming](http://eloquentjavascript.net/11_async.html) > > 譯者:[飛龍](https://github.com/wizardforcel) > > 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 自豪地采用[谷歌翻譯](https://translate.google.cn/) > 孰能濁以澄?靜之徐清; > > 孰能安以久?動之徐生。 > > 老子,《道德經》 計算機的核心部分稱為處理器,它執行構成我們程序的各個步驟。 到目前為止,我們看到的程序都是讓處理器忙碌,直到他們完成工作。 處理數字的循環之類的東西,幾乎完全取決于處理器的速度。 但是許多程序與處理器之外的東西交互。 例如,他們可能通過計算機網絡進行通信或從硬盤請求數據 - 這比從內存獲取數據要慢很多。 當發生這種事情時,讓處理器處于閑置狀態是可恥的 - 在此期間可以做一些其他工作。 某種程度上,它由你的操作系統處理,它將在多個正在運行的程序之間切換處理器。 但是,我們希望單個程序在等待網絡請求時能做一些事情,這并沒有什么幫助。 ## 異步 在同步編程模型中,一次只發生一件事。 當你調用執行長時間操作的函數時,它只會在操作完成時返回,并且可以返回結果。 這會在你執行操作的時候停止你的程序。 異步模型允許同時發生多個事件。 當你開始一個動作時,你的程序會繼續運行。 當動作結束時,程序會收到通知并訪問結果(例如從磁盤讀取的數據)。 我們可以使用一個小例子來比較同步和異步編程:一個從網絡獲取兩個資源然后合并結果的程序。 在同步環境中,只有在請求函數完成工作后,它才返回,執行此任務的最簡單方法是逐個創建請求。 這有一個缺點,僅當第一個請求完成時,第二個請求才會啟動。 所花費的總時間至少是兩個響應時間的總和。 在同步系統中解決這個問題的方法是啟動額外的控制線程。 線程是另一個正在運行的程序,它的執行可能會交叉在操作系統與其他程序當中 - 因為大多數現代計算機都包含多個處理器,所以多個線程甚至可能同時運行在不同的處理器上。 第二個線程可以啟動第二個請求,然后兩個線程等待它們的結果返回,之后它們重新同步來組合它們的結果。 在下圖中,粗線表示程序正常花費運行的時間,細線表示等待網絡所花費的時間。 在同步模型中,網絡所花費的時間是給定控制線程的時間線的一部分。 在異步模型中,從概念上講,啟動網絡操作會導致時間軸中出現分裂。 啟動該動作的程序將繼續運行,并且該動作將與其同時發生,并在程序結束時通知該程序。 ![](https://img.kancloud.cn/de/f3/def38cbcc87cd6b3831674b21d2eaeb7.svg) 另一種描述差異的方式是,等待動作完成在同步模型中是隱式的,而在異步模型中,在我們的控制之下,它是顯式的。 異步性是個雙刃劍。 它可以生成不適合直線控制模型的程序,但它也可以使直線控制的程序更加笨拙。 本章后面我們會看到一些方法來解決這種笨拙。 兩種重要的 JavaScript 編程平臺(瀏覽器和 Node.js)都可能需要一段時間的異步操作,而不是依賴線程。 由于使用線程進行編程非常困難(理解程序在同時執行多個事情時所做的事情要困難得多),這通常被認為是一件好事。 ## 烏鴉科技 大多數人都知道烏鴉非常聰明。 他們可以使用工具,提前計劃,記住事情,甚至可以互相溝通這些事情。 大多數人不知道的是,他們能夠做一些事情,并且對我們隱藏得很好。我聽說一個有聲望的(但也有點古怪的)專家 corvids 認為,烏鴉技術并不落后于人類的技術,并且正在迎頭趕上。 例如,許多烏鴉文明能夠構建計算設備。 這些并不是電子的,就像人類的計算設備一樣,但是它們操作微小昆蟲的行動,這種昆蟲是與白蟻密切相關的物種,它與烏鴉形成了共生關系。 鳥類為它們提供食物,對之對應,昆蟲建立并操作復雜的殖民地,在其內部的生物的幫助下進行計算。 這些殖民地通常位于大而久遠的鳥巢中。 鳥類和昆蟲一起工作,建立一個球形粘土結構的網絡,隱藏在巢的樹枝之間,昆蟲在其中生活和工作。 為了與其他設備通信,這些機器使用光信號。 鳥類在特殊的通訊莖中嵌入反光材料片段,昆蟲校準這些反光材料將光線反射到另一個鳥巢,將數據編碼為一系列快速閃光。 這意味著只有具有完整視覺連接的巢才能溝通。 我們的朋友 corvid 專家已經繪制了 Rh?ne 河畔的 Hières-sur-Amby 村的烏鴉鳥巢網絡。 這張地圖顯示了鳥巢及其連接。 在一個令人震驚的趨同進化的例子中,烏鴉計算機運行 JavaScript。 在本章中,我們將為他們編寫一些基本的網絡函數。 ![](https://img.kancloud.cn/65/8e/658e4985aec438da80d1510b3ccdc943_536x275.png) ## 回調 異步編程的一種方法是使執行慢動作的函數接受額外的參數,即回調函數。動作開始,當它結束時,使用結果調用回調函數。 例如,在 Node.js 和瀏覽器中都可用的`setTimeout`函數,等待給定的毫秒數(一秒為一千毫秒),然后調用一個函數。 ```js setTimeout(() => console.log("Tick"), 500); ``` 等待通常不是一種非常重要的工作,但在做一些事情時,例如更新動畫或檢查某件事是否花費比給定時間更長的時間,可能很有用。 使用回調在一行中執行多個異步操作,意味著你必須不斷傳遞新函數來處理操作之后的計算延續。 大多數烏鴉鳥巢計算機都有一個長期的數據存儲器,其中的信息刻在小樹枝上,以便以后可以檢索。雕刻或查找一段數據需要一些時間,所以長期存儲的接口是異步的,并使用回調函數。 存儲器按照名稱存儲 JSON 編碼的數據片段。烏鴉可以存儲它隱藏食物的地方的信息,其名稱為`"food caches"`,它可以包含指向其他數據片段的名稱數組,描述實際的緩存。為了在 Big Oak 鳥巢的存儲器中查找食物緩存,烏鴉可以運行這樣的代碼: ```js import {bigOak} from "./crow-tech"; bigOak.readStorage("food caches", caches => { let firstCache = caches[0]; bigOak.readStorage(firstCache, info => { console.log(info); }); }); ``` (所有綁定名稱和字符串都已從烏鴉語翻譯成英語。) 這種編程風格是可行的,但縮進級別隨著每個異步操作而增加,因為你最終會在另一個函數中。 做更復雜的事情,比如同時運行多個動作,會變得有點笨拙。 烏鴉鳥巢計算機為使用請求-響應對進行通信而構建。 這意味著一個鳥巢向另一個鳥巢發送消息,然后它立即返回一個消息,確認收到,并可能包括對消息中提出的問題的回復。 每條消息都標有一個類型,它決定了它的處理方式。 我們的代碼可以為特定的請求類型定義處理器,并且當這樣的請求到達時,調用處理器來產生響應。 `"./crow-tech"`模塊所導出的接口為通信提供基于回調的函數。 鳥巢擁有`send`方法來發送請求。 它接受目標鳥巢的名稱,請求的類型和請求的內容作為它的前三個參數,以及一個用于調用的函數,作為其第四個和最后一個參數,當響應到達時調用。 ```js bigOak.send("Cow Pasture", "note", "Let's caw loudly at 7PM", () => console.log("Note delivered.")); ``` 但為了使鳥巢能夠接收該請求,我們首先必須定義名為`"note"`的請求類型。 處理請求的代碼不僅要在這臺鳥巢計算機上運行,而且還要運行在所有可以接收此類消息的鳥巢上。 我們只假定一只烏鴉飛過去,并將我們的處理器代碼安裝在所有的鳥巢中。 ```js 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`,并使用你的值立即結束。 ```js 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`的接口的方式。 ```js 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`的解析和拒絕。 ```js 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`或明確的值,并且連接到我們的回調。 ```js 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`的結果本身被拒絕。 ```js 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`和類似的高階數組方法也一樣)。 ## 網絡泛洪 鳥巢僅僅可以鄰居通信的事實,極大地減少了這個網絡的實用性。 為了將信息廣播到整個網絡,一種解決方案是設置一種自動轉發給鄰居的請求。 然后這些鄰居轉發給它們的鄰居,直到整個網絡收到這個消息。 ```js 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`看看村子里的消息流。 ```js sendGossip(bigOak, "Kids with airgun in the park"); ``` ## 消息路由 如果給定節點想要與其他單個節點通信,泛洪不是一種非常有效的方法。 特別是當網絡很大時,這會導致大量無用的數據傳輸。 另一種方法是為消息設置節點到節點的傳輸方式,直到它們到達目的地。 這樣做的困難在于,它需要網絡布局的知識。 為了向遠方的鳥巢發送請求,有必要知道哪個鄰近的鳥巢更靠近其目的地。 以錯誤的方向發送它不會有太大好處。 由于每個鳥巢只知道它的直接鄰居,因此它沒有計算路線所需的信息。 我們必須以某種方式,將這些連接的信息傳播給所有鳥巢。 當放棄或建造新的鳥巢時,最好是允許它隨時間改變的方式。 我們可以再次使用泛洪,但不檢查給定的消息是否已經收到,而是檢查對于給定鳥巢來說,鄰居的新集合,是否匹配我們擁有的當前集合。 ```js 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`,它搜索到達網絡中給定節點的路線。 但不是返回整個路線,而是返回下一步。 下一個鳥巢將使用它的有關網絡的當前信息,來決定將消息發送到哪里。 ```js 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"`請求類型,將其發送到更接近目標的鄰居,這將導致該鄰居重復相同的行為。 ```js 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); }); ``` 我們現在可以將消息發送到教堂塔樓的鳥巢中,它的距離有四跳。 ```js routeRequest(bigOak, "Church Tower", "note", "Incoming jackdaws!"); ``` 我們已經在原始通信系統的基礎上構建了幾層功能,來使其便于使用。 這是一個(盡管是簡化的)真實計算機網絡工作原理的很好的模型。 計算機網絡的一個顯著特點是它們不可靠 - 建立在它們之上的抽象可以提供幫助,但是不能抽象出網絡故障。所以網絡編程通常關于預測和處理故障。 ## `async`函數 為了存儲重要信息,據了解烏鴉在鳥巢中復制它。 這樣,當一只鷹摧毀一個鳥巢時,信息不會丟失。 為了檢索它自己的存儲器中沒有的信息,鳥巢計算機可能會詢問網絡中其他隨機鳥巢,直到找到一個鳥巢計算機。 ```js 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`: ```js 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`被拒絕。 ```js findInStorage(bigOak, "events on 2017-12-21") .then(console.log); ``` 在異步函數內部,`await`這個詞可以放在表達式的前面,等待解`Promise`被解析,然后才能繼續執行函數。 這樣的函數不再像常規的 JavaScript 函數一樣,從頭到尾運行。 相反,它可以在有任何帶有`await`的地方凍結,并在稍后恢復。 對于有意義的異步代碼,這種標記通常比直接使用`Promise`更方便。即使你需要做一些不適合同步模型的東西,比如同時執行多個動作,也很容易將`await`和直接使用`Promise`結合起來。 ## 生成器 函數暫停然后再次恢復的能力,不是異步函數所獨有的。 JavaScript 也有一個稱為生成器函數的特性。 這些都是相似的,但沒有`Promise`。 當用`function*`定義一個函數(在函數后面加星號)時,它就成為一個生成器。 當你調用一個生成器時,它將返回一個迭代器,我們在第 6 章已經看到了它。 ```js 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 章的練習): ```js 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`處理程序不會在堆棧中。 ```js try { setTimeout(() => { throw new Error("Woosh"); }, 20); } catch (_) { // This will not run console.log("Caught!"); } ``` 無論事件發生多么緊密(例如超時或傳入請求),JavaScript 環境一次只能運行一個程序。 你可以把它看作在程序周圍運行一個大循環,稱為事件循環。 當沒有什么可以做的時候,那個循環就會停止。 但隨著事件來臨,它們被添加到隊列中,并且它們的代碼被逐個執行。 由于沒有兩件事同時運行,運行緩慢的代碼可能會延遲其他事件的處理。 這個例子設置了一個超時,但是之后占用時間,直到超時的預定時間點,導致超時延遲。 ```js 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`,等待它會導致你的回調在當前腳本完成后運行,而不是立即執行。 ```js Promise.resolve("Done").then(console.log); console.log("Me first!"); // → Me first! // → Done ``` 在后面的章節中,我們將看到在事件循環中運行的,各種其他類型的事件。 ## 異步的 bug 當你的程序同步運行時,除了那些程序本身所做的外,沒有發生任何狀態變化。 對于異步程序,這是不同的 - 它們在執行期間可能會有空白,這個時候其他代碼可以運行。 我們來看一個例子。 我們烏鴉的愛好之一是計算整個村莊每年孵化的雛雞數量。 鳥巢將這一數量存儲在他們的存儲器中。 下面的代碼嘗試枚舉給定年份的所有鳥巢的計數。 ```js 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`。 但它有嚴重問題。 它總是只返回一行輸出,列出響應最慢的鳥巢。 ```js chicks(bigOak, 2017).then(console.log); ``` 你能解釋為什么嗎? 問題在于`+=`操作符,它在語句開始執行時接受`list`的當前值,然后當`await`結束時,將`list`綁定設為該值加上新增的字符串。 但是在語句開始執行的時間和它完成的時間之間存在一個異步間隔。 `map`表達式在任何內容添加到列表之前運行,因此每個`+ =`操作符都以一個空字符串開始,并在存儲檢索完成時結束,將`list`設置為單行列表 - 向空字符串添加那行的結果。 通過從映射的`Promise`中返回行,并對`Promise.all`的結果調用`join`,可以輕松避免這種情況,而不是通過更改綁定來構建列表。 像往常一樣,計算新值比改變現有值的錯誤更少。 ```js 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`。 在兩個版本中,請求故障是否正確顯示為拒絕? 如何實現? ```js 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`的故障的方式。 ```js 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); } }); ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看