歡迎關注 jsliang 的文檔庫 —— 一個窮盡一輩子更新的倉庫,查看更多技術、理財、健身文章:github.com/LiangJunron…html
不折騰的前端,和鹹魚有什麼區別前端
目錄 |
---|
一 目錄 |
二 前言 |
三 Event Loop |
四 瀏覽器 Event Loop |
4.1 示例 1 |
4.2 示例 2 |
4.3 示例 3 |
4.4 小結 |
五 Node.js Event Loop |
5.1 setTimeout & setImmediate |
5.2 process.nextTick() |
5.3 示例 1 |
5.4 示例 2 |
5.5 小結 |
六 總結 |
七 參考文獻 |
返回目錄node
Hello 小夥伴們早上好、中午好、下午好、晚上好、凌晨好~git
在平常工做中,你有沒有碰到過這種疑惑:程序員
1 2 3
?for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// console:
// 3
// 3
// 3
複製代碼
jsliang
?let name;
setTimeout(() => {
name = '梁峻榮';
console.log(name);
}, 1000);
if (name) {
name = 'jsliang';
console.log(name);
}
// console: '梁峻榮'
複製代碼
孩子沒娘,說來話長。es6
既然說來話長,jsliang 只能嘗試長話短說了:github
這麼一說,咱也好對文章進行劃分了:web
OK,Let's go!面試
返回目錄算法
答:
首先,咱們須要知道的是:JavaScript 是單線程的。
單線程意味着,全部任務都須要排隊,前一個任務結束,纔會執行後一個任務。
假設 jsliang 和 JavaScript 同樣一次只能作一件事,那麼大概就是以下圖所示。
而這種 主線程從 「任務隊列」 中讀取執行事件,不斷循環重複的過程,就被稱爲 事件循環(Event Loop)。
而後,若是前一個任務耗時很長,後一個任務就不得不一直等着,那麼咱們確定要對這種狀況作一些特殊處理,畢竟不少時候咱們並非徹底但願它如此執行。
因此爲了協調事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網絡(networking)等,用戶代理(user agent)必須使用事件循環(event loops)。
這樣,在瞭解 瀏覽器 Event Loop 和 Node.js Event Loop 的狀況下,咱們就能夠了解它的執行過程。
經過自身的瞭解,來處理一些較爲棘手的問題。
爲了加深小夥伴們的印象,能夠看下圖:
jsliang 平常中,強制被加上了 「被豆豆媽打」(廢話,豆豆那麼可愛,你怎麼能夠打豆豆)。
固然,這個被打的順序也不必定是在後面,可能打多兩次後,「睡覺」 完以後就是 「被豆豆媽打」 了。
經過這個解釋,小夥伴們應該知道爲啥有 瀏覽器 Event Loop 和 Node.js Event Loop 了。
等等,你剛纔說到了 瀏覽器 Event Loop 和 Node.js Event Loop,爲何都是關於 JavaScript 的,在這兩部分都不同呢?
你說了跟沒說同樣,爲何會這樣你沒有解釋啊!
好的,說得再仔細點:
libuv 是一個多平臺支持庫,主要用於異步 I/O。它最初是爲 Node.js 開發的,如今 Luvit、Julia、pyuv 和其餘的框架也使用它。Github - libuv 倉庫
恍然大悟,的確是不同的啊!
因此,我們得將這兩個 Event Loop 區分開來,它們是不同的東東哈~
最後,我們解疑開頭的兩個問題,爲何會這樣子,有沒辦法解決?
1 2 3
?for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// console:
// 3
// 3
// 3
複製代碼
這道題是面試常備題,它是個頗有意思的問題,不只可讓面試官跟你閒聊到 Event Loop,也能夠閒聊下 var let const
。
爲此,jsliang 特地錄製了一個 GIF,但願能幫助小夥伴進一步探索這個機制:
軟件是 VS Code,調試方式是 Node.js
請仔細觀看 GIF 圖:
for
遍歷的時候,它先執行了和 setTimeout
同級的 console
,而後往下執行,到 setTimeout
的時候,跳過了(放到某個位置)setTimeout
,依次打印了 0, 1, 2
。setTimeout
開始執行,可是這時候的 i
的值,通過前面的 i++
後,變成了 3
(for
停止循環後,i
已是 3
了)。因此,再依次打印了 3 3 3
。就是說,先走了正常的 for
,而後碰到 setTimeout
時,將 setTimeout
依次放到了異次元,最後走完 for
後,再將異次元中的的 setTimeout
放出,依次將數字給輸出了。
這個執行機制,就是 Event Loop 的影響,恍然大悟有木有~
這個問題的精妙之處在於,它不只能夠問你關於 Event Loop 的部分,還能夠考察你對於 ES6 的 let
和 ES5 的 var
的區分,由於它有一個解決方式就是使用了 ES6 的 let
。
解決這個問題以前,不妨思考下下面的輸出:
for (var i = 0; i < 3; i++) {
}
for (let j = 0; j < 3; j++) {
}
console.log(i);
console.log(j);
複製代碼
若是小夥伴對 ES6 有些許瞭解,應該不難猜出:
3
ReferenceError: j is not defined
複製代碼
是否是有些想法,那麼我們再看下下面的解決方法,再進行總結:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// console:
// 0
// 1
// 2
複製代碼
是的,將 var i
改爲了 let i
後,輸出的結果依次是 0 1 2
了。
爲何呢?簡單回覆就是:
let
在 for
中造成了獨特的做用域塊,當前的 i
只在本輪循環中有效,而後 setTimeout
會找到本輪最接近的 i
,從而做出了正確的輸出。
而咱們經過 var
進行的定義,它會污染全局變量,因此在 for
外層,還能夠看到 i
的值。
固然,講到這裏,你可能仍是不太清楚更細節的區分,亦或者面試官進一步問你 var let const
的區分了,你要怎麼更好回答?
看看阮一峯大佬的 ES6 文檔吧:es6.ruanyifeng.com/#docs/let
這裏就不哆嗦了,有空我再將 ES6 這塊內容整理到個人文檔庫中,歡迎持續關注 jsliang 的文檔庫:github.com/LiangJunron…
梁峻榮
?let name;
setTimeout(() => {
name = 'jsliang';
console.log(name);
}, 1000);
if (name) {
name = '梁峻榮';
console.log(name);
}
// console: 'jsliang'
複製代碼
當你瞭解產生疑惑一的緣由後,疑惑二也就不破而解了。
咱們但願的是 JavaScript 按照咱們須要的順序寫,結果它並無,就是由於受到了 Event Loop 的影響。
JavaScript 在碰到 setTimeout
的時候,會將它封印進異次元,只有等全部正常的語句(if
、for
……)執行完畢後,纔會將它從異次元解封,輸出最終結果。
咦,這就有意思了,瀏覽器的異次元和 Node.js 的異次元都是怎樣的呢?咱們一塊兒往下看。
在講解瀏覽器的 Event Loop 前,咱們須要先了解一下 JavaScript 的運行機制:
而 JavaScript 的異步任務,還細分兩種任務:
script
(總體代碼)、setTimeout
、setInterval
、XMLHttpRequest.prototype.onload
、I/O
、UI 渲染Promise
、MutationObserver
這麼講是不太容易理解的,我們上圖:
圖較大,若是是公衆號看的小夥伴,能夠點擊【閱讀原文】看全圖
好的,若是小夥伴們看不清楚,那麼我們仍是經過代碼來進行講解,畢竟以上屬於 jsliang 我的理解,是從 15 篇以上文章和本身觀察代碼運行總結出來的。
那麼,上代碼~
示例 1
// 位置 1
setTimeout(function () {
console.log('timeout1');
}, 1000);
// 位置 2
console.log('start');
// 位置 3
Promise.resolve().then(function () {
// 位置 5
console.log('promise1');
// 位置 6
Promise.resolve().then(function () {
console.log('promise2');
});
// 位置 7
setTimeout(function () {
// 位置 8
Promise.resolve().then(function () {
console.log('promise3');
});
// 位置 9
console.log('timeout2')
}, 0);
});
// 位置 4
console.log('done');
複製代碼
提問:請指出上面代碼的輸出結果?
回答:
這是經典的面試題型,因此我們看到不用慌,先拿咱們上面的點,區分下分宏任務和微任務:
script
(總體代碼)、setTimeout
、setInterval
、XMLHttpRequest.prototype.onload
、I/O
、UI 渲染Promise
、MutationObserver
OK,開始走流程:
若是你以爲文字很差理解,請往下翻,有 GIF 圖演示!!!
script
(總體代碼),先看【位置 1】,屬於宏任務 setTimeout
下的,因此作個標記,待會回來執行。script
(總體代碼)下的無阻礙代碼,直接執行便可。script
(總體代碼)下的微任務,因此我們作個標記,走完文件全部代碼後,優先執行微任務,再執行宏任務。script
(總體代碼)下的無阻礙代碼,直接執行便可。這樣,第一波步驟,咱們輸出的是【位置 2】的 start
和【位置 4】的 done
。
咱們接着走:
script
(總體代碼)下的微任務,即【位置 3】
到這一步,咱們就走完了 script
(總體代碼)及之下的全部微任務了。
這時候,咱們會說,【位置 1】和【位置 7】都被丟到任務隊列了,是否是【位置 1】先走呢?
答案爲:不是的。
一樣的 setTimeout
,jsliang 在測試的時候,就發現它們的輸出結果在各個環境都有本身的流程,有時候先走【位置 7】,再走【位置 1】;而有時候先走【位置 1】,再走【位置 7】。
固然,若是你指定是在 Chrome
的控制檯輸出一下上面的代碼,那就是先【位置 7】,再【位置 1】~
因此答案是:
start
done
promise1
promise2
timeout2
promise3
timeout1
複製代碼
你猜對沒有?
沒有能夠看下 GIF 圖加深印象:
在上面,jsliang 花費了許多口水,講了一些繁雜冗餘的步驟,因此下面這個示例,請小夥伴們先自行猜設,得出結論後再翻看答案和調試 GIF~
示例 2
console.log("script start");
setTimeout(function() {
console.log("setTimeout---0");
}, 0);
setTimeout(function() {
console.log("setTimeout---200");
setTimeout(function() {
console.log("inner-setTimeout---0");
});
Promise.resolve().then(function() {
console.log("promise5");
});
}, 200);
Promise.resolve()
.then(function() {
console.log("promise1");
})
.then(function() {
console.log("promise2");
});
Promise.resolve().then(function() {
console.log("promise3");
});
console.log("script end");
複製代碼
script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
複製代碼
最後再看一個示例:
示例 3
setTimeout(function() {
console.log(4);
}, 0);
const promise = new Promise(function executor(resolve) {
console.log(1);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
複製代碼
1
2
3
5
4
複製代碼
若是不經常使用 Promise
的小夥伴,可能對此感到疑惑,爲啥不是:3 1 2 5 4
?
手動滑稽,別問,問就是進一步探索 Promise
:
固然,還沒將全部探索結果更新,若是有小夥伴催更會加快速度,歡迎留言或者私聊催更,哈哈~
這樣,咱們就經過 3 個示例,大體瞭解了瀏覽器的 Event Loop。
固然,實際應用中的代碼,何止這麼簡單,甚至有時候,面試官給你的面試題,也會讓你瞠目結舌。
因此,這裏我們廢話兩點:
if...else...
,再走 Promise
……可是,詳細到每一個 point
都記下來,這裏不推薦。大人,時代在進步,記住死的不如多在業務實踐中嘗試,取最新的知識。碰到問題不要慌,程序員,折騰就對了~
那麼,下面我們吐槽下 Node.js 的 Event Loop。
說實話,看完 Node 官網和大佬們關於 Node.js 的 Event Loop 講解,讓我想起了 Vue、React、微信小程序 的【生命週期】,再聯想到咱們的人生彷彿就像被寫死的程序同樣週期性、事件性運行,很是可惡,哈哈~
上面咱們講解過:Node.js 的 Event Loop 是基於 libuv。libuv 已經對 Event Loop 做出了實現。
那麼其機制是怎樣子的呢?看圖:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製代碼
關於這 6 個階段,官網描述爲:
setTimeout()
和 setInterval()
的調度回調函數。setImmediate()
調度的以外),其他狀況 Node
將在適當的時候在此阻塞。setImmediate()
回調函數在這裏執行。socket.on('close', ...)
。固然,這裏 jsliang 並不想多此一舉,將官網或者其餘大佬的文章照搬過來講是本身的,推薦小夥伴們閱讀官網關於 Event Loop 的各個階段的描述,以期在工做中有所使用:
Node.js 在不停的探索中,也會有所更新,因此正應了 jsliang 在瀏覽器 Event Loop 中的小結所說:不要限定死本身的知識點,與時俱進纔是王道。
Node.js v9.5.0 Event Loop
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
可是,迫於生活所需,有些時候,前端面試官仍是會跟你扯 setTimeout & setImmediate
和 process.nextTice()
。
n
毫秒後執行定時器裏面的內容。setTimeout
和 setInterval
有些小弊端,因此設計了個 setImmediate
,該方法被設計爲一旦在當前輪詢階段完成,就執行這個腳本。固然,光說無益,看代碼:
index.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
複製代碼
猜想下在 VS Code 中執行 node index.js
命令會發生什麼?
結局 1
immediate
timeout
複製代碼
結局 2
timeout
immediate
複製代碼
事實上這兩個結局都是會存在的,看似 happy ending,可是有的小夥伴可能內心鬧翻天。
按照官網的解釋:
setImmediate
老是被有限調用。const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
雖然官方解釋的很 巧妙,可是無論你懂不懂,反正我以爲有點扯淡。
最後再來句官方總結:
setImmediate()
相對於 setTimeout
的主要優點是:若是 setImmediate()
是在 I/O 週期內被調度的,那麼它將會在任何的定時器以前執行,跟這裏存在多少個定時器無關。enm...後面若是我具體使用 Node.js 的時候,我再進一步觀察吧,至於如今,我仍是先了解下便可。
nextTick
比較特殊,它存有本身的隊列。
而且,它獨立於 Event Loop,不管 Event Loop 處於何種階段,都會在階段結束的時候清空 nextTick
隊列。
還有須要注意的是:process.nextTick()
優先於其餘的微任務(microtask)執行。
固然,若是你對此有所興趣,你能夠進一步探索源碼,或者觀察大佬們探索源碼:
沒有使用就沒有發言權,做爲一個 Node.js 菜雞,這裏就不妄加評論分析了。
下面開始示例,咱們看下 Node.js 的 Event Loop 有何差別:
示例 1
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
});
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function() {
console.log("promise2");
});
});
複製代碼
若是你還記得上面講解的瀏覽器的 Event Loop,你可能會將答案直接寫成:
瀏覽器 Event Loop 輸出:
timer1
promise1
timer2
promise2
複製代碼
是的你是對的,那就是瀏覽器的 Event Loop,到了 Node.js 這塊,就有不一樣變化了:
Node.js Event Loop 輸出:
timer1
timer2
promise1
promise2
複製代碼
嘗試接受它!
而後大聲默唸:根據具體環境進行對應觀察和得出結論。
下面我們再看一個示例:
示例 2
setTimeout(function () {
console.log(1);
});
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
setImmediate(function () {
console.log(6)
})
console.log('end');
複製代碼
node index.js
2
4
end
3
5
1
6
複製代碼
這裏不打算解析,由於我怕初識 Event Loop 的小夥伴看完解釋後懵逼,而後搞混淆了。
實話:我也不敢解析,由於我就是 Node.js 菜雞
終上所述,咱們進行小結:
Node 端事件循環中的異步隊列也是這兩種:Macrotask(宏任務)隊列和 Microtask(微任務)隊列。
setTimeout
、setInterval
、setImmediate
、script
(總體代碼)、 I/O 操做等。process.nextTick
、new Promise().then(回調)
等。OK,我們就探索了一遍 Node.js 的 Event Loop 啦,可是由於咱還成就不了 Node.js 工程師,因此咱就不對其進行詳細探索,以避免和瀏覽器的 Event Loop 混淆了。
感興趣的小夥伴能夠自行探索咯~
若是你看到這裏,你已經近乎懵逼,那麼,仍是那個建議:
你不能徹底保證你的記憶力是 OK 的,因此你只須要知道有這個問題,而後在工做中實踐解決便可。
enm...因此你看完了一篇水文,惟一的做用是讓你面試的時候,能愉快地玩耍一些簡單題目~
哈哈,Good luck.
若是你以爲個人文章還不錯,想持續關注或者加我微信好友,歡迎前往 github.com/LiangJunron… 進行 star 或者加微信。
感謝如下大佬們的文章,讓我受益頗多。
並在他們創做的基礎上,基於本身的想法,進行了整合。
不折騰的前端,和鹹魚有什麼區別!
jsliang 會天天更新一道 LeetCode 題解,從而幫助小夥伴們夯實原生 JS 基礎,瞭解與學習算法與數據結構。
浪子神劍 會天天更新面試題,以面試題爲驅動來帶動你們學習,堅持天天學習與思考,天天進步一點!
掃描上方二維碼,關注 jsliang 的公衆號(左)和 浪子神劍 的公衆號(右),讓咱們一塊兒折騰!
jsliang 的文檔庫 由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的做品創做。
本許可協議受權以外的使用權限能夠從 creativecommons.org/licenses/by… 處得到。