這一次,完全弄懂 JavaScript 執行機制

1. JavaScript事件循環

JavaScript是單線程,JS任務要一個一個順序執行。若是一個任務耗時過長,那麼後一個任務必須等待。會形成阻塞, 所以聰明的程序員將任務分爲兩類:javascript

  • 同步任務
  • 異步任務

當咱們打開網站時,網頁的渲染過程就是一大堆同步任務,好比頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。關於這部分有嚴格的文字定義,但本文的目的是用最小的學習成本完全弄懂執行機制,因此咱們用導圖來講明:前端

導圖要表達的內容用文字來表述的話:java

  • 首先判斷JS是同步仍是異步,同步就進入主線程異步的進入Event Table並註冊函數
  • 當知足觸發條件後,Event Table會將這個函數移入Event Queue(事件隊列)。
  • 主線程內的任務執行完畢爲空,會去Event Queue查看是否有可執行的異步任務,若是有就推入主線程中
  • 上述過程會不斷重複,這就是常說的Event Loop(事件循環)
  • 事件循環是JS實現異步的一種方法,也是JS的執行機制。

那怎麼知道主線程執行棧爲空呢?js引擎存在監控進程monitoring process,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue 檢查是否有等待被調用的函數。node

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
                console.log('發送成功!');
            }
})
console.log('代碼執行結束');
  • ajax進入Event Table,註冊回調函數success。
  • 執行console.log('代碼執行結束')。
  • ajax事件完成,回調函數success進入Event Queue。
  • 主線程從Event Queue讀取回調函數success並執行。

2.setTimeout

大名鼎鼎的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函數,很慢,很是慢,計時仍在繼續。
  • 3秒到了,計時事件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
// 執行啦

3.Promise與process.nextTick(callback)

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
  • 遇到console.log(),當即執行。
  • 此時總體代碼script做爲第一個宏任務執行結束,看看有哪些微任務?咱們發現了then在微任務Event Queue裏面,執行。
  • ok,第一輪事件循環結束了,咱們開始第二輪循環,固然要從宏任務Event Queue開始。咱們發現了宏任務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直接執行,輸出 7then被分發到微任務Event Queue中。咱們記爲 then1

  • 又遇到了setTimeout,其回調函數被分發到宏任務Event Queue中,咱們記爲 setTimeout2

    宏任務Event Queue 微任務Event Queue
    setTimeout1 process1
    setTimeout2 then1
  • 上表是第一輪事件循環宏任務結束時各Event Queue的狀況,此時已經輸出了1和7。

  • 發現存在 process1then1 兩個微任務。

  • 執行process1,輸出 6

  • 執行then1,輸出 8

  • 第一輪事件循環正式結束,這一輪的結果是輸出 1,7,6,8。

2. 第二輪事件循環從setTimeout1宏任務開始:

  • 首先輸出 2。遇到process.nextTick(),將其分發到微任務Event Queue中,標記爲 process2

  • new Promise當即執行輸出 4then也分發到微任務Event Queue中,記爲 then2

    宏任務Event Queue 微任務Event Queue
    setTimeout2 process2
    then2
  • 第二輪事件循環宏任務結束後,發現有 process2then2 兩個微任務能夠執行。

  • 輸出 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
  • 第三輪事件循環宏任務執行結束後,執行兩個微任務 process3then3

  • 輸出 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)。

若是仍是不懂得同窗能夠去看一下這個小哥哥/小姐姐的文章,總結的挺到位的。向他/她學習:

參考大神連接:https://juejin.cn/post/6844903512845860872

相關文章
相關標籤/搜索