javascript,區別於後臺,就是javascript是單執行緒的。單執行緒做到不阻塞,起到作用的其實就是我們常說的非同步。
執行時概念
首先,我們來理解一下javascript的幾個概念
- 堆(heap)
- 棧(stack)
- 任務佇列(queue),這裡又分為巨集任務 & 微任務
瀏覽器的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)
複製程式碼
我們修改一下程式碼,我們在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)
複製程式碼
我們前面的例子其實都是立即執行的程式碼,當傳送http請求時,請求先掛起,當請求結果回來時,再將請求回撥加入到任務佇列中。
node的event loop
node程式碼也是javascript,解析javascript的是V8引擎。非同步i/o採用的是libuv。
node的event loop,有六個事件,依次迴圈
- poll:獲取新的i/o事件,大部分事件都在這裡執行
- check:執行setImmediate的回撥
- close:執行socket的close事件回撥
- timers:執行setTimeout、setInterval的回撥
- i/o:處理上一輪迴圈中少量未執行的i/o回撥
- idle,prepare:node內部使用
我們來看下程式碼的執行情況
結果是這樣的:同步任務 - 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')
})
複製程式碼
當我們把程式碼嵌到非同步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')
})
})
複製程式碼
最後,我們來看一下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')
})
})
複製程式碼
process.nextTick()
在上面的例子中,我們會發現,nextTick的執行總是比微任務要快。
在node中,nextTick其實是獨立於event loop之外的,nextTick擁有自己的任務佇列,event loop,執行完一個階段之後,就會將nextTick中的所有任務先清空,再執行微任務。
寫在最後
瀏覽器的event loop 與node的event loop還是有稍許不同,不過大致的概念是差不多的,只要弄懂其中的關係之後,程式碼中出現的問題就迎刃而解。