上次大家跟我吃飽喝足又擼了一遍PromiseA+,想必大家肯定滿腦子想的都是西瓜可樂......
什麼西瓜可樂!明明是Promise!
呃,清醒一下,今天大家搬個小板凳,聽我說說JS中比較有意思的事件環,在瞭解事件環之前呢,我們先來了解幾個基本概念。
棧(Stack)
棧是一種遵循後進先出(LIFO)的資料集合,新新增或待刪除的元素都儲存在棧的末尾,稱作棧頂,另一端稱作棧底。在棧裡,新元素都靠近棧頂,舊元素都接近棧底
感覺說起來並不是很好理解,我們舉個例子,比如有一個乒乓球盒,我們不停的向球盒中放進乒乓球,那麼最先放進去的乒乓球一定是在最下面,最後放進去的一定是在最上面,那麼如果我們想要把這些球取出來是不是就必須依次從上到下才能拿出來,這個模型就是後進先出,就是我們後進入球盒的球反而最先出來。
棧的概念其實在我們js中十分的重要,大家都知道我們js是一個單執行緒語言,那麼他單執行緒在哪裡呢,就在他的主工作執行緒,也就是我們常說的執行上下文,這個執行上下文就是棧空間,我們來看一段程式碼:
console.log('1');
function a(){
console.log('2');
function b(){
console.log('3')
}
b()
}
a()
複製程式碼
我們知道函式執行的時候會將這個函式放入到我們的執行上下文中,當函式執行完畢之後會彈出執行棧,那麼根據這個原理我們就能知道這段程式碼的執行過程是
- 首先我們程式碼執行的時候會有一個全域性上下文,此時程式碼執行,全域性上下文進行執行棧,處在棧底的位置
- 我們遇到
console.log('1')
,這個函式在呼叫的時候進入執行棧,當這句話執行完畢也就是到了下一行的時候我們console
這個函式就會出棧,此時棧中仍然只有全域性上下文 - 接著執行程式碼,這裡注意的是我們遇到的函式宣告都不會進入執行棧,只有當我們的函式被呼叫被執行的時候才會進入,這個原理和我們執行棧的名字也就一模一樣,接著我們遇到了
a();
這句程式碼這個時候我們的a函式就進入了執行棧,然後進入到我們a
的函式內部中,此時我們的函式執行棧應該是全域性上下文 —— a
- 接著我執行
console.log('2')
,執行棧變成全域性上下文——a——console
,接著我們的console
執行完畢,我們執行棧恢復成全域性上下文 —— a
- 接著我們遇到了
b()
;那麼b
進入我們的執行棧,全域性上下文——a——b
, - 接著進入b函式的內部,執行
console.log('3')
的時候執行棧為全域性上下文——a——b——console
,執行完畢之後回覆成全域性上下文——a——b
- 然後我們的
b
函式就執行完畢,然後就被彈出執行棧,那麼執行棧就變成全域性上下文——a
- 然後我們的
a
函式就執行完畢,然後就被彈出執行棧,那麼執行棧就變成全域性上下文
- 然後我們的全域性上下文會在我們的瀏覽器關閉的時候出棧
我們的執行上下文的執行過程就是這樣,是不是清楚了很多~
通過上面的執行上下文我們可以發現幾個特點:
- 執行上下文是單執行緒
- 執行上下文是同步執行程式碼
- 當有函式被呼叫的時候,這個函式會進入執行上下文
- 程式碼執行會產生一個全域性的上下文,只有當瀏覽器關閉才會出棧
佇列(Queue)
佇列是一種遵循先進先出(FIFO)的資料集合,新的條目會被加到佇列的末尾,舊的條目會從佇列的頭部被移出。
這裡我們可以看到佇列和棧不同的地方是棧是後進先出類似於乒乓球盒,而佇列是先進先出,也就是說最先進入的會最先出去。 同樣我們舉個例子,佇列就好比是我們排隊過安檢,最先來到的人排在隊伍的首位,後來的人接著排在隊伍的後面,然後安檢員會從隊伍的首端進行安檢,檢完一個人就放行一個人,是不是這樣的一個隊伍就是先進先出的一個過程。
佇列這裡我們就要提到兩個概念,巨集任務(macro task),微任務(micro task)。
任務佇列
Js的事件執行分為巨集仁務和微任務
- 巨集仁務主要是由
script
(全域性任務),setTimeout
,setInterval
,setImmediate
,I/O ,UI rendering - 微任務主要是
process.nextTick
,Promise.then
,Object.observer
,MutationObserver
.
瀏覽器事件環
js執行程式碼的過程中如果遇到了上述的任務程式碼之後,會先把這些程式碼的回撥放入對應的任務佇列中去,然後繼續執行主執行緒的程式碼知道執行上下文中的函式全部執行完畢了之後,會先去微任務佇列中執行相關的任務,微任務佇列清空之後,在從巨集仁務佇列中拿出任務放到執行上下文中,然後繼續迴圈。
- 執行程式碼,遇到巨集仁務放入巨集仁務佇列,遇到微任務放入微任務佇列,執行其他函式的時候放入執行上下文
- 執行上下文中全部執行完畢後,執行微任務佇列
- 微任務佇列執行完畢後,再到巨集仁務佇列中取出第一項放入執行上下文中執行
- 接著就不停迴圈1-3的步驟,這就是
瀏覽器環境
中的js事件環
//學了上面的事件環 我們來看一道面試題
setTimeout(function () {
console.log(1);
}, 0);
Promise.resolve(function () {
console.log(2);
})
new Promise(function (resolve) {
console.log(3);
});
console.log(4);
//上述程式碼的輸出結果是什麼???
複製程式碼
思考思考思考思考~~~
正確答案是3 4 1
,是不是和你想的一樣?我們來看一下程式碼的執行流程
// 遇到setTimeout 將setTimeout回撥放入巨集仁務佇列中
setTimeout(function () {
console.log(1);
}, 0);
// 遇到了promise,但是並沒有then方法回撥 所以這句程式碼會在執行過程中進入我們當前的執行上下文 緊接著就出棧了
Promise.resolve(function () {
console.log(2);
})
// 遇到了一個 new Promise,不知道大家還記不記得我們上一篇文章中講到Promise有一個原則就是在初始化Promise的時候Promise內部的構造器函式會立即執行 因此 在這裡會立即輸出一個3,所以這個3是第一個輸入的
new Promise(function (resolve) {
console.log(3);
});
// 然後輸入第二個輸出4 當程式碼執行完畢後回去微任務佇列查詢有沒有任務,發現微任務佇列是空的,那麼就去巨集仁務佇列中查詢,發現有一個我們剛剛放進去的setTimeout回撥函式,那麼就取出這個任務進行執行,所以緊接著輸出1
console.log(4);
複製程式碼
看到上述的講解,大家是不是都明白了,是不是直呼簡單~
那我們接下來來看看node環境中的事件執行環
NodeJs 事件環
瀏覽器的 Event Loop 遵循的是 HTML5 標準,而 NodeJs 的 Event Loop 遵循的是 libuv標準,因此呢在事件的執行中就會有一定的差異,大家都知道nodejs其實是js的一種runtime,也就是執行環境,那麼在這種環境中nodejs的api大部分都是通過回撥函式,事件釋出訂閱的方式來執行的,那麼在這樣的環境中我們程式碼的執行順序究竟是怎麼樣的呢,也就是我們不同的回撥函式究竟是怎麼分類的然後是按照什麼順序執行的,其實就是由我們的libuv所決定的。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製程式碼
我們先來看下這六個任務是用來幹什麼的
- timers: 這個階段執行setTimeout()和setInterval()設定的回撥。
- pending callbacks: 上一輪迴圈中有少數的 I/O callback會被延遲到這一輪的這一階段執行。
- idle, prepare: 僅內部使用。
- poll: 執行 I/O callback,在適當的條件下會阻塞在這個階段
- check: 執行setImmediate()設定的回撥。
- close callbacks: 執行比如socket.on('close', ...)的回撥。
我們再來看網上找到的一張nodejs執行圖,我們能看到圖中有六個步驟 ,當程式碼執行中如果我們遇到了這六個步驟中的回撥函式,就放入對應的佇列中,然後當我們同步人物執行完畢的時候就會切換到下一個階段,也就是timer階段,然後timer階段執行過程中會把這個階段的所有回撥函式全部執行了然後再進入下一個階段,需要注意的是我們在每次階段發生切換的時候都會先執行一次微任務佇列中的所有任務,然後再進入到下一個任務階段中去,所以我們就能總結出nodejs的事件環順序
- 同步程式碼執行,清空微任務佇列,執行timer階段的回撥函式(也就是setTimeout,setInterval)
- 全部執行完畢,清空微任務佇列,執行pending callbacks階段的回撥函式
- 全部執行完畢,清空微任務佇列,執行idle, prepare階段的回撥函式
- 全部執行完畢,清空微任務佇列,執行poll階段的回撥函式
- 全部執行完畢,清空微任務佇列,執行check階段的回撥函式(也就是setImmediate)
- 全部執行完畢,清空微任務佇列,執行close callbacks階段的回撥函式
- 然後迴圈1-6階段
那我們來練練手~~~
// 我們來對著我們的執行階段看看
let fs = require('fs');
// 遇到setTimeout 放入timer回撥中
setTimeout(function(){
Promise.resolve().then(()=>{
console.log('then1');
})
},0);
// 放入微任務佇列中
Promise.resolve().then(()=>{
console.log('then2');
});
// i/o操作 放入pending callbacks回撥中
fs.readFile('./text.md',function(){
// 放入check階段
setImmediate(()=>{
console.log('setImmediate')
});
// 放入微任務佇列中
process.nextTick(function(){
console.log('nextTick')
})
});
複製程式碼
首先同步程式碼執行完畢,我們先清空微任務,此時輸出then2,然後切換到timer階段,執行timer回撥,輸出then1,然後執行i/o操作回撥,然後清空微任務佇列,輸出nextTick,接著進入check階段,清空check階段回撥輸出setImmediate
所有的規則看著都雲裡霧裡,但是呢只要我們總結出來了規律,理解了他們的執行機制那麼我們就掌握了這些規則,好咯,今天又學了這麼多,不說了不說了,趕緊滾去寫業務程式碼了.............