banner
SlhwSR

SlhwSR

热爱技术的一名全栈开发者
github
bilibili

事件循環原理

一、是什麼#

首先,JavaScript 是一門單線程的語言,意味著同一時間內只能做一件事,但是這並不意味著單線程就是阻塞,而實現單線程非阻塞的方法就是事件循環

JavaScript中,所有的任務都可以分為

  • 同步任務:立即執行的任務,同步任務一般會直接進入到主線程中執行

  • 異步任務:異步執行的任務,比如ajax網絡請求,setTimeout 定時函數等

同步任務與異步任務的運行流程圖如下:

image

從上面我們可以看到,同步任務進入主線程,即主執行棧,異步任務進入任務隊列,主線程內的任務執行完畢為空,會去任務隊列讀取對應的任務,推入主線程執行。上述過程的不斷重複就事件循環

二、宏任務與微任務#

如果將任務劃分為同步任務和異步任務並不是那麼的準確,舉個例子:

console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

如果按照上面流程圖來分析代碼,我們會得到下面的執行步驟:

  • console.log(1) ,同步任務,主線程中執行
  • setTimeout() ,異步任務,放到 Event Table,0 毫秒後console.log(2) 回調推入 Event Queue
  • new Promise ,同步任務,主線程直接執行
  • .then ,異步任務,放到 Event Table
  • console.log(3),同步任務,主線程執行

所以按照分析,它的結果應該是 1 => 'new Promise' => 3 => 2 => 'then'

但是實際結果是:1=>'new Promise'=> 3 => 'then' => 2

出現分歧的原因在於異步任務執行順序,事件隊列其實是一個 “先進先出” 的數據結構,排在前面的事件會優先被主線程讀取

例子中 setTimeout回調事件是先進入隊列中的,按理說應該先於 .then 中的執行,但是結果卻偏偏相反

原因在於異步任務還可以細分為微任務與宏任務

微任務#

一個需要異步執行的函數,執行時機是在主函數執行結束之後、當前宏任務結束之前

常見的微任務有:

  • Promise.then

  • MutaionObserver

  • Object.observe(已廢棄;Proxy 對象替代)

  • process.nextTick(Node.js)

宏任務#

宏任務的時間粒度比較大,執行的時間間隔是不能精確控制的,對一些高實時性的需求就不太符合

常見的宏任務有:

  • script (可以理解為外層同步代碼)
  • setTimeout/setInterval
  • UI rendering/UI 事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

這時候,事件循環,宏任務,微任務的關係如圖所示

image

按照這個流程,它的執行機制是:

  • 執行一個宏任務,如果遇到微任務就將它放到微任務的事件隊列中
  • 當前宏任務執行完成後,會查看微任務的事件隊列,然後將裡面的所有微任務依次執行完

回到上面的題目

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)

流程如下

// 遇到 console.log(1) ,直接打印 1
// 遇到定時器,屬於新的宏任務,留著後面執行
// 遇到 new Promise,這個是直接執行的,打印 'new Promise'
// .then 屬於微任務,放入微任務隊列,後面再執行
// 遇到 console.log(3) 直接打印 3
// 好了本輪宏任務執行完畢,現在去微任務列表查看是否有微任務,發現 .then 的回調,執行它,打印 'then'
// 當一次宏任務執行完,再去執行新的宏任務,這裡就剩一個定時器的宏任務了,執行它,打印 2

三、async 與 await#

async 是異步的意思,await 則可以理解為 async wait。所以可以理解async就是用來聲明一個異步方法,而 await 是用來等待異步方法執行

async#

async函數返回一個promise對象,下面兩種方法是等效的

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await#

正常情況下,await命令後面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值

async function f(){
    // 等同於
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

不管await後面跟著的是什麼,await都會阻塞後面的代碼

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

上面的例子中,await 會阻塞下面的代碼(即加入微任務隊列),先執行 async 外面的同步代碼,同步代碼執行完,再回到 async 函數中,再執行之前阻塞的代碼

所以上述輸出結果為:1fn232

四、流程分析#

通過對上面的了解,我們對JavaScript對各種場景的執行順序有了大致的了解

這裡直接上代碼:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析過程:

  1. 執行整段代碼,遇到 console.log('script start') 直接打印結果,輸出 script start
  2. 遇到定時器了,它是宏任務,先放著不執行
  3. 遇到 async1(),執行 async1 函數,先打印 async1 start,下面遇到await怎麼辦?先執行 async2,打印 async2,然後阻塞下面代碼(即加入微任務列表),跳出去執行同步代碼
  4. 跳到 new Promise 這裡,直接執行,打印 promise1,下面遇到 .then(),它是微任務,放到微任務列表等待執行
  5. 最後一行直接打印 script end,現在同步代碼執行完了,開始執行微任務,即 await 下面的代碼,打印 async1 end
  6. 繼續執行下一個微任務,即執行 then 的回調,打印 promise2
  7. 上一個宏任務所有事都做完了,開始下一個宏任務,就是定時器,打印 settimeout

所以最後的結果是:script startasync1 startasync2promise1script endasync1 endpromise2settimeout

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。