咱們知道JS語言是串行執行、阻塞式、事件驅動的,那麼它又是怎麼支持併發處理數據的呢?javascript
在瀏覽器實現中,每一個單頁都是一個獨立進程,其中包含了JS引擎、GUI界面渲染、事件觸發、定時觸發器、異步HTTP請求等多個線程。html
進程(Process)是操做系統CPU等資源分配的最小單位,是程序的執行實體,是線程的容器。
線程(Thread)是操做系統可以進行運算調度的最小單位,一條線程指的是進程中一個單一順序的控制流。
所以咱們能夠說JS是"單線程"式的語言,代碼只能按照單一順序進行串行執行,並在執行完成前阻塞其餘代碼。java
如上圖所示爲JS的幾種重要數據結構:node
咱們的經驗告訴咱們JS是能夠併發執行的,好比定時任務、併發AJAX請求,那這些是怎麼完成的呢?其實這些都是JS在用單線程模擬多線程完成的。git
如上圖所示,JS串行執行主線程任務,當遇到異步任務如定時器時,將其放入事件隊列中,在主線程任務執行完畢後,再去事件隊列中遍歷取出隊首任務進行執行,直至隊列爲空。es6
所有執行完成後,會有主監控進程,持續檢測隊列是否爲空,若是不爲空,則繼續事件循環。github
定時任務setTimeout(fn, timeout)
會先被交給瀏覽器的定時器模塊,等延遲時間到了,再將事件放入到事件隊列裏,等主線程執行結束後,若是隊列中沒有其餘任務,則會被當即處理,而若是還有沒有執行完成的任務,則須要等前面的任務都執行完成纔會被執行。所以setTimeout的第2個參數是最少延遲時間,而非等待時間。c#
當咱們預期到一個操做會很繁重耗時又不想阻塞主線程的執行時,會使用當即執行任務:瀏覽器
setTimeout(fn, 0);
然而考慮這麼一段代碼會怎麼執行:網絡
setTimeout(()=>{console.log(5)},5) setTimeout(()=>{console.log(4)},4) setTimeout(()=>{console.log(3)},3) setTimeout(()=>{console.log(2)},2) setTimeout(()=>{console.log(1)},1) setTimeout(()=>{console.log(0)},0)
瞭解完事件隊列機制,你的答案應該是0,1,2,3,4,5
,然而答案倒是1,0,2,3,4,5
,這個是由於瀏覽器的實現機制是最小間隔爲1ms。
// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456 if (!(after >= 1 && after <= TIMEOUT_MAX)) after = 1; // schedule on next tick, follows browser behavior
瀏覽器以32位bit來存儲延時,若是大於 2^32-1 ms(24.8天)
,致使溢出會馬上執行。
定時器的嵌套調用超過4層時,會致使最小間隔爲4ms:
var i=0; function cb() { console.log(i, new Date().getMilliseconds()); if (i < 20) setTimeout(cb, 0); i++; } setTimeout(cb, 0);
能夠看到前4層也不是標準的馬上執行,在第4層後間隔明顯變大到4ms以上:
0 667 1 669 2 670 3 672 4 676 5 681 6 685
Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.
爲了優化後臺tab的加載佔用資源,瀏覽器對後臺未激活的頁面中定時器延遲限制爲1s。
對追蹤型腳本,如谷歌分析等,在當前頁面,依然是4ms的延時限制,然後臺tabs爲10s。
此時,咱們會知道,setInterval會在每一個定時器延時時間到了後,將一個新的事件fn放入事件隊列,若是前面的任務執行過久,咱們會看到連續的fn事件被執行而感受不到時間預設間隔。
所以,咱們要儘可能避免使用setInterval,改用setTimeout來模擬循環定時任務。
JS一直缺乏休眠的語法,藉助ES6新的語法,咱們能夠模擬這個功能,可是一樣的這個方法由於藉助了setTimeout也不能保證準確的睡眠延時:
function sleep(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }) } // 使用 async function test() { await sleep(3000); }
async函數是Generator函數的語法糖,提供更方便的調用和語義,上面的使用能夠替換爲:
function* test() { yield sleep(3000); } // 使用 var g = test(); test.next();
可是調用使用更加複雜,所以通常咱們使用async函數便可。但JS時如何實現睡眠函數的呢,其實就是提供一種執行時的中間狀態暫停,而後將控制權移交出去,等控制權再次交回時,從上次的斷點處繼續執行。所以營造了一種睡眠的假象,其實JS主線程還能夠在執行其餘的任務。
Generator函數調用後會返回一個內部指針,指向多個異步任務的暫停點,當調用next函數時,從上一個暫停點開始執行。
協程(coroutine)是指多個線程互相協做,完成異步任務的一種多任務異步執行的解決方案。他的運行流程:
能夠看到這也就是Generator函數的實現方案。
一個JS的任務能夠定義爲:在標準執行機制中,即將被調度執行的全部代碼塊。
咱們上面介紹了JS如何使用單線程完成異步多任務調用,但咱們知道JS的異步任務分不少種,如setTimeout定時器、Promise異步回調任務等,它們的執行優先級又同樣嗎?
答案是不。JS在異步任務上有更細緻的劃分,它分爲兩種:
宏任務(macrotask)包含:
微任務(microtask)包含:
宏任務和微任務都有自身的事件循環機制,也擁有獨立的事件隊列(Event Queue),都會按照隊列的順序依次執行。但宏任務和微任務主要有兩點區別:
瀏覽器是多進程式的,每一個頁面和插件都是一個獨立的進程,這樣能夠保證單頁面崩潰或者插件崩潰不會影響到其餘頁面和瀏覽器總體的穩定運行。
它主要包括:
瀏覽器的單個頁面就是一個進程,指的就是Renderer進程,而進程中又包含有多個線程用於處理不一樣的任務,主要包括:
須要注意的是,GUI渲染進程和JS引擎進程互斥,二者只會同時執行一個。主要的緣由是爲了節流,由於JS的執行會可能屢次改變頁面,頁面的改變也會屢次調用JS,如resize。所以瀏覽器採用的策略是交替執行,每一個宏任務執行完成後,執行GUI渲染,而後執行下一個宏任務。
由於JS只有一個引擎線程,同時和GUI渲染線程互斥,所以在繁重任務執行時會致使頁面卡住,因此在HTML5中支持了Webworker,它用於向瀏覽器申請一個新的子線程執行任務,並經過postMessage API來和worker線程通訊。因此咱們在繁重任務執行時,能夠選擇新開一個Worker線程來執行,並在執行結束後通訊給主線程,這樣不會影響頁面的正常渲染和使用。
最後給你們出一個考題,能夠猜下執行的輸出結果來驗證學習成果:
function sleep(ms) { console.log('before first microtask init'); new Promise(resolve => { console.log('first microtask'); resolve() }) .then(() => {console.log('finish first microtask')}); console.log('after first microtask init'); return new Promise(resolve => { console.log('second microtask'); setTimeout(resolve, ms); }); } setTimeout(async () => { console.log('start task'); await sleep(3000); console.log('end task'); }, 0); setTimeout(() => console.log('add event'), 0); console.log('main thread');
輸出爲:
main thread start task before first microtask init first microtask after first microtask init second microtask finish first microtask add event end task