本文是我翻譯《JavaScript Concurrency》書籍的第一章 JavaScript併發簡介,該書主要以Promises、Generator、Web workers等技術來說解JavaScript併發編程方面的實踐。javascript
完整書籍翻譯地址:github.com/yzsunlei/ja… 。因爲能力有限,確定存在翻譯不清楚甚至翻譯錯誤的地方,歡迎朋友們提issue指出,感謝。前端
JavaScript並非一門與併發有關聯的語言。事實上,它的特性還與併發應用是徹底不相符的。近幾年來,它已經改變了不少,特別是ES2015新的語言特性。Promises已經在JavaScript中運用了好幾年了; 只是如今,它成爲JavaScript語言的 一種原生類型。java
Generators是JavaScript的另外一個特性,它改變了咱們對JavaScript語言中併發的思考方式。Web workers也已經被瀏覽器支持好幾年了,然而,咱們卻運用的並很少。也許,是由於它與併發關係不大,並且更多的緣由是它在咱們的應用程序中關於併發扮演的角色的理解。node
本章的目標是探討一些通用的併發思想,從併發是什麼開始講起。若是您在工做或學習中沒有任何的併發編程經驗,那很好,本章對您來講是一個很不錯的起點。若是您之前使用JavaScript或其餘語言完成過併發編程相關的項目,那能夠將本章做爲複習,並使用JavaScript來回顧下。git
咱們將以一些重要的併發原則來貫穿本章。這些原則是有價值的編程工具,咱們應該在編寫併發代碼時牢記在腦海中。一旦咱們學會應用這些原則,它們會告訴咱們咱們的併發設計是否正確,或者須要退後一步,問問本身真正想要實現的目標。這些原則採用自上而下的方法來設計咱們的應用程序。這意味着它們從一開始就是適用的,甚至在咱們開始編寫代碼以前。在整本書中,咱們將引用這些原則,所以若是您只閱讀本章的一節,那最好是併發原則那部分。github
在咱們開始構建大規模併發JavaScript體系結構以前,讓咱們先將注意力轉移到咱們熟悉的、老舊的同步JavaScript代碼上。這些JavaScript代碼塊,它們做爲單擊事件的回調結果,或者做爲加載網頁的運行結果。一旦它們開始執行,它們就不會中止。也就是說,它們是一直運行到完成的。在接下來的章節中,咱們將進一步深刻研究它們。編程
咱們在整個章節中偶爾會看到術語「同步」和「串行」,它們可互換使用。它們都是指代一個接一個地運行代碼語句,直到沒有其餘代碼能夠運行。後端
儘管JavaScript被設計爲單線程,運行直到完成的,但Web的特性不得不使其複雜化。想一想Web瀏覽器及其全部可應用的模塊。有用於渲染用戶界面的文檔對象模型(DOM),有用於獲取遠程數據的XMLHttpRequest(XHR)對象。如今讓咱們先來看看JavaScript的同步特性和Web的異步特性。promise
當代碼是同步的,它很容易被理解。將咱們在屏幕上看到的指令集映射到頭腦中的有序步驟至關容易; 這樣作,而後那樣作;判斷一下,若是是,則執行此操做,依此類推。這種串行類型的代碼處理很容易理解,由於沒有什麼特別的,假想代碼的運行並不可怕。如下是一大塊同步代碼的示例:瀏覽器
相反的,併發編程並不容易理解。這是由於代碼在編輯器並不能線性的去追蹤。相反,咱們不斷跳轉,試圖映射這段代碼相對於那段代碼所作的事情。時間是並行設計的重要因素; 這是違背大腦以天然方式來理解代碼的東西。當咱們閱讀代碼時,咱們天然會在腦海中假想去執行它。這是咱們弄清楚它在作什麼的方式。
當代碼實際執行不符合咱們的假想時,這時就會很崩潰。一般狀況下,看代碼就像看一本書 - 而看併發代碼就像看一本雖然被編號,可是不按順序編號的書。咱們來看一些簡單的僞JavaScript代碼:
var collection = ['a', 'b', 'c', 'd'];
var results = [];
for (let item of collection) {
results.push(String.fromCharCode(item.charCodeAt(0)));
}
// ['b','c','d','e' ]
複製代碼
在傳統的多線程編程環境中,線程與線程之間是異步運行。咱們使用多線程來充分利用當今大多數系統中的多核CPU,從而得到更好的性能。可是,這須要付出一些代價的,由於它迫使咱們去從新思考代碼在運行時的執行方式。它再也不是一般的一步一步的去執行。一段代碼能夠與另外一個CPU中的其餘代碼一塊兒運行,也能夠在同一CPU上與其餘線程一塊兒運行。
當咱們將併發引入同步代碼時,不少簡易性就消失了 - 它就會是很燒腦的代碼。這就是咱們編寫併發代碼的緣由:提出併發的前期假設的代碼。隨着本書的進展,咱們將詳細闡述這一律念。使用JavaScript,併發設計很重要,由於這就是Web的工做方式。
JavaScript中的併發是一個很重要的方法,緣由是無論是從很是高的層次,仍是實現細節水平上來講,Web是一個併發的東西。換句話說,網絡是併發的,由於在任何一個時間點,都有大量的數據流過數英里的光纖,這些光纖包圍着全球。它與部署到Web瀏覽器的應用程序自己以及後端服務器如何處理一連串的數據請求有關。
讓咱們仔細看看瀏覽器以及在那裏發生的各類異步操做。當用戶加載網頁時,頁面執行的第一個操做就是下載和運行頁面JavaScript代碼。這自己就是一個異步操做,由於咱們的代碼在下載時,瀏覽器會繼續執行其餘操做,例如渲染頁面元素。
經過網絡傳輸的異步數據是應用程序數據自己。加載頁面並開始運行JavaScript代碼後,咱們須要爲用戶展現數據。這其實是咱們的代碼將要作的第一件事,以便用戶能夠儘快看到。一樣,當咱們等待這些數據返回時,JavaScript引擎會將咱們的代碼移動到它的下一組指令。對遠程數據的請求,在繼續執行代碼以前不會等待響應:
頁面元素所有渲染並填充數據後,用戶開始與咱們的頁面進行交互。這意味着事件被觸發 - 單擊元素將觸發click事件。發送這些事件的DOM環境是一個沙盒環境。這意味着在瀏覽器中,DOM是一個子系統,與JavaScript解釋器是分離的,後者運行咱們的代碼。這種分離使某些JavaScript併發方案很難進行。咱們將在下一章深刻介紹這些內容。
有了全部這些異步的來源,毫無疑問,咱們的頁面會因特殊的狀況處理而變得臃腫,以應對不可避免地出現的特殊狀況。異步思考是不符合邏輯的,所以這種類型的動態修補多是同步思考的結果。最好採用Web的異步特性。可是,同步網絡可能會致使令用戶沒法忍受的體驗。如今,讓咱們進一步瞭解咱們在JavaScript體系結構中可能遇到的併發類型。
JavaScript是一種運行直到完成的語言。儘管在運行上存在併發機制,但並無解決它。換句話說,咱們的JavaScript代碼不會在if語句中間轉而去控制另外一個線程。這很重要的緣由是咱們能夠選擇一個有助於咱們思考JavaScript併發的抽象層次。讓咱們看看在JavaScript代碼中併發操做的兩種類型。
異步操做的一個特徵是它們不會阻止其餘後續操做。異步操做並不必定意味着「一勞永逸」。相反,當那部分咱們等待的操做完成時,咱們會運行一個回調函數。這個回調函數與咱們的其餘代碼不一樣步; 所以,這被稱爲異步。
在Web前端中,常常從遠程服務器獲取數據。這些請求操做相對較慢,由於它們必須經過網絡鏈接。這些操做是異步的,由於咱們的代碼會等待一些數據返回以便觸發回調函數,這並不意味着用戶必須停下來等待。此外,用戶當前正在查看的任何頁面都不太可能僅依賴於一個遠程資源。所以,串行處理多個遠程數據請求會產生很是糟糕的用戶體驗。 如下是異步代碼的簡單示例:
var request = fetch('/ foo');
request.addEventListener((response) => {
//如今它已經返回了,可使用「response」作些事情了
});
//不要等待響應,當即更新DOM
updateUI();
複製代碼
下載示例代碼
您能夠從www.packtpub.com上的賬戶下載所購買的全部Packt Publishing書籍的示例代碼文件。 若是您在其餘地方購買了本書,能夠訪問www.packtpub.com/support並註冊以直接經過電子郵件發送給您。
咱們不只限於獲取遠程數據,而是將其做爲異步操做的一個案例。當咱們發出網絡請求時,這些異步控制流實際上會離開瀏覽器。可是,限制在瀏覽器中的異步操做呢?以setTimeout()函數爲例。它遵循與網絡請求使用同樣的回調模式。該函數已經過回調,將在稍後執行。然而,沒有任何東西離開瀏覽器。相反,該操做排在任何的其餘操做後面。這是由於異步操做仍然只是一個控制線程,由一個CPU執行。這意味着隨着咱們的應用程序在規模和複雜性方面的增加,咱們就會面臨併發擴展問題。可是,也許異步操做並不意味着只是解決單一CPU問題。
考慮在單個CPU上執行異步操做的更好方法多是想象一下雜技師拋球的場景。雜技師的大腦比做CPU,協調他的動做。被拋出的球是咱們操做的數據。咱們關心的只有兩個基本動做 - 拋球和接球:
因爲雜技師只有一個大腦,因此他不可能將本身的精力用於一次執行多項任務。然而,雜技師經驗豐富,而且知道他不須要分出一小部分精力用於投擲或捕捉動做。一旦球到空中,他能夠自由地將注意力轉移到即將降落的球上。
別人在看這個雜技師的動做時,覺得他全神貫注於全部拋出的六個球,而實際上,他在同一個時間點會忽視其餘五個在空中的球。
與異步同樣,並行容許控制流繼續而無需等待操做完成。與異步不一樣,並行要取決於硬件。這是由於咱們不能在單個CPU上並行運行兩個或更多個控制流程。然而,將並行與異步區分開來的主要是使用它的合理性方面。這兩種併發方式解決了不一樣的問題,而且須要不一樣的設計原則。
有時,咱們但願並行執行操做,不然若是同步執行則會耗費時間。想一想正在等待完成三項複雜操做的用戶。若是每一個操做都須要10秒鐘才能完成,那麼這意味着用戶必須等待30秒。若是咱們可以並行執行這些任務,咱們可使得總等待時間接近10秒。咱們以更少的成本得到更多,從而實現高效的用戶交互體驗。
這些都不是免費的。與異步操做同樣,並行操做會將回調做爲通訊機制。一般,設計並行很難,由於除了與worker線程進行通訊以外,咱們還要擔憂手頭的任務,也就是說,咱們但願經過使用worker線程來實現什麼?咱們如何將問題分解爲更小的操做?如下是咱們開始引入並行代碼的示例:
var worker = new Worker('worker.js');
var myElement = document.getElementById('myElement');
worker.addEventListener('message', (e) => {
myElement.textContent = 'Done working!';
});
myElement.addEventListener('click', (e) => {
worker.postMessage('work');
});
複製代碼
不要擔憂這段代碼運行時的機制,由於它們將在後面深刻討論。須要注意的是,當咱們將一些線程放入工做環境時,咱們會向已經混亂的環境添加更多回調。這就是爲何在咱們的代碼中須要併發設計,這是本書的主要話題,從「第5章,使用Web workers」開始。
讓咱們考慮下前一節中雜技師的比方。拋擲和捕獲動做由雜技師異步執行; 也就是說,他只有一個腦(CPU)。可是假設咱們周圍的環境在不斷變化。咱們指望的雜技動做愈來愈多,一個雜技師不可能所有完成:
解決方案是爲該表演中加入更多的雜技師。經過這種方式,咱們能夠添加更多的計算能力,在同一時刻執行屢次拋擲和捕獲操做。對於單個異步運行的雜技師來講,這是不可能的。
咱們尚未解決好問題,由於咱們不能只讓新添加的雜技師站在一個地方,並按照一個雜技師玩雜技的方式執行他們的動做。觀衆不少,更多樣化,都須要被逗樂。雜技師須要可以有不一樣的動做。他們須要在地板上不斷的四處移動以讓每個觀衆都能感受開心。他們甚至可能開始互相玩雜技。該由咱們來作一個可以實現這些雜技動做的設計。
既然咱們已經瞭解了併發的基礎知識,以及它在前端Web開發中的做用,那麼讓咱們看一下JavaScript開發的一些基本併發編程原則。這些原則僅僅是咱們在編寫併發JavaScript代碼時爲咱們的設計選擇提供信息的工具。
當咱們應用這些原則時,它們迫使咱們退後一步,在咱們推動實施以前提出適當的問題。特別的,是關於爲何和如何作的問題:
這是每一個併發原則的參考示圖,在開發過程當中相互依賴。有了這個,咱們將把注意力轉向每一個原則,以便進一步探究:
併發原則意味着利用現代CPU功能在更短的時間內計算結果。如今能夠在任何現代瀏覽器或NodeJS環境中使用。在瀏覽器中,咱們可使用Web workers實現真正的併發。在NodeJS中,咱們能夠經過生成新進程來實現真正的併發。從瀏覽器的角度來看,下圖這就是CPU的大體樣子:
因爲目標是在更短的時間內進行更多的計算,咱們如今必須問本身爲何要這樣作?除了性能自己很是酷的事實以外,還必須對用戶產生一些切實的影響。這個原則讓咱們看着咱們的並行代碼並想一想 - 用戶從中得到了什麼?答案是咱們可使用較大的數據集做爲輸入進行計算,而且不多可能因爲JavaScript長時間運行,給用戶帶來無響應的體驗。
重要的是仔細想一想併發的實際好處,由於當咱們這樣作時,咱們會增長代碼的複雜性,不然就沒多大意義了。所以,若是用戶看到相同的結果,獲得一樣的體驗,那不管咱們作什麼,併發原則可能都不適用。另外一方面,若是可擴展性很重要且數據集大小增長的可能性很大,那麼併發的代碼簡單性的折衷多是值得的。在考慮併發原則時,這裏有一個要遵循的檢查清單:
同步原則是有關用於協調併發操做和抽象這些機制的一些方式。回調函數是一個具備深遠根源的JavaScript概念。這是個很不錯的方式選擇,當咱們須要運行一些代碼,但咱們不但願立刻就運行它。咱們但願當一些條件符合時再運行它。往大的方面講,這種方式沒有什麼內在的問題。回調函數在單獨使用時,是一種很簡潔、方便、可讀性強的一種併發模式。但在大量使用回調,而且在回調之間存在有大量的依賴時,就很使人崩潰了。
Promise API是ECMAScript 6中引入的核心JavaScript語法,用於解決當前應用程序所面臨的同步問題。這是一個在實際使用回調時更簡單的API(是的,咱們正在與嵌套回調作鬥爭)。Promise的目的不是要消除回調,而是要移除沒必要要的回調。 如下是用於同步兩個網絡請求調用的Promise示例:
Promise的關鍵在於它們是一種通用的同步機制。這意味着它們不是專門針對網絡請求,Web workers或DOM事件而產生的。咱們必須使用promises包裝咱們的異步操做,並在必要時處理它們。這看起來不錯的緣由是依賴promise接口的調用者並不關心promise中的內容。顧名思義,Promise是在某個時刻完成的。這可能須要5秒或更快。數據能夠來自網絡資源或Web用戶。調用者並不關心,由於它假設併發,這意味着咱們能夠在不破壞應用程序的狀況下以任何方式實現它。這是上圖的修改版本,它將爲咱們提供實現promises的可能性:
當咱們學會用它來實現時,併發代碼忽然變得更加易於理解了。Promise和相似的機制可用於同步網絡請求,或僅僅是Web用戶事件。但它們真正有能力使用它們來編寫併發應用程序,其中默認是併發的。在考慮同步原則時,這裏有一個能夠參考的檢查清單:
保護原則是關於節省計算和內存資源。這是經過使用惰性計算技術完成的。惰性的名稱源於咱們在肯定咱們確實須要它以前不會實際計算新值的方法。想象一下渲染頁面元素的應用程序組件。咱們能夠傳遞此組件給它須要渲染的確切數據。這意味着在組件實際須要以前會進行屢次計算。它還意味着所使用的數據須要分配到內存中,以便咱們能夠將它傳遞給組件。這種方法並無錯。實際上,它是在JavaScript組件中傳遞數據的通用方法。
使用惰性計算的替代方法來實現相同的結果。不是計算要渲染的值,而是在要傳遞的結構中分配它們,咱們計算一項,而後渲染它。將此視爲一種合做的多任務,其中較大的操做被分解爲較小的任務,來回傳遞控制的焦點。
這是一種快速的計算數據方法,並將其傳遞給渲染UI元素的組件:
這種方法有兩個很差的地方。首先,轉換是預先進行的,這多是一項成本高昂的計算。若是組件發生了什麼問題,沒法以任何方式渲染它 - 因爲某種限制?而後咱們執行了這個計算來轉換不須要的數據。做爲必然結果,咱們爲轉換後的數據分配了一個新的數據結構,以便咱們能夠將它傳遞給咱們的組件。這種瞬時存儲的結構實際上並無用於任何目的,由於它會當即被垃圾收集。讓咱們來看看惰性方法是什麼樣子的:
使用惰性方法,咱們能夠刪除預先進行的成本昂貴的轉換計算。相反,咱們一次只轉換一項。咱們還可以刪除轉換後的數據結構前期分配的存儲空間。相反,只有轉換後的項將傳遞到組件中。而後,組件能夠請求另外一項或中止。保護原則是使用併發做爲僅計算所需內容,並僅分配所需內存的方法。
如下檢查清單將幫助咱們在編寫併發代碼時考慮保護原則:
在本章中,咱們介紹了JavaScript中併發的一些目標。雖然同步JavaScript易於維護和理解,但異步JavaScript代碼在Web上是不可避免的。所以,在編寫JavaScript應用程序時,將併發做爲默認的很是重要。
咱們感興趣的有兩種主要的併發類型 - 異步操做和並行操做。異步是關於操做在時間上排序,這給人一種事情都發生在同一時間的感受。若是沒有這種類型的併發,對用戶體驗會形成很大的影響,由於它會不斷地等待其餘操做完成。並行是另外一種類型的併發,解決了另外一個不一樣類型的問題,咱們但願經過更快地計算結果來提升性能。
最後,咱們研究了JavaScript併發編程中的三種原則。併發原則是利用現代系統中的多核CPU。同步原則是關於建立抽象機制,使咱們可以編寫併發代碼,從咱們的功能代碼中隱藏併發機制。保護原則使用惰性計算來僅計算所需內容並避免沒必要要的內存分配。
在下一章中,咱們將把注意力轉向JavaScript執行環境。爲了有效地使用JavaScript併發,咱們須要對代碼運行時實際發生的事情有充分的理解。
另外還有講解兩章nodeJs後端併發方面的,和一章項目實戰方面的,這裏就再也不貼了,有興趣可轉向github.com/yzsunlei/ja…查看。