JavaScript程序採用了異步事件驅動編程(Event-driven programming)模型,維基百科對它的解釋是:javascript
事件驅動程序設計(英語:Event-driven programming)是一種電腦程序設計模型。這種模型的程序運行流程是由用戶的動做(如鼠標的按鍵,鍵盤的按鍵動做)或者是由其餘程序的消息來決定的。相對於批處理程序設計(batch programming)而言,程序運行的流程是由程序員來決定。批量的程序設計在初級程序設計教學課程上是一種方式。然而,事件驅動程序設計這種設計模型是在交互程序(Interactive program)的狀況下孕育而生的html
簡而言之,在web前端編程裏面JavaScript經過瀏覽器提供的事件模型API和用戶交互,接受用戶的輸入。前端
事件驅動程序模型基本的實現原理基本上都是使用 事件循環(Event Loop)。html5
而JS的運行環境主要有兩個:瀏覽器、Node。java
在兩個環境下的Event Loop實現是不同的,在瀏覽器中基於 規範 來實現,不一樣瀏覽器可能有小小區別。在Node中基於 libuv 這個庫來實現node
JS是單線程執行的,而基於事件循環模型,造成了基本沒有阻塞(除了alert或同步XHR等操做)的狀態。程序員
先看HTML標準的一系列解釋:web
爲了協調事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網絡(networking)等,用戶代理(user agent)必須使用事件循環(event loops)。 有兩類事件循環:一種針對瀏覽上下文(browsing context),還有一種針對worker(web worker)。編程
爲了更好地理解Event Loop,請看下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)vim
上圖中,主線程運行的時候,產生堆棧,棧中的代碼調用各類外部API,異步操做執行完成後,就在消息隊列中排隊。只要棧中的代碼執行完畢,主線程就會去讀取「任務隊列」,依次執行那些事件所對應的回調函數。
下面看一個有意思的例子,猜一下它的運行結果:
setTimeout(
function(){
console.log('1')
},0);
new Promise(
function(resolve){
console.log('2');
resolve()
}).then(
function(){
console.log('3');
});
console.log('4');
複製代碼
打印結果:
2
4
3
1
複製代碼
這是爲何?是否是跟上面說的相違背了?其實這裏面就有了兩個概念宏任務(task/macrotask),微任務(microtask),下面咱們來詳細介紹一下這兩個東東。
根據 規範,每一個線程都有一個事件循環(Event Loop),在瀏覽器中除了主要的頁面執行線程 外,Web worker是在一個新的線程中運行的,因此能夠將其獨立看待。
每一個事件循環有至少一個任務隊列(Task Queue,也能夠稱做Macrotask宏任務),各個任務隊列中放置着不一樣來源(或者不一樣分類)的任務,可讓瀏覽器根據本身的實現來進行優先級排序
以及一個微任務隊列(Microtask Queue),主要用於處理一些狀態的改變,UI渲染工做以前的一些必要操做(能夠防止屢次無心義的UI渲染)
主線程的代碼執行時,會將執行程序置入執行棧(Stack)中,執行完畢後出棧,另外有個堆空間(Heap),主要用於存儲對象及一些非結構化的數據。
常見的macrotask有:
run <script>(同步的代碼執行)
setTimeout
setInterval
setImmediate (Node環境中)
requestAnimationFrame
I/O
UI rendering
複製代碼
常見的microtask有:
process.nextTick (Node環境中)
Promise callback
Object.observe (基本上已經廢棄)
MutationObserver
複製代碼
一、執行宏任務(先進先出),一次循環只執行一個宏任務)
二、執行棧 —— 同步方法順序執行,異步方法交給異步處理模塊
三、執行棧爲空時取出微任務執行(先進先出),直到微任務隊列爲空
四、更新UI渲染。完成一輪循環,反覆執行1-4。(不必定每次循環都會渲染)
複製代碼
在一輪event loop中屢次修改同一dom,只有最後一次會進行繪製。
渲染更新(Update the rendering)會在event loop中的tasks和microtasks完成後進行,但並非每輪event loop都會更新渲染,瀏覽器有本身的機制來肯定是否要更新渲染。若是在一幀(16.7ms)裏屢次修改了dom,瀏覽器可能只會渲染繪製一次。
若是但願在每輪event loop都即時呈現變更,可使用requestAnimationFrame.
複製代碼
那麼咱們回到上面的那個例子就不難解釋了:
==注意==: Promise 自身的代碼是同步執行的,只有 .then後的回調函數纔是微任務。
主線程的執行過程:
在Node環境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而這個nextTick是獨立出來自成隊列的,優先級高於其餘microtask
不過事件循環的的實現就不太同樣了,能夠參考 Node事件文檔 libuv事件文檔
每一輪事件循環都會通過六個階段,在每一個階段後,都會執行microtask
比較特殊的是在poll階段,執行程序同步執行poll隊列裏的回調,直到隊列爲空或執行的回調達到系統上限
接下來再檢查有無預設的setImmediate,若是有就轉入check階段,沒有就先查詢最近的timer的距離,以其做爲poll階段的阻塞時間,若是timer隊列是空的,它就一直阻塞下去
而nextTick並不在這些階段中執行,它在每一個階段以後都會執行。
一個簡單的例子:
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
console.log(5);
複製代碼
根據以上知識,應該很快就能知道輸出結果是 5 3 4 1 2
修改一下:
process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => {
process.nextTick(() => console.log(0));
console.log(4);
});
複製代碼
輸出爲 1 3 2 4 0,由於nextTick隊列優先級高於同一輪事件循環中其餘microtask隊列
再次修改:
process.nextTick(() => console.log(1));
console.log(0);
setTimeout(()=> {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
process.nextTick(() => console.log(2));
setTimeout(()=> {
console.log('timer2');
process.nextTick(() => console.log(3));
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
複製代碼
輸出結果爲:
0
1
2
timer1
timer2
3
promise1
promise2
複製代碼
與在瀏覽器中不一樣,這裏promise1並非在timer1以後輸出,由於在setTimeout執行的時候是出於timer階段,會先一併處理timer回調.
知道JS的事件循環是怎麼樣的了,就須要知道怎麼才能把它用好:
在microtask中不要放置複雜的處理程序,防止阻塞UI的渲染
可使用process.nextTick處理一些比較緊急的事情
能夠在setTimeout回調中處理上輪事件循環中UI渲染的結果
注意不要濫用setInterval和setTimeout,它們並非能夠保證可以按時處理的,setInterval甚至還會出現丟幀的狀況,可考慮使用 requestAnimationFrame
一些可能會影響到UI的異步操做,可放在promise回調中處理,防止多一輪事件循環致使重複執行UI的渲染
在Node中使用immediate來可能會獲得更多的保證
若有錯誤歡迎指正,相互進步。
參考連接:
JavaScript 運行機制詳解:再談Event Loop