javascript - event loop

寫不好程式碼的格子襯衫發表於2019-01-20

javascript,區別於後臺,就是javascript是單執行緒的。單執行緒做到不阻塞,起到作用的其實就是我們常說的非同步。

執行時概念

首先,我們來理解一下javascript的幾個概念

  • 堆(heap)
  • 棧(stack)
  • 任務佇列(queue),這裡又分為巨集任務 & 微任務

javascript - event loop

瀏覽器的event loop

當javascript執行的時候,首先,程式碼會進入執行棧,變數之類的會儲存在堆中,而任務佇列儲存的就是javascript中的非同步任務。

我們來看下下面的例子,首先,script程式碼會進入執行棧,然後執行同步程式碼,接著將非同步任務放到任務佇列中。

先執行同步程式碼,列印1,2,Promise(promise中的程式碼是同步執行的),3。

接著將非同步任務放入任務佇列中,promise回撥放入微任務中,setTimeout回撥放到巨集任務中。

在event loop中,執行棧的程式碼執行完之後,在微任務佇列取一個事件放到執行棧中執行,當微任務佇列為空時,就從巨集任務中取一個事件放到執行棧中執行,如此反覆迴圈。

console.log(1)
setTimeout(() => {
    console.log('setTimeout')
})
console.log(2)
new Promise((resolve, reject) => {
    console.log('Promise')
    resolve()
}).then(() => {
    console.log('then')
})
console.log(3)
複製程式碼

javascript - event loop

我們修改一下程式碼,我們在promise的回撥中又加了一個promise。

其他不變,當執行第一個promise的回撥時,同步執行第二個promise,這個沒有問題,此時,把第二個promise的回撥加入到微任務中。

在下一次event loop中,先檢視微任務佇列,於是執行第二個promise的回撥,列印了then1。

最後,微任務佇列清空了,於是檢視巨集任務,執行setTimeout的回撥。

console.log(1)
setTimeout(() => {
    console.log('setTimeout')
})
console.log(2)
new Promise((resolve, reject) => {
    console.log('Promise')
    resolve()
}).then(() => {
    console.log('then')
    new Promise((resolve1, reject1) => {
        console.log('Promise1')
        resolve1()
    }).then(() => {
        console.log('then1')
    })
})
console.log(3)
複製程式碼

javascript - event loop

我們前面的例子其實都是立即執行的程式碼,當傳送http請求時,請求先掛起,當請求結果回來時,再將請求回撥加入到任務佇列中。

node的event loop

node程式碼也是javascript,解析javascript的是V8引擎。非同步i/o採用的是libuv。

javascript - event loop

node的event loop,有六個事件,依次迴圈

  • poll:獲取新的i/o事件,大部分事件都在這裡執行
  • check:執行setImmediate的回撥
  • close:執行socket的close事件回撥
  • timers:執行setTimeout、setInterval的回撥
  • i/o:處理上一輪迴圈中少量未執行的i/o回撥
  • idle,prepare:node內部使用

javascript - event loop

我們來看下程式碼的執行情況

結果是這樣的:同步任務 - nextTick - 微任務 - 巨集任務 - setImmediate

setTimeout(() => {
    console.log('setTimeout')
})
new Promise((resolve, reject) => {
    console.log('Promise')
    resolve()
}).then(() => {
    console.log('then')
})
setImmediate(() => {
	console.log('setImmediate')
})
process.nextTick(() => {
	console.log('nextTick')
})
複製程式碼

javascript - event loop

當我們把程式碼嵌到非同步i/o裡面呢

結果是這樣的:同步任務 - nextTick - 微任務 - setImmediate -巨集任務

與剛剛不同的是,程式碼放到非同步i/o裡面,執行完poll之後,執行的是check,所以setImmediate會在巨集任務之前


setTimeout(() => {
    console.log('setTimeout')
    setTimeout(() => {
        console.log('setTimeout1')
    })
    new Promise((resolve, reject) => {
        console.log('Promise')
        resolve()
    }).then(() => {
        console.log('then')
    })
    setImmediate(() => {
    	console.log('setImmediate')
    })
    process.nextTick(() => {
    	console.log('nextTick')
    })
})

複製程式碼

javascript - event loop

最後,我們來看一下node中巨集任務與微任務的順序

結果是先把巨集任務佇列中的回撥全部執行完畢,接著執行全部nextTick,最後執行所有的微任務。

這個就是跟瀏覽器不同了,瀏覽器是執行完一個任務之後,先執行所有微任務,然後再執行下一個巨集任務。

setTimeout(() => {
    console.log('setTimeout')
    Promise.resolve().then(() => {
	    console.log('then')
	})
	process.nextTick(() => {
		console.log('nextTick')
	})
})

setTimeout(() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
	    console.log('then2')
	})
	process.nextTick(() => {
		console.log('nextTick2')
	})
})
複製程式碼

javascript - event loop

process.nextTick()

在上面的例子中,我們會發現,nextTick的執行總是比微任務要快。

在node中,nextTick其實是獨立於event loop之外的,nextTick擁有自己的任務佇列,event loop,執行完一個階段之後,就會將nextTick中的所有任務先清空,再執行微任務。

寫在最後

瀏覽器的event loop 與node的event loop還是有稍許不同,不過大致的概念是差不多的,只要弄懂其中的關係之後,程式碼中出現的問題就迎刃而解。

相關文章