javascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變。因此一切javascript版的"多線程"都是用單線程模擬出來的,一切javascript多線程都是紙老虎!javascript
既然js是單線程,後一個任務會等前一個任務執行完成後纔會執行,若是前一個任務執行時間過長後面的任務一直得不到執行,就會引發阻塞。那麼問題來了,假如咱們想瀏覽新聞,可是新聞包含的超清圖片加載很慢,難道咱們的網頁要一直卡着直到圖片徹底顯示出來?所以咱們會將任務分爲兩類:html
當咱們打開網站時,網頁的渲染過程就是一大堆同步任務,好比頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。具體邏輯見下面的導圖: java
文字描述node
上圖中Event Queue 包括 macro task queue 和 micro task queue,下一小節咱們會詳細解釋一下。 上代碼咱們體會一下這個流程:網絡
console.log('1');
setTimeout(function () {
console.log('timeout');
});
console.log('2');
複製代碼
上面的代碼解釋多線程
console.log('1');
和console.log('2');
是同步任務會放到主線程中,setTimeout
聲明的回調函數會放到Event Table。主線程內的任務(console.log('1');console.log('2');
)執行完畢爲空,會去Event Queue讀取console.log('timeout');
,進入主線程執行。因此執行的結果爲1 2 timeout
。下面兩張圖爲Event Loop 和 macro-task 及 micro-task的關係異步
導圖解釋ide
看到這麼多的定義和導圖,咱們來段代碼屢一下:函數
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')
})
複製代碼
macro task Queue | macro task Queue |
---|---|
setTimeout | process1 |
- | then1 |
macro task Queue | macro task Queue |
---|---|
- | process2 |
- | then2 |
1 7 6 8 2 4 3 5
。下圖爲node中Event Loop的執行順序的簡略圖 oop
note
1.poll階段的功能
2.poll階段的處理流程
下面我用if else的方式描述一下poll階段的處理邏輯,以下:
if ('事件循環進入到 poll 階段 ' && '沒有timers註冊的scripts') {
if ('poll 隊列 不爲空') {
console.log('循環遍歷它的回調隊列,以同步執行它們,直到隊列耗盡,或者達到系統依賴的最大值');
} else {
if ('存在setImmediate()註冊的scripts') {
console.log('結束poll phase 進入到check phase 執行這些註冊的scripts');
} else {
console.log('事件循環將等待被添加到隊列中的回調,而後當即執行它們');
}
}
}
console.log('一旦輪詢隊列爲空,事件循環將檢查有無到期的計時器。若是有一個或多個計時器準備就緒,事件循環將返回到計時器階段,以執行這些計時器的回調。');
複製代碼
3.比較setImmediate() 和 setTimeout()
setImmediate()
和 setTimeout()
很類似的,它們什麼時候被調用,決定了它們的行爲方式的不一樣。
setImmediate
用於在當前輪詢階段完成後執行腳本setTimeout
用於把註冊的腳本在最小閾值結束後運行。它們執行的順序將根據調用它們的上下文而變化。若是兩個都是從主模塊中調用,那麼它們將受到進程性能的約束(這可能會受到其餘應用程序的影響)。
例如,若是咱們運行的腳本不是在I/O循環中(即主模塊),那麼執行兩個定時器的順序是不肯定的,由於它受過程性能的約束:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// 打印結果的前後順序是不肯定的,有時`timeout`在前,有時'immediate'在前
複製代碼
可是,若是把這段代碼放到I/O循環的回調中,immediate
老是先被打印出來,以下:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 在一個I/O週期內,在任何計時器的狀況下,setImmediate的回調,由於在一個I/O週期內,I/O callback 的下一個階段爲setImmediate的回調。
複製代碼
以下圖:
說明
咱們在回頭看一下,下面的代碼:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
複製代碼
能夠看出因爲兩個setTimeout延時相同,被合併入了同一個expired timers queue,而一塊兒執行了。因此,只要將第二個setTimeout的延時改爲超過2ms(1ms無效,由於最小間隔爲1s),就能夠保證這兩個setTimeout不會同時過時,也可以保證輸出結果的一致性。
咱們在回頭看一下,上面提到的另一段代碼:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
爲什麼這樣的代碼能保證setImmediate的回調優先於setTimeout的回調執行呢?由於當兩個回調同時註冊成功後,當前node的Event Loop正處於I/O queue階段,而下一個階段是immediates queue,因此可以保證即便setTimeout已經到期,也會在setImmediate的回調以後執行。
因爲水平有限,理解的程度可能會有誤差,歡迎你們指正。