JavaScript中異步編程問題能夠說是基礎中的重點,也是比較難理解的地方。首先要弄懂的是什麼叫異步?javascript
咱們的代碼在執行的時候是從上到下按順序執行,一段代碼執行了以後纔會執行下一段代碼,這種方式叫同步(synchronous)執行,也是咱們最容易理解的方式。可是在某些場景下:java
上面這些場景可能很是耗時,並且時間不定長,這時候這些代碼就不該該同步執行了,先執行能夠執行的代碼,在將來的某個時間再來執行他們的handler,這就是異步。node
經過這篇文章咱們來了解幾個知識點:git
先作些準備工做,補一補一些很是重要的前置的概念。es6
一個程序(program)至少包含一個進程(process),一個進程至少包含一個線程(thread)。github
進程有如下特色:web
線程有如下特色:面試
從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行。但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。 這就是進程和線程的重要區別。
畫張圖來簡單描述下:
全部的程序都要交給CPU實現計算任務,可是CPU一個時間點只能處理一個任務。這時若是多個程序在運行,就涉及到了《操做系統原理》中重要的線程調度算法,線程是CPU輪轉的最小單位,其餘上下文信息用所在進程中的。ajax
進程是資源的分配單位,線程是CPU在進程內切換的單位。
瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由如下常駐線程組成:算法
Javascript是單線程的,那麼爲何Javascript要是單線程的?
這是由於Javascript這門腳本語言誕生的使命所致:JavaScript爲處理頁面中用戶的交互,以及操做DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。若是JavaScript是多線程的方式來操做這些UI DOM,則可能出現UI操做的衝突; 若是Javascript是多線程的話,在多線程的交互下,處於UI中的DOM節點就可能成爲一個臨界資源,假設存在兩個線程同時操做一個DOM,一個負責修改一個負責刪除,那麼這個時候就須要瀏覽器來裁決如何生效哪一個線程的執行結果。固然咱們能夠經過鎖來解決上面的問題。但爲了不由於引入了鎖而帶來更大的複雜性,Javascript在最初就選擇了單線程執行。
這時候再理解阻塞非阻塞就好理解了,對於異步任務,單線程的JavaScript若是什麼也不幹等待異步任務結束,這種狀態就是阻塞的;若是將異步消息放到一邊,過會再處理,就是非阻塞的。
請求不能當即獲得應答,須要等待,那就是阻塞;不然能夠理解爲非阻塞。
生活中這種場景太常見了,上廁所排隊就是阻塞,沒人直接上就是非阻塞。
由於JavaScript是單線程的,每一個時刻都只能一個事件,因此JavaScript中的同步和異步事件就有了一個奇妙的執行順序。
JavaScript在運行時(runtime)會產生一個函數調用棧,先入棧的函數先被執行。可是有一些任務是不須要進入調用棧的,這些任務被加入到消息隊列中。當函數調用棧被清空時候,就會執行消息隊列中的任務(任務總會關聯一個函數,並加入到調用棧),依次執行直至全部任務被清空。因爲JavaScript是事件驅動,當用戶觸發事件JavaScript再次運行直至清空全部任務,這就是事件循環。
函數調用棧中的任務永遠優先執行,調用棧無任務時候,遍歷消息隊列中的任務。消息隊列中的任務關聯的函數(通常就是callback)放入調用棧中執行。
舉兩個例子:異步請求
function ajax (url, callback){ var req = new XMLHttpRequest(); req.onloadend = callback; req.open('GET', url, true); req.send(); }; console.log(1); ajax('/api/xxxx', function(res){ console.log(res); }); console.log(2);
一個開發常常遇到的業務場景,異步請求一個數據,上述過程用圖表示:
圖中三條線分別表示函數執行的調用棧,異步消息隊列,以及請求所依賴的網絡請求線程(瀏覽器自帶)。執行順序:
console.log(1);
。ajax
方法,方法裏面配置XMLHttpRequest
的回調函數,並交由線程執行異步請求。console.log(2);
。function(res){console.log(res);}
。定時器任務:
console.log(1); setTimeout(function(){ console.log(2); }, 100); setTimeout(function(){ console.log(3); }, 10); console.log(4); // 1 // 4 // 3 // 2
跟上面的例子很像,只不過異步請求變成了定時器,上述代碼的指向過程圖:
執行順序以下:
console.log(1);
。setTimeout
向消息隊列添加一個定時器任務1。setTimeout
向消息隊列添加一個定時器任務2。console.log(4);
。console.log(3);
。console.log(2);
。經過上面例子能夠很好理解,就像工做中你正在作一件事情,這時候領導給你安排一個不着急的任務,你停下來跟領導說'等我忙完手裏的活就去幹',而後把手裏的活幹完去幹領導安排的任務。全部任務完成至關於完成了一個事件循環。
macrotask 和 microtask 都是屬於上述的異步任務中的一種,分別是一下 API :
setTimeout
, setInterval
, setImmediate
, I/O, UI renderingprocess.nextTick
(node), Promises
, Object.observe
(廢棄), MutationObserver
setTimeout
的 macrotask ,和 Promise
的 microtask 有什麼不一樣呢:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // "script start" // "script end" // "promise1" // "promise2" // "setTimeout"
這裏的運行結果是Promise
的當即返回的異步任務會優先於setTimeout
延時爲0的任務執行。
緣由是任務隊列分爲 macrotasks 和 microtasks,而Promise
中的then
方法的函數會被推入 microtasks 隊列,而setTimeout
的任務會被推入 macrotasks 隊列。在每一次事件循環中,macrotask 只會提取一個執行,而 microtask 會一直提取,直到 microtasks 隊列清空。
因此上面實現循環的順序:
併發咱們應該常常聽過,跟他相似的一個詞叫並行。
併發:多個進程在一臺處理機上同時運行,一個時間段內處理多件事情,宏觀上比如一我的邊唱邊跳,微觀上這我的唱一句跳一步。(能夠類比時間片輪轉法,多個線程同時佔用一個CPU,外部看來能夠併發處理多個線程)
並行:多態擁有相同處理能力的處理機在同時處理不一樣的任務,比如廣場上多個大媽同時再調廣場舞。(多個CPU同時處理多個線程任務)
在JavaScript中,由於其是單線程的緣由,因此決定了其每時刻只能幹一件事情,事件循環是併發在JavaScript單線程中的一種處理方式。
可是在平常開發中咱們確定見過,同時發送多個請求。這種狀況下多個網絡線程和js線程共同佔用一個CPU,就是併發。
雖然已經理解了JavaScript中運行異步任務的過程,可是這樣顯然對開發不友好,由於咱們一般並不知道異步任務在什麼時候結束。因此前人開發了多種處理異步的方法。每種方法咱們都從三個角度考慮其優缺點:
一種最多見的處理異步問題的方法,將異步任務結束時候要乾的事情(回調函數)做爲參數傳給他,等任務結束時候運行回調函數。咱們經常使用的$.ajax()
和setTimeout
都屬於這種方式,可是這樣的問題很明顯:多個異步任務按順序執行很是恐怖。
// 著名的回調金字塔 asyncEvent1(()=>{ asyncEvent2(()=>{ asyncEvent3(()=>{ asyncEvent4(()=>{ .... }); }); }); });
上面這種狀況很是難以維護,在早期Node項目中常常出現這種狀況,有人對上面小改動:
function asyncEvent1CB (){ asyncEvent2(asyncEvent2CB); } function asyncEvent2CB (){ asyncEvent3(asyncEvent3CB); } function asyncEvent3CB (){ asyncEvent4(asyncEvent4CB); } function asyncEvent4CB () { // ... } asyncEvent1(asyncEvent1CB);
這樣講回調函數分離出來,邏輯清晰了一些,可是仍是很明顯:方法調用順序是硬編碼,耦合性仍是很高。並且一旦同時發送多個請求,這多個請求的回調函數執行順序很難保證,維護起來很是麻煩。
這就是回調函數的弊端:
雖然回調函數這種方式問題不少,可是不能否認的是在ES6以前,他就是處理異步問題廣泛較好的方式,並且後面不少方式仍然基於回調函數。
JavaScript是事件驅動,任務的執行不取決代碼的順序,而取決於某一個事件是否發生。DOM中有大量事件如onclick
,onload
,onerror
等等。
$('.element1').on('click', function(){ console.log(1); }); $('#element2').on('click', function(){ console.log(2); }); document.getElementById('#element3').addEventListener('click', function(){ console.log(3); }, false);
例如上面這段代碼 你沒法預知輸出結果,由於事件觸發沒法被預知。跟這個很像的還有訂閱者發佈者模式:
github上有個有意思的小demo。註冊在發佈者裏面的回調函數什麼時候被觸發取決於發佈者什麼時候發佈事件,這個不少時候也是不可預知的。
回調函數與事件監聽的區別:
不過事件監聽也存在問題:
promise出場了,當年理解promise花了我很多功夫。Promise確實跟前二者很不同,簡單說下promise。
promise更詳細的內容能夠看阮一峯老師的文章。
Promise對於異步處理已經十分友好,大多生產環境已經在使用,不過仍有些缺點:
中文翻譯成'生成器',ES6中提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。簡單來講,我能夠聲明一個生成器,生成器能夠在執行的時候暫停,交出函數執行權給其餘函數,而後其餘函數能夠在須要的時候讓該函數再次運行。這與以前的JavaScript聽起來徹底不一樣。
詳細的內容參考阮一峯老師的文章,這裏咱們來據幾個例子,正常的ajax調用寫法看起來以下:
// 使用setTimeout模擬異步 function ajax (url, cb){ setTimeout(function(){ cb('result'); }, 100); } ajax('/api/a', function(result){ console.log(result); }); // 'result'
一旦咱們想要多個異步按順序執行,簡直是噩夢。這裏使用generator處理異步函數利用了一個特色:調用next()
函數就會繼續執行下去,因此利用這個特色咱們處理異步原理:
yield
出去。next()
將生成器繼續進行下去。咱們對上面的例子加以改進:
// 使用setTimeout模擬異步 function ajax (url, cb){ setTimeout(function(){ cb(url + ' result.'); }, 100); } function ajaxCallback(result){ console.log(result); it.next(result); } function* ajaxGen (){ var aResult = yield ajax('/api/a', ajaxCallback); console.log('aResult: ' + aResult); var bResult = yield ajax('/api/b', ajaxCallback); console.log('bResult: ' + bResult); } var it = ajaxGen(); it.next(); // /api/a result. // aResult: /api/a result. // /api/b result. // bResult: /api/b result.
運行下上面代碼,能夠看到控制檯輸出結果竟然跟咱們書寫的順序同樣!咱們稍加改動:
// 使用setTimeout模擬異步 function ajax (url, cb){ setTimeout(function(){ cb(url + ' result.'); }, 100); } function run (generator) { var it = generator(ajaxCallback); function ajaxCallback(result){ console.log(result); it.next(result); } it.next(); }; run(function* (cb){ var aResult = yield ajax('/api/a', cb); console.log('aResult: ' + aResult); var bResult = yield ajax('/api/b', cb); console.log('bResult: ' + bResult); });
簡單幾下改造即可以生成一個自執行的生成器函數,同時也完成了異步場景同步化寫法。generator的核心在於:同步,異步,回調三者分離,遇到異步交出函數執行權,再利用回調控制程序生成器繼續進行。上面的run函數只是一個簡單的實現,業界已經有CO這樣成熟的工具。實際上開發過程當中一般使用generator搭配Promise實現,再來修改上面的例子:
// 使用setTimeout模擬異步 function ajax (url){ return new Promise(function(resolve, reject){ setTimeout(function(){ resolve(url + ' result.'); }, 100); }); } function run (generator) { var it = generator(); function next(result){ var result = it.next(result); if (result.done) return result.value; result.value.then(function(data){ console.log(data); next(data); }); } next(); }; run(function* (){ var aResult = yield ajax('/api/a'); console.log('aResult: ' + aResult); var bResult = yield ajax('/api/b'); console.log('bResult: ' + bResult); });
使用Promise來代替callback,理解上花費點時間,大大提升了效率。上面是一種常見,以前我用過generator實現多張圖片併發上傳,這種狀況下利用generator控制上傳上傳數量,達到斷斷續續上傳的效果。
進化到generator這一步能夠說是至關智能了,不管是單個異步,多個按順序異步,併發異步處理都十分友好,可是也有幾個問題:
有沒有更簡便的方法?
理解了上面的generator,再來理解async/await就簡單多了。
ES2017 標準引入了 async 函數,使得異步操做變得更加方便。async 函數是什麼?一句話,它就是 Generator 函數的語法糖。
再看一遍上面的例子,而後修改上面的例子用async/await:
// 使用setTimeout模擬異步 function ajax (url){ return new Promise(function(resolve, reject){ setTimeout(function(){ console.log(url + ' result.'); resolve(url + ' result.'); }, 100); }); } async function ajaxAsync () { var aResult = await ajax('/api/a'); console.log('aResult: ' + aResult); var bResult = await ajax('/api/b'); console.log('bResult: ' + bResult); } ajaxAsync();
能夠明顯的看到,async/await寫法跟generator最後一個例子很像,基本上就是使用async/await關鍵字封裝了一個自執行的run方法。
async
函數對 Generator 函數的改進,體如今如下四點。
- 內置執行器:Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍,只要一行。
- 更好的語義:
async
和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。- 更廣的適用性:
co
模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。- 返回值是 Promise:
async
函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then方法指定下一步的操做。
這裏async/await不作深刻介紹,詳情移步阮一峯老師的博客。
一個很不經常使用的api,可是是一個異步編程的方法,跟以上幾種又不太同樣。
你可能會遇到一個很是耗時的計算任務,若是在js線程裏運行會形成頁面卡頓,這時使用web worker,將計算任務丟到裏面去,等計算完成再以事件監聽的方式通知主線程處理,這是一個web work的應用場景。在這時候,瀏覽器中是有多個線程在處理js的,worker同時能夠在建立子線程,實現js'多線程'。web worker的文檔。實戰的話看這篇。
與前面幾種方法不一樣的是,咱們絞盡腦汁想把異步事件同步化,可是web worker卻反其道而行,將同步的代碼放到異步的線程中。
目前,web worker一般用於頁面優化的一種手段,使用場景:
<canvas>
或者<video>
元素中獲取的數據,能夠把圖像分割成幾個不一樣的區域而且把它們推送給並行的不一樣Workers來作計算。JavaScript中的異步編程方式目前來講大體這些,其中回調函數這種方式是最簡單最多見的,Promise是目前最受歡迎的方式。前四種方式讓異步編碼模式使咱們可以編寫更高效的代碼,而最後一種web worker則讓性能更優。這裏主要是對異步編程流程梳理,前提知識點的補充,而對於真正的異步編程方式則是以思考分析爲主,使用沒有過多介紹。最後補充一個鏈接:JavaScript異步編程常見面試題,幫助理解。