ps寫在前面:這一篇是緊跟上一節的異步。不過值得一說的是以我現在的知識掌握程度要把node的libuv知識解釋清楚,那我怕是在做夢(沒辦法菜是原罪)。故雖然我事前也查閱了許許多多的博客專欄書籍,但是呢查到的越多越不敢寫。故只能已最基礎的表述。如有誤,望指教。感恩!
開始之前先來看這麼一段代碼
console.log(111)
setTimeout(function() {
console.log(444)
}, 0)
new Promise(function(resolve, reject) {
console.log(222)
resolve(333)
}).then(function(res) {
console.log(res)
})
console.log(555)
promise中的then和setTimeout的代碼都是異步執行的那麼上面這個代碼段的輸出順序是怎麼樣的呢
我們知道了代碼在執行時,異步函數不會跟同步函數一樣的在調用棧中執行。這些個異步函數均會被放在一個異步的任務隊列中。
並且,根據異步函數的不同。異步隊列被劃分爲宏任務隊列和微任務隊列
來看一下常見的
macro-task:setTimeout、setInterval、 setImmediate、 script(整體代碼)、I/O 操作等。
micro-task:process.nextTick、Promise、MutationObserver 等
值得注意的是在瀏覽器環境和node環境它們的機制是不一樣的
寫在正文之前 知識儲備 回顧一下執行上下文與執行棧
執行上下文是什麼?
簡單來說執行上下文就是代碼執行的環境,種類分爲三種全局上下文
,函數上下文
,eval執行上下文
向with,eval這樣的詞法作用域欺騙語法就十分消耗性能的本身就不建議使用,故eval執行上下文在這裏也沒有前兩者重要故不作介紹了
從全局上下文開始:
當一段js代碼被執行,js引擎首先會創建一個全局的執行上下文並推入執行棧
那麼開始的全局上下文中有什麼東西呢?
答:如果此時我們的腳本中沒有代碼,則此時全局上下文中只有兩個東西
1.全局變量2.this
js引擎創建執行上下文是有兩個階段的
- 創建階段
- 執行階段
舉例
如果此時js腳本中的代碼是這個樣子的。
let a=1
const fn=function(){}
const foo={}
簡單畫一下創建階段此時的執行上下文中的情況
那麼創建階段做了哪些事情呢?
- 確定this
- LexicalEnvironment(詞法環境)
- VariableEnvironment(變量環境)
簡單理解:就是這個時候創建了全局對象(瀏覽器的話就是window),創建this並指向全局對象,存放變量和函數(簡單的去棧複雜去堆),變量默認undefind,創建作用域鏈
這時候想一眼變量提升。我們就明白它是爲啥提升的了吧
執行階段它們變量與值之間的映射就完成了
函數上下文
js引擎會爲每個函數都創建一個執行上下文,它與全局上下文的內容基本是一致的,不同之處在於函數上下文不創建全局對象而是創建一個參數對象(我們的arguments),再者就是this的指向,和我們平常一下這個this也是要指向該函數調用者,而不是像全局上下文那樣直接指向全局
執行棧
用於存儲代碼指向期間創建的所有執行上下文
跑起來就是開始一個 全局上下文入棧,遇到一個函數的調用指向,這個函數的執行上下文出棧。如果此函數中又調用了其他函數,則又有一個函數上下文入棧,否則此函數執行完出棧。依次往復
再提一點:想一下閉包,因爲閉包的存在導致外函數始終不能執行完畢。故它就一直在執行棧中了,故仍能訪問其詞法作用域鏈或者說是執行上文中的資源
好了開始步入正題咯
瀏覽器中的Event-Loop
瀏覽器的事件循環比較簡單,從文章開始的那段代碼來說
console.log(111)
setTimeout(function() {
console.log(444)
}, 0)
new Promise(function(resolve, reject) {
console.log(222)
resolve(333)
}).then(function(res) {
console.log(res)
})
console.log(555)
已經說了整個腳本也是一個宏任務,這段腳本首先從宏任務隊列中出隊並執行。console.log(111)
執行完遇到一個setTimeout,在一個地方執行完(webapis)後進入宏任務隊列。js的非阻塞就體現在這,它不會在這浪費時間。繼續往下走,promise構造函數會馬上執行(上節已經手寫過了)故console.log(222)
同步執行這時遇到微任務的promise,在一個地方執行完進入微任務隊列,往下走執行完console.log(555)
此時執行棧爲空了,這個時候主線程會去微任務隊列檢測有沒有東西有的化微任務隊列的回調進執行棧。注意不管此時微任務隊列有多少它都會移出執行完,微任務隊列麼東西了。再去宏任務隊列,注意宏任務隊列中的東西可不是和微任務隊列一樣一串執行完。它每次執行完一個宏任務隊列的回調,主線程便會再又去檢查微任務隊列是否有東西
就是說微任務隊列的東西是親兒子,在執行中優先級高一點
故上面的代碼我們一眼便可以看出它的執行結果了吧
Node中的Event-Loop
繼續看node中的事件循環,比瀏覽器複雜多一點哈
先來了解一下node的底層依賴
- v8引擎
- libuv事件引擎
偉大的v8引擎大家應該都熟悉一下,libuv就是不那麼出名了
但在這裏它異步這塊它卻是絕對的主角
看圖:
官方介紹:libuv使用異步,事件驅動的編程方式,核心是提供i/o事件循環與異步回調。libuv的API包含有時間,非阻塞網絡,異步文件操作,子進程等等
接下來看官網給的libuv引擎事件循環模型
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
接下來分別對個個階段進行解釋
- timers(重點):在此階段執行 setTimeout 和 setInterval 中的回調
- pending callback:在此階段處理網絡i/o或者文件i/o中出錯的回調
- idle,prepare:系統使用,我們略過
- poll(重點):在此階段執行i/o回調,計算應該阻塞並且輪詢i/o的時間(如:setTimeout 定時器的時間)
- check(重點):在此階段處理 setImmediate 的回調
- close callbacks:處理
socket.on('close', ...) socket.destroy()
這種關閉的回調
在node中比較注意的是setImmediate 和 process.nextTick
大體捋一下node中事件循環的流程
v8將解析的代碼給了事件引擎,循環直接進入poll階段(看上面的模型)。poll階段有兩個主要功能。1. 計算應該阻塞並輪詢i/o的事件 2. 處理輪詢隊列中的事件
接下來呢?
還是看官方的的解釋吧,我覺得這就很好理解了
如果筆者繪圖繪的好肯定拿出一個流程圖了,但是遺憾。總之重點放到times,poll,check
階段就好了,其實還是蠻容易理解的。
換種方式說:
開始先執行全局js腳本,然後將微任務隊列清空。且值得注意的是node中有兩類微任務隊列。即next-tick隊列和其他普通微任務隊列。在處理時,next-tick隊列優於其他
接下來開始執行宏任務隊列,且與瀏覽器中不同的是。這裏的宏任務隊列的東西也是全部執行完
setTimeout 與 setImmediate
setTimeout我們熟悉,這裏主要看setImmediate。setTimeout的回調執行時機可以由我們指定,但是setImmediate就沒有這麼聽話了。它的回調執行時機在當前poll完成
看下面一段代碼
setTimeout(function() {
console.log(111);
}, 0)
setImmediate(function() {
console.log(222);
})
這是一個非常經典的栗子:它們輸出結果在不瞭解下面知識的情況下你可以說出正確的結果嗎?
先說結果:首先上面代碼片段的1和2的輸出順序是不確定的
驗證:
爲什麼會出現這樣的結果呢?
有兩點我們應該事先知道,1.雖然setTimeout中我們指定的時間是0但是這顯然是不可能的;2.事件循環也是需要初始時間的。
這裏顯然有兩個不可控的時間間隔,並且timers的執行順序根據調用它們的上下文而有所不同。
本來筆者對這一塊的解釋還是比較有自信的,但是爲了想解釋的更加完善一點。我崩了,查了不少的文檔,10個人的東西8個人寫的都不一樣。
官網給出的結論是受性能的約束(這個解釋也是忒標準了)。
故這裏我所能給出的解釋就是上面存在的兩種不可控的時間導致的。
但是呢,如果上面的代碼在一個i/o週期中。那邊是可以確定的了
如這樣:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
即此時的poll階段又派發的兩個任務,poll的下一階段check馬上就是執行了setImmediate
的回調timers就得往後稍稍了。
細究一下poll:
當poll 隊列本來就是空的時候,它首先會檢查有沒有待執行的 setImmediate 任務,如果有,則往下走、進入到 check 階段開始處理 setImmediate;如果沒有 setImmediate 任務,那麼再去檢查一下有沒有到期的 setTimeout 任務需要處理,若有,則跳轉到 timers 階段
nextTick與promise
這個就相對簡單多了,上面也說過。node中的兩個微任務隊列,next-tick是優於普通隊列的
即process.nextTick的回調先執行
注意一下node的新版本變化
從node11開始,timers 階段的setTimeout、setInterval等函數派發的任務、包括 setImmediate 派發的任務,都被修改爲:一旦執行完當前階段的一個任務,就立刻執行微任務隊列。
setTimeout(() => {
console.log('1');
}, 0);
setTimeout(() => {
console.log('2');
Promise.resolve().then(function() {
console.log('3');
});
}, 0);
setTimeout(() => {
console.log('4')
}, 0)
如這段代碼:按node之前。開始全是宏任務,那麼直接一批走完
即1243
但是現在1沒事2被輸出完後微任務隊列就有數據了走3最後4
參考文獻
- libuv參考: https://www.jianshu.com/p/8e0ad01c41dc
- node官網:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
- JavaScript中執行環境和棧:https://www.cnblogs.com/mqliutie/p/4422247.html
- Node.js介紹5-libuv的基本概念:Node.js介紹5-libuv的基本概念
- 修言大佬的專欄:http://www.imooc.com/read/70/article/1972