無論你是前端新手仍是老鳥,在平常的工做或者面試的過程當中總會遇到這樣的狀況:給定的幾行代碼,寫出其輸出內容和順序。因此咱們就須要搞懂javascript的運行原理和執行機制javascript
首先,咱們先看一道經典的面試題html
setTimeout(function(){
console.log('定時器開始啦')
});
new Promise(function(resolve){
console.log('立刻執行for循環啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('執行then函數啦')
});
console.log('代碼執行結束');
複製代碼
我把這段代碼粘貼到chrome執行了一下,輸出的結果如圖所示前端
JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?java
技術的出現,都跟現實世界裏的應用場景密切相關的。web
JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。面試
好比,假定如今同時有兩個線程操做同一個DOM元素,一個線程在DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?chrome
因此js就被設計成單線程segmentfault
HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。這個新標準並無改變js單線程的本質。api
javascript事件循環既然js是單線程,那就像只有一個窗口的銀行,客戶須要排隊一個一個辦理業務,同理js任務也要一個一個順序執行。若是一個任務耗時過長,那麼後一個任務也必須等着。瀏覽器
若是JS中不存在異步,只能自上而下執行,若是上一行解析時間很長,那麼下面的代碼就會被阻塞。 對於用戶而言,阻塞就意味着"卡死",這樣就致使了不好的用戶體驗
因此js存在同步任務和異步任務
- 同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
- 異步任務:不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。
既然JS是單線程的,只能在一條線程上執行,又是如何實現的異步呢?
是經過的事件循環(event loop),理解了event loop機制,就理解了JS的執行機制
當javascript代碼執行的時候會將不一樣的變量存於內存中的不一樣位置:堆(heap)和棧(stack)中來加以區分。其中,堆裏存放着一些對象。而棧中則存放着一些基礎類型變量以及對象的指針。 可是咱們這裏說的執行棧和上面這個棧的意義卻有些不一樣。
執行棧
當咱們調用一個方法的時候,js會生成一個與這個方法對應的執行環境(context),又叫執行上下文。這個執行環境中存在着這個方法的私有做用域,上層做用域的指向,方法的參數,這個做用域中定義的變量以及這個做用域的this對象。 而當一系列方法被依次調用的時候,由於js是單線程的,同一時間只能執行一個方法,因而這些方法被排隊在一個單獨的地方。這個地方被稱爲執行棧。
任務隊列(Task Queue)
js的另外一大特色是非阻塞,實現這一點的關鍵在於任務隊列
任務隊列是先進先出的原則,先進隊列的事件先執行,後進隊列的事件後執行。
js引擎遇到一個異步任務後並不會一直等待其返回結果,而是會將這個任務掛起(壓入到任務隊列中),繼續執行執行棧中的其餘任務。當一個異步任務返回結果後,js會將這個任務加入與當前執行棧不一樣的另外一個隊列,咱們稱之爲任務隊列。被放入任務隊列不會馬上執行其回調,而是等待當前執行棧中的全部任務都執行完畢, 主線程處於閒置狀態時,主線程會去查找任務隊列是否有任務。若是有,那麼主線程會從中取出排在第一位的事件,並把這個任務對應的回調放入執行棧中,而後執行其中的同步代碼,如此反覆,這樣就造成了一個無限的循環。(下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)。)
圖中的stack表示咱們所說的執行棧,web apis則是表明一些異步事件,而callback queue即事件隊列。主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)同步和異步任務分別進入不一樣的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
(4)當指定的事情完成時,Event Table會將這個函數移入Event Queue。
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列(event Queue)"的異步任務,若是有就推入主線程中。
(4)主線程不斷重複上面的步驟。
以上循環執行,這就是event loop
宏任務(macro task)
包括總體代碼script,setTimeout,setInterval
微任務(micro-task)
Promise,process.nextTick
不一樣類型的任務會進入對應的Event Queue,好比宏任務就會進入到宏任務的事件隊列中,微任務就會進入到微任務的事件隊列中。
事件循環的順序,進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。
請看網絡盜圖
按照這種分類方式:JS的執行機制是
執行一個宏任務,過程當中若是遇到微任務,就將其放到微任務的【事件隊列】裏
當前宏任務執行完成後,會查看微任務的【事件隊列】,並將裏面所有的微任務依次執行完
下面代碼是文章開頭的面試題:
setTimeout(function(){
console.log('定時器開始啦')
});
new Promise(function(resolve){
console.log('立刻執行for循環啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('執行then函數啦')
});
console.log('代碼執行結束');
複製代碼
首先執行script下的宏任務,遇到setTimeout,將其放到宏任務的【隊列】裏
遇到 new Promise直接執行,打印"立刻執行for循環啦"
遇到then方法,是微任務,將其放到微任務的【隊列裏】
打印 "代碼執行結束"
本輪宏任務執行完畢,查看本輪的微任務,發現有一個then方法裏的函數, 打印"執行then函數啦"
到此,本輪的event loop 所有完成。
因此最後的執行順序是【立刻執行for循環啦 --- 代碼執行結束 --- 執行then函數啦 --- 定時器開始啦】
(1)js的異步咱們從最開頭就說javascript是一門單線程語言,不論是什麼新框架新語法糖實現的所謂異步,其實都是用同步的方法去模擬的,緊緊把握住單線程這點很是重要。
(2)事件循環Event Loop事件循環是js實現異步的一種方法,也是js的執行機制。
參考資料
一、阮一峯老師的JavaScript 運行機制詳解:再談Event Loop:www.ruanyifeng.com/blog/2014/1… 二、segmentfault 10分鐘理解JS引擎的執行機制:segmentfault.com/a/119000001…
若是你以爲對你有幫助,記得點個贊和在看哦。同時也期待你們的留言討論。