JavaScript是單線程,JS任務要一個一個順序執行。若是一個任務耗時過長,那麼後一個任務必須等待。會形成阻塞, 所以聰明的程序員將任務分爲兩類:javascript
當咱們打開網站時,網頁的渲染過程就是一大堆同步任務,好比頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。關於這部分有嚴格的文字定義,但本文的目的是用最小的學習成本完全弄懂執行機制,因此咱們用導圖來講明:前端
導圖要表達的內容用文字來表述的話:java
同步就進入主線程
,異步的進入Event Table並註冊函數
。那怎麼知道主線程執行棧爲空呢?js引擎存在監控進程monitoring process,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue 檢查是否有等待被調用的函數。node
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('發送成功!'); } }) console.log('代碼執行結束');
大名鼎鼎的setTimeout
無需再多言,你們對他的第一印象就是異步能夠延時執行,咱們常常這麼實現延時3秒執行:程序員
setTimeout(() => { console.log('延時3秒'); },3000) console.log('執行console'); //執行console //延時3秒
漸漸的setTimeout
用的地方多了,問題也出現了,有時候明明寫的延時3秒,實際卻5,6秒才執行函數,這又咋回事啊?es6
咱們修改一下前面的代碼:ajax
setTimeout(() => { task() },3000) sleep(10000000)
乍一看其實差很少嘛,但咱們把這段代碼在chrome執行一下,卻發現控制檯執行task()
須要的時間遠遠超過3秒,這時候咱們須要從新理解setTimeout
的定義。咱們先說上述代碼是怎麼執行的:chrome
task()
進入Event Table並註冊,計時開始。sleep
函數,很慢,很是慢,計時仍在繼續。timeout
完成,task()
進入Event Queue,可是sleep
也太慢了吧,還沒執行完,只好等着。sleep
終於執行完了,task()
終於從Event Queue進入了主線程執行。上述的流程走完,咱們知道setTimeout
這個函數,是通過指定時間後,把要執行的任務(本例中爲task()
)加入到Event Queue中,又由於是單線程任務要一個一個執行,若是前面的任務須要的時間過久,那麼只能等着,致使真正的延遲時間遠遠大於3秒。promise
咱們還常常遇到setTimeout(fn,0)這樣的代碼,0秒後執行又是什麼意思呢?是否是能夠當即執行呢?異步
答案是不會的
,setTimeout(fn,0)的含義是,指定某個任務在主線程最先可得的空閒時間執行,意思就是不用再等多少秒了,(而HTML5標準規定了setTimeout的最短間隔,不得低於4毫秒,若是低於這個值,就會自動增長,所以即使主線程爲空,0毫秒實際上也是達不到的),只要主線程執行棧內的同步任務所有執行完成,棧爲空就開始執行。
console.log('先執行這裏'); setTimeout(() => { console.log('執行啦') },0); //先執行這裏 //執行啦 //----------------------------------- console.log('先執行這裏'); setTimeout(() => { console.log('執行啦') },3000); //先執行這裏 // ... 3s later // 執行啦
Promise
的定義和功能本文再也不贅述,不瞭解的讀者能夠學習一下阮一峯老師的Promise。而process.nextTick(callback)
(Nodejs獨有),在事件循環的下一次循環中調用 callback 回調函數。
咱們進入正題,除了廣義的同步任務和異步任務,咱們對任務有更精細的定義:
macro-task(宏任務)
:包括總體代碼script,setTimeout,setInterval,事件綁定,ajax,回調函數等micro-task(微任務)
:Promise,process.nextTick不一樣類型的任務會進入對應的Event Queue,事件循環的順序,決定JS代碼的執行順序。進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。聽起來有點繞,用一段代碼說明:
setTimeout(function() { console.log('setTimeout'); }) new Promise(function(resolve) { console.log('promise'); }).then(function() { console.log('then'); }) console.log('console');
setTimeout
,那麼將其回調函數註冊後分發到宏任務Event Queue。(註冊過程與上同,下文再也不描述)new Promise
當即執行,then
函數分發到微任務Event Queue。then
在微任務Event Queue裏面,執行。setTimeout
對應的回調函數,當即執行。事件循環,宏任務,微任務的關係如圖所示:
咱們來分析一段較複雜的代碼,看看你是否真的掌握了JS的執行機制:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
1. 第一輪事件循環流程分析以下:
總體script做爲第一個宏任務進入主線程,遇到console.log
,輸出 1。
遇到setTimeout
,其回調函數被分發到宏任務Event Queue中。咱們暫且記爲 setTimeout1。
遇到process.nextTick()
,其回調函數被分發到微任務Event Queue中。咱們記爲 process1。
遇到new Promise
直接執行,輸出 7。then
被分發到微任務Event Queue中。咱們記爲 then1。
又遇到了setTimeout
,其回調函數被分發到宏任務Event Queue中,咱們記爲 setTimeout2。
宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
上表是第一輪事件循環宏任務結束時各Event Queue的狀況,此時已經輸出了1和7。
發現存在 process1 和 then1 兩個微任務。
執行process1
,輸出 6。
執行then1
,輸出 8。
第一輪事件循環正式結束,這一輪的結果是輸出 1,7,6,8。
2. 第二輪事件循環從setTimeout1
宏任務開始:
首先輸出 2。遇到process.nextTick()
,將其分發到微任務Event Queue中,標記爲 process2。
new Promise
當即執行輸出 4,then
也分發到微任務Event Queue中,記爲 then2。
宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
第二輪事件循環宏任務結束後,發現有 process2 和 then2 兩個微任務能夠執行。
輸出 3。
輸出 5。
第二輪事件循環結束,第二輪輸出 2,4,3,5。
3. 第三輪事件循環開始,此時只剩setTimeout2
,執行。
直接輸出 9。
將process.nextTick()
分發到微任務Event Queue中。記爲 process3。
new Promise
當即執行,輸出 11。
將then
分發到微任務Event Queue中,記爲 then3。
宏任務Event Queue | 微任務Event Queue |
---|---|
process3 | |
then3 |
第三輪事件循環宏任務執行結束後,執行兩個微任務 process3 和 then3。
輸出 10。
輸出 12。
第三輪事件循環結束,第三輪輸出 9,11,10,12。
整段代碼,共進行了三次事件循環,完整的輸出爲 1,7,6,8,2,4,3,5,9,11,10,12
。
注意,node環境與前端環境不徹底相同,輸出順序可能會有偏差
node版本 < 11,輸出 1,7,6,8 ,2,4,9,11,3,10,5,12)。
若是仍是不懂得同窗能夠去看一下這個小哥哥/小姐姐的文章,總結的挺到位的。向他/她學習: