本文是我翻譯《JavaScript Concurrency》書籍的第二章 JavaScript運行模型,該書主要以Promises、Generator、Web workers等技術來說解JavaScript併發編程方面的實踐。javascript
完整書籍翻譯地址:https://github.com/yzsunlei/javascript_concurrency_translation 。因爲能力有限,確定存在翻譯不清楚甚至翻譯錯誤的地方,歡迎朋友們提issue指出,感謝。html
本書第一章咱們探討了JavaScript併發的一些狀況。通常來講,在JavaScript應用程序中處理併發只是一件小事。有不少想編寫併發JavaScript代碼的,提出的一些解決辦法並非很是規範的。有不少回調,而且用到的全部這些回調就足夠讓人發瘋了。咱們還看了下咱們編寫的併發JavaScript代碼如何改變現有的組件。Web workers已經開始成熟,javascript語言的併發結構纔剛剛引入。前端
JavaScript語言和運行時環境已是定下來的。咱們須要在設計層面考慮併發,而不是在編寫代碼時。併發應該是默認的。這提及來很容易,但很難作到。在本書中,咱們將探討JavaScript併發所提供的全部特性,以及咱們如何利用好它們做爲設計工具的優點。可是,咱們在這樣作以前,須要深刻了解JavaScript到底是怎樣運行的。這些是設計併發應用程序的必要知識,由於咱們須要確切地知道在選擇一種併發機制時會發生什麼。java
在本章中,咱們將從瀏覽器環境開始,看看代碼運行所涉及的全部子系統 - 例如JavaScript解釋器,任務隊列和DOM自己。而後咱們將介紹一些代碼示例,這些代碼將揭示運行咱們的代碼時真正發生的事情。最後咱們將經過討論在這個模型中面臨的挑戰來結束本章。node
當咱們訪問網頁時,會在瀏覽器中爲咱們建立整個環境。這個環境有幾個子系統,咱們瀏覽的網頁外觀和行爲都應該遵循萬維網聯盟(W3C)規範。任務是Web瀏覽器中的一個基本抽象。任何發生的事情要麼是一個任務自己,要麼是較大任務的一部分。git
若是您正在閱讀任何W3C規範,他們使用術語「用戶代理」而不是「Web瀏覽器」。在99.9%的狀況下,咱們正在閱讀的內容
符合主流的瀏覽器提供商。程序員
在本節中,咱們將介紹這些環境的主要組件,以及任務隊列和事件循環如何在這些組件之間進行通訊,以實現網頁的總體外觀和交互行爲。github
這裏先介紹一些術語,它們將在本章的各個部分進行講解:編程
• 執行環境:每當打開新網頁時,都會建立一個容器。這是一個豐富複雜的環境,它擁有咱們的JavaScript代碼將與之交互的一切。它也能夠做爲沙箱 - 咱們的JavaScript代碼沒法訪問環境以外的東西。json
• JavaScript解釋器:這是負責解析和執行JavaScript源代碼的組件。瀏覽器的工做是使用全局變量來擴充解釋器,例如window和XMLHttpRequest。
• 任務隊列:只要發生一些事情,就會有任務排隊。一個執行環境至少有一個這樣的隊列,但一般它有幾個隊列。
• 事件循環:執行環境具備單一的事件循環,負責爲全部任務隊列提供服務。只有一個事件循環,由於只有一個線程。
Web瀏覽器中建立的執行環境以下示圖。任務隊列是瀏覽器中發生的任何事情的入口。例如,一個任務能夠用於經過將腳本傳遞給JavaScript解釋器來執行腳本,而另外一個任務用於渲染DOM更新。如今咱們將深刻探究這個環境的組成部分。
也許Web瀏覽器執行環境中最具啓發性的方面是咱們的JavaScript代碼相對於執行它的解釋器所佔的部分要小。咱們的代碼能夠看做只是大機器中的一個齒輪。在這些環境中確定會發生不少事情,由於瀏覽器實現的平臺有大量的用途。這不只僅包括在屏幕上渲染元素,或是使用樣式屬性加強這些元素。DOM自己相似於微平臺,就像網絡設施,文件訪問,安全性等同樣。全部這些部分對於網站運行的網絡環境以及相關的應用程序都相當重要。
在併發環境中,咱們最感興趣的是將全部這些組件組合在一塊兒運行的機制。咱們的應用程序主要用JavaScript編寫,解釋器知道如何解析和運行它。可是,這最終如何轉化爲頁面上的視覺變化?瀏覽器的網絡組件如何知道發出HTTP請求,以及響應完成後如何調用JavaScript解釋器?
這些不一樣組件之間的協調限制了咱們在JavaScript中的使用併發。這些限制是必要的,由於沒有它們,開發Web應用程序將變得至關複雜。
一旦執行環境準備好了,事件循環就是首先要啓動運行的組件之一。它的工做是爲環境中的一個或多個任務隊列提供服務。瀏覽器提供商能夠根據須要自由實現隊列,但必須至少有一個隊列。若是他們願意的話,瀏覽器能夠將每一個任務放在一個隊列中,以同等的優先級執行每項任務。這樣作的問題意味着若是隊列被堆積,那麼有些必須優先執行的任務(例如鼠標或鍵盤事件)就會出現問題了。
在實踐中,有幾個隊列是有意義的,若是沒有其餘緣由,除了按優先級分隔任務。這一點很重要,由於只有一個控制線程 - 意味着只有一個CPU - 來處理這些隊列。如下是經過不一樣級別的優先級爲幾個隊列提供服務的事件循環:
即便事件循環與執行環境一塊兒啓動,這並不意味着它老是要處理它的任務。若是老是要處理任務,那麼實際應用程序永遠不會有任何CPU空閒時間。事件循環將等待更多任務,優先級高的隊列首先獲得服務。例如,使用前面這張圖中應用的隊列,將始終首先爲交互隊列提供服務。即便事件循環正在處理渲染隊列任務,若是交互式任務排隊,事件循環將在處理渲染任務以前恢復處理此任務。
任務隊列的概念對於理解Web瀏覽器的工做方式相當重要。瀏覽器這個術語其實是有誤導性的。咱們在早期的一些網站中使用它們瀏覽靜態網頁。如今,大型複雜的應用程序在瀏覽器中運行 - 它實際上更像是一個Web平臺。爲它們提供服務的任務隊列和事件循環多是處理這麼多組件的最佳設計。
咱們在本章前面看到,從執行環境的角度來看,JavaScript解釋器以及它解析和運行的代碼實際上只是一個黑盒子。事實上,調用解釋器自己就是一項任務,並且反映了JavaScript的運行直到完成的特性。許多任務涉及JavaScript解釋器的調用,以下所示:
這些事件中的任何一個 - 用戶單擊元素,頁面中加載的腳本或來自先前API調用的數據返回瀏覽器 - 建立調用JavaScript解釋器的任務。它告訴解釋器運行一段特定的代碼,而且它將繼續運行直到完成。這是JavaScript的運行直到完成的特性。接下來,咱們將深刻探究這些任務建立的執行上下文。
如今是時候看看JavaScript解釋器自己 - 這是當事件發生而且代碼須要運行時從其餘瀏覽器組件接管的組件。在解釋器中,咱們會找到一堆上下文,但總有一個活躍的JavaScript上下文。這與堆棧控制活動上下文的許多編程語言相似。
將活動上下文視爲咱們JavaScript代碼中正在發生的事件的快照。使用堆棧結構是由於活動上下文能夠被隨時更改成其餘內容,例如調用函數時。發生這種狀況時,會將新快照推送到堆棧,成爲活動上下文。當它運行完成時,它會從堆棧中彈出,將下一個上下文保留爲活動上下文。
在本節中,咱們將瞭解JavaScript解釋器如何處理上下文切換,以及管理上下文堆棧的內部任務隊列。
JavaScript解釋器中的上下文堆棧不是靜態結構 - 它在不斷變化。在這個堆棧的整個生命週期中發生了兩件重要的事情。首先,在堆棧的頂部,咱們有活動的上下文。這是解釋器在其指令中移動時當前執行的代碼。這裏有張示圖說明JavaScript執行上下文堆棧的概念,活動上下文始終位於頂部:
調用堆棧的另外一個重要事情是當活動上下文停用時爲其記錄狀態。例如,假設在幾條語句以後,func1()調用func2()。此時,在調用func2()以後,直接將上下文添加到該位置。而後,它被替換爲新的活動上下文 - func2()。完成後,重複該過程,func1()再次成爲活動上下文。
這種上下文切換髮生在咱們的整個代碼執行過程當中。例如,有一個全局上下文,它是咱們代碼執行的入口,函數自己具備本身的上下文。最近JavaScript還有一些新增的語言特性,它們也有本身的上下文,如模塊和生成器。接下來,咱們將看看負責建立新執行上下文的任務隊列。
工做隊列相似於咱們以前查看的任務隊列。不一樣之處在於工做隊列特定於JavaScript解釋器。也就是說,它們被封裝在解釋器中 - 瀏覽器不直接與這些隊列交互。可是,當瀏覽器調用解釋器時,例如,響應於加載的腳本或事件回調任務時,解釋器將建立新的工做。
JavaScript解釋器中的工做隊列實際上比用於協調全部Web瀏覽器組件的任務隊列簡單得多。只有兩個必要的隊列。一個用於建立新的執行上下文堆棧(調用堆棧)。另外一個特定於promise解析回調函數。
咱們將在下一章深刻探討promise解析回調的工做原理。
鑑於這些內部JavaScript工做隊列的職責限制,有些人可能得出結論:它們是沒必要要的 - 過分設計的行爲。事實並不是如此,由於雖然今天在這些工做中發現的它們職責有限,可是工做隊列設計讓語言更容易地擴展和改進。特別是,在考慮將來語言版本中的新併發結構時,工做隊列機制是頗有意義的。
到目前爲止,在本章中,咱們已經瞭解了Web瀏覽器環境的全部內部組件,以及JavaScript解釋器在此環境中的位置。全部這些與將併發原則應用於咱們的代碼有什麼關係?經過了解底層發生的事情,咱們能夠更深刻地弄明白運行代碼塊時發生的狀況。特別是,咱們知道相對於其餘代碼塊發生了什麼; 時間排序是一個相當重要的併發屬性。
這就是說,讓咱們實際寫一些代碼。在本節中,咱們將使用定時器將任務顯式添加到任務隊列。咱們還將瞭解JavaScript解釋器什麼時候何地跳轉並開始執行咱們的代碼。
所述的setTimeout()函數是在任何JavaScript代碼定住。它用於在未來的某個時刻執行代碼。JavaScript新手常常被setTimeout()函數弄迷糊,由於它是一個定時器。設定在將來的某個時間點,好比說3秒後,將調用回調函數。當咱們調用setTimeout()時,咱們將得到一個timer ID值,稍後可使用clearTimeout()清除它。如下是setTimeout()的基本用法:
//建立一個能夠調用咱們函數的定時器好比300ms。 //咱們可使用console.time()和console.timeEnd()函數看到它實際須要多長時間。 // //這一般是301ms左右,根本不是用戶能夠注意到的, //但調度函數調用獲得的準確性並不可靠。 var timer = setTimeout(() => { console.timeEnd('setTimeout'); }, 300); console.time('setTimeout');
這是JavaScript新手經常誤解的部分;這個定時器只能儘可能保證時間準確性。咱們使用setTimeout()時惟一的保證是咱們的回調函數永遠不會比咱們傳遞它的時間更早的被調用。所以,若是咱們說在300毫秒內調用此函數,它將永遠不會在275毫秒內調用它。一旦300毫秒過去,新任務就會排隊。若是在此任務以前沒有任何排隊等待,則回調會按時運行。即便有一些事情在它前面的隊列,其實也不容易被察覺 - 它彷佛在正確的時間運行。
但正如咱們所見,JavaScript是單線程運行的。這意味着一旦JavaScript解釋器啓動,它就不會中止直到它完成; 即便有任務等待定時器事件回調。所以,即便咱們要求定時器在300毫秒時執行回調,它徹底有可能會在500毫秒時執行。咱們來看一個例子來看看爲何它是可能的:
//注意,這個函數會消耗CPU ... function expensive(n = 25000) { var i = 0; while(++ i <n * n) {} return i; } //建立一個定時器,回調使用console.timeEnd()看看咱們等了多久。 //是不是真的等了咱們期待的300ms。 var timer = setTimeout(() => { console.timeEnd('setTimeout'); }, 300); console.time('setTimeout'); //這須要幾秒鐘的時間在CPU上完成。 //同時任務已排隊等待運行咱們的回調函數, //但事件循環沒法得到到那個任務隊列,直到expensive()完成。 expensive();
setInterval()函數是setTimeout()函數的姐妹。正如其名,它接受一個回調函數,以固定的時間間隔進行調用執行。事實上,setInterval()函數採用了和setTimeout()徹底相同的參數。惟一的區別是在於它會不斷的調用執行回調函數的功能,每隔X毫秒,直到該計時器被使用clearInterval()函數清除。
當咱們想要一遍又一遍地調用相同的函數時,這個函數很實用。例如,若是咱們輪詢API接口,則setInterval()是一個很好的候選解決方案。可是,請記住,回調函數的調用是固定的。也就是說,一旦咱們用1000毫秒調用setInterval(),沒有清除定時器就沒有改變1000毫秒。對於間隔須要是動態的場景,使用setTimeout()能夠更好的實現。回調函數中設定下一個間隔,容許間隔是動態的。例如,經過增長間隔時間來不斷地輪詢API。
在咱們上次查看的setTimeout()示例中,咱們看到了運行JavaScript代碼如何破環事件循環。也就是說,它阻止事件循環使用咱們的回調函數來調用JavaScript解釋器的任務。這容許咱們將代碼執行推遲到未來的某個點,但沒有準確的保證。讓咱們看看當咱們使用setInterval()計劃任務時會發生什麼。還有一些後續運行的JavaScript代碼塊:
//一個跟蹤咱們正在進行第幾回執行的計數器。 var cnt = 0; //設置interval定時器。回調會記錄調度回調函數的次數。 var timer = setInterval(() => { console.log('Interval', ++cnt); }, 3000); //阻塞CPU一段時間。當咱們再也不阻塞CPU時,調用第一個interval, //如預期的那樣。而後第二個,若是預料到的話。依次類推 //所以,當咱們阻止回調任務時,咱們就是阻止執行下一個間隔的任務。 expensive(50000);
在上一節中,咱們瞭解瞭如何延時運行JavaScript代碼。這是由其餘JavaScript代碼明確完成的。大多數狀況下,咱們的代碼會響應用戶交互而直接運行。在本節中,咱們將介紹一些公共接口,不只由DOM事件使用的,還包括網絡事件和Web worker事件等。咱們還將研究一種處理大量相似事件的技術 - 稱爲去抖。
事件對象接口被許多瀏覽器組件所使用,包括DOM元素。這是咱們如何分發事件到元素以及監聽到的事件和執行一個回調函數做爲響應。它其實是一個很是簡單的交互,很容易被捕捉到。這是相當重要的,由於許多不一樣類型的組件使用相同的接口進行事件管理。咱們將會經過這本書進一步看到。
上一節中使用的定時器的回調函數與執行EventTarget事件是相同的任務隊列機制。若是事件被觸發,一個使用對應的回調函數調用JavaScript解釋器的任務將被加入任務隊列。在這裏使用setTimeout()所面臨的限制一樣會出現。下面是當長時間運行的JavaScript代碼阻塞用戶事件時的任務隊列的示圖:
除了將偵聽器函數附加到對用戶交互作出反應的事件目標上以外,咱們還能夠手動觸發這些事件,以下代碼所示:
//通用事件回調,記錄事件時間戳。 function onClick(e) { console.log('click', new Date(e.timeStamp)); } //咱們將要用做事件的元素目標對象。 var button = document.querySelector('button'); //設置咱們的 onClick 函數做爲此目標上 click 事件的事件偵聽器。 button.addEventListener('click', onClick); //除了用戶點擊按鈕外,還有EventTarget接口讓咱們手動調度事件 button.dispatchEvent(new Event('click'));
最好是儘量命名一下回調中使用的函數。這樣,當咱們的代碼出錯時,跟蹤查找問題就容易得多。使用匿名函數並非不能夠,它只是在追蹤問題時會耗費更多的時間。另外一方面,箭頭函數更簡潔,而且具備更大的綁定靈活性。選擇使用它是明智的。
用戶交互事件的一個挑戰是在很短的時間內可能有不少這樣的事件。例如,當用戶在屏幕上移動鼠標時,會觸發數百個事件。若是咱們有監聽這些事件,任務隊列將很快被填滿,用戶體驗也就將會很糟糕。
即便咱們確實建立有高頻事件(例如鼠標移動)的事件監聽器,咱們也不必響應全部這些事件。例如,若是在1-2秒內發生了150次鼠標移動事件,咱們只關心最後一次移動 - 鼠標指針的最近位置。也就是說,使用咱們的事件回調代碼調用JavaScript解釋器的次數比須要的多149倍。
爲了處理這種高頻事件場景,咱們可使用一種稱爲去抖的技術。去抖函數意味着若是在給定時間範圍內連續屢次調用它,則實際僅使用最後一個調用,並忽略全部先前的調用。讓咱們來看看下面例子是如何實現的:
//跟蹤「mousemove」事件的數量。 var events = 0; //debounce()將提供的 func 來限制調用它的頻率。 function debounce(func, limit) { var timer; return function debounced(...args) { //移除全部現有的計時器 clearTimeout(timer); //在「limit」毫秒後調用函數 timer = setTimeout(() => { timer = null; func.apply(this, args); }, limit); }; } //記錄有關鼠標事件的一些信息, 並記錄事件總數。 function onMouseMove(e) { console.log(`X ${e.clientX} Y ${e.clientY}`); console.log('events', ++events); } //將輸入的內容記錄到文本輸入中 function onInput(e) { console.log('input', e.target.value); } //使用debounced監聽 mousemove 事件 //onMouseMove函數的版本。要是咱們 //沒有使用debounce()包裝此回調。 window.addEventListener('mousemove', debounce(onMouseMove, 300)); //使用去抖動版本監聽 input 事件 //onInput()函數,以防止每次按鍵觸發事件。 document.querySelector('input').addEventListener('input', debounce(onInput, 250));
使用去抖技術來避免給CPU帶來不少不必的工做量。經過忽略149個事件,咱們保存了正確的值,不然大量執行CPU指令而且獲得的不是正確值。咱們還節省了在這些事件處理程序中可能發生的各類類型的內存分配。
JavaScript的併發原則在「第一章,JavaScript併發簡介?」結尾時已經講過了,本書後面部分將經過代碼示例來講明它。
前端應用程序的另外一個重要部分是網絡交互,獲取數據,發出命令等。因爲網絡通訊本質上是異步進行的,所以咱們必須依賴事件 - EventTarget接口來確保準確性。
咱們首先看一下通用機制,它將咱們的回調函數與請求掛起並從後端獲取響應數據。而後,咱們將看看如未嘗試同步多個網絡請求建立一個看似不太可能的併發場景。
爲了與網絡進行交互,咱們建立了一個XMLHttpRequest的實例。而後咱們告訴它咱們要作的請求類型 - GET或POST和請求接口。這些請求對象還實現了EventTarget接口,以便咱們能夠監遵從網絡返回的數據。如下是此代碼的示例:
//回調成功的網絡請求,解析JSON數據 function onLoad(e) { console.log('load', JSON.parse(this.responseText)); } //回調失敗的網絡請求,記錄錯誤信息 function onError() { console.error('network', this.statusText || '未知錯誤'); } //回調已取消的網絡請求,記錄警告信息 function onAbort() { console.warn('request aborted ...'); } var request = new XMLHttpRequest(); //針對每種狀況,使用 EventTarget 綁定不一樣的事件監聽 request.addEventListener('load', onLoad); request.addEventListener('error', onError); request.addEventListener('abort', onAbort); //發送 api.json接口 的 GET 請求。 request.open('get', 'api.json'); request.send();
咱們能夠在這裏看到網絡請求有許多可能的狀態。成功狀態是服務器響應咱們須要的數據,而且咱們可以將其解析爲JSON。錯誤狀態是出現問題時,可能服務器沒法訪問。咱們在這裏關注的最後的一個狀態是請求被取消或停止。這意味着咱們再也不關心成功狀態,由於咱們的應用程序中的某些內容在請求執行時發生了變化。例如,用戶跳轉到其它地方。
雖然以前的代碼很容易使用和理解,但狀況並不是老是如此。咱們如今看到的只是單個請求和一些回調。而咱們的應用程序不多由單個網絡請求組成的。
在上一節中,咱們看到了與XMLHttpRequest實例的基本交互與發出網絡請求的例子。當有多個請求時,挑戰就來了。大多數狀況下,咱們會發出多個網絡請求,以便咱們獲得渲染UI組件所需的全部數據。而來自後端的響應將在不一樣時間返回,而且還可能彼此依賴。
無論怎樣,咱們須要將這些異步網絡請求的響應同步化。讓咱們看看如何使用EventTaget回調函數來完成這項工做:
//獲得響應時調用的函數,它還負責協調響應 function onLoad() { //當響應準備就緒時,咱們將解析的響應添加到 responses 數組 //以便後面的請求返回時咱們可使用其餘的響應數據 responses.push(JSON.parse(this.responseText)); //是否出現了全部期待的響應? if(responses.length === 3) { //咱們如何按順序作任何咱們須要的事情, //由於咱們須要全部數據來渲染UI組件 for(let response of responses) { console.log('hello', response.hello); } } } //建立咱們的API請求實例和 responses數組用於保存不一樣步的響應結果 var req1 = new XMLHttpRequest(), req2 = new XMLHttpRequest(), req3 = new XMLHttpRequest(), responses = []; //發出咱們全部須要的網絡請求 for(let req of [req1, req2, req3]) { req.addEventListener('load', onLoad); req.open('get', 'api.json'); req.send(); }
當有多個請求時,須要考慮不少額外的問題。因爲它們都在不一樣的時間返回,咱們須要將解析後的響應存儲在一個數組中,隨着每一個響應的返回,咱們須要檢查是否有咱們指望的一切。這個簡化的示例甚至沒有考慮失敗或取消的請求。正如此代碼所表示的那樣,同步的回調函數方法是有限的。在接下來的章節中,咱們將學習如何克服這一侷限。
咱們在本章中討論這個執行模型對JavaScript併發帶來的挑戰。有兩個基本問題。第一個問題是任何運行的JavaScript代碼都會阻止其餘任何事情的發生。第二個問題是嘗試使用回調函數完成異步操做,會致使回調地獄。
過去,JavaScript中缺少並行性並非真正的問題。沒有人注意它,由於JavaScript僅被視爲HTML頁面的漸進加強工具。當前端開始承擔更多責任時,這種狀況發生了變化。目前,大多數應用程序邏輯處理實際上都放在前端。這容許後端組件專一於JavaScript沒法解決的問題(從瀏覽器的角度來看,NodeJS徹底是咱們將在本書後面討論的另外一個問題)。
例如,後端能夠實現將API數據映射和轉換爲某種特殊的形式。這意味着前端JavaScript代碼只須要查詢此接口。問題是這個API接口是爲某些特定的UI功能而建立的,而不是咱們數據模型的必須實現的。若是咱們能夠在前端執行這些任務,咱們會將UI功能和所需的數據轉換緊密結合在一塊兒。這樣能夠減輕後端工做量,專一於複製和負載平衡等更重要的事情上。
咱們能夠在前端執行這些類型的數據轉換,但它們會嚴重破壞接口的可用性。這主要是因爲全部模塊須要相同的計算資源。換句話說,這個模型使咱們沒法實現併發原則並利用多個資源。咱們將在Web workers的幫助下克服這個Web瀏覽器限制,這將在後面的章節中介紹。
經過回調進行同步很難實現,而且不能很好地擴展。回調地獄,這是在JavaScript編程中一個流行的術語。毋庸置疑,經過代碼中的回調進行無休止的同步會產生問題。咱們常常須要建立某種狀態跟蹤機制,例如全局變量。當出現問題時,回調函數的嵌套在總體上遍歷是很是耗時的。
通常來講,同步多個異步操做的回調方法須要大量開銷。也就是說,用於處理異步操做的代碼存在不少重複的。同步併發原則是編寫併發代碼,而不是將主要目標嵌入同步處理邏輯的迷宮中。Promise經過減小回調函數的使用,幫助咱們在整個應用程序中一致地編寫併發代碼。
本章的重點是Web瀏覽器平臺以及JavaScript在其中的地位。每當咱們瀏覽網頁並與網頁交互時,都會觸發不少事件。這些做爲任務處理,從隊列中獲取。其中一個任務是調用帶有運行代碼的JavaScript解釋器。
當JavaScript解釋器運行時,它包含執行上下文堆棧。函數,模塊和全局腳本代碼 - 這些都是JavaScript執行上下文的示例。解釋器也有本身的內部工做隊列; 一個用於建立新的執行上下文堆棧,另外一個用於調用promise解析回調函數。
咱們編寫了一些使用setTimeout()函數手動建立任務的代碼,並演示了長時間運行的JavaScript代碼對於這些任務的影響。而後咱們查看了EventTarget接口,用於監聽DOM事件和網絡請求,以及咱們在本章中未討論的其餘內容,如Web workers和文件讀取。
咱們貫穿本章的是JavaScript程序員在使用這個模型時所面臨的一些挑戰。特別是,很難遵照咱們的JavaScript併發原則。咱們不使用並行,並試圖只使用同步,但回調倒是一個噩夢。
在下一章中,咱們將介紹一種使用promises進行同步的新思路。這將使咱們可以認真開始設計和構建併發JavaScript應用程序。
另外還有講解兩章nodeJs後端併發方面的,和一章項目實戰方面的,這裏就再也不貼了,有興趣可轉向https://github.com/yzsunlei/javascript_concurrency_translation查看。