從setTimeout理解JS運行機制

setTimeout()函數(shù):用來指定某個函數(shù)或某段代碼在多少毫秒之后執(zhí)行。它返回一個整數(shù),表示定時器timer的編號,可以用來取消該定時器。

例子

console.log(1);
setTimeout(function () {
    console.log(2);
}, 0);
console.log(3);

問:最后的打印順序是什么?(如果不了解js的運行機制就會答錯)

正確答案:1 3 2

解析:無論setTimeout的執(zhí)行時間是0還是1000,結(jié)果都是先輸出3后輸出2,這就是面試官常常考查的js運行機制的問題,接下來我們要引入一個概念,JavaScript 是單線程的。

一、JavaScript 單線程

JavasScript引擎是基于事件驅(qū)動和單線程執(zhí)行的,JS引擎一直等待著任務(wù)隊列中任務(wù)的到來,然后加以處理,瀏覽器無論什么時候都只有一個JS線程在運行程序,即主線程

通俗的說:JS在同一時間內(nèi)只能做一件事,這也常被稱為 “阻塞式執(zhí)行”。

任務(wù)隊列

那么單線程的JavasScript是怎么實現(xiàn)“非阻塞執(zhí)行”呢?

答:異步容易實現(xiàn)非阻塞,所以在JavaScript中對于耗時的操作或者時間不確定的操作,使用異步就成了必然的選擇。

諸如事件點擊觸發(fā)回調(diào)函數(shù)、ajax通信、計時器這種異步處理是如何實現(xiàn)的呢?

答:任務(wù)隊列

所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。

任務(wù)隊列:一個先進先出的隊列,它里面存放著各種事件和任務(wù)。

同步任務(wù)

同步任務(wù):在主線程上排隊執(zhí)行的任務(wù),只有前一個任務(wù)執(zhí)行完畢,才能執(zhí)行后一個任務(wù)。

  • 輸出
    如:console.log()
  • 變量的聲明
  • 同步函數(shù):如果在函數(shù)返回的時候,調(diào)用者就能夠拿到預(yù)期的返回值或者看到預(yù)期的效果,那么這個函數(shù)就是同步的。

異步任務(wù)

  • setTimeout和setInterval
  • DOM事件
  • Promise
  • process.nextTick
  • fs.readFile
  • http.get
  • 異步函數(shù):如果在函數(shù)返回的時候,調(diào)用者還不能夠得到預(yù)期結(jié)果,而是需要在將來通過一定的手段得到,那么這個函數(shù)就是異步的。

除此之外,任務(wù)隊列又分為macro-task(宏任務(wù))與micro-task(微任務(wù)),在ES5標準中,它們被分別稱為task與job。

宏任務(wù)

  1. I/O
  2. setTimeout
  3. setInterval
  4. setImmdiate
  5. requestAnimationFrame

微任務(wù)

  1. process.nextTick
  2. Promise
  3. Promise.then
  4. MutationObserver

宏任務(wù)和微任務(wù)的執(zhí)行順序

一次事件循環(huán)中,先執(zhí)行宏任務(wù)隊列里的一個任務(wù),再把微任務(wù)隊列里的所有任務(wù)執(zhí)行完畢,再去宏任務(wù)隊列取下一個宏任務(wù)執(zhí)行。

注:在當前的微任務(wù)沒有執(zhí)行完成時,是不會執(zhí)行下一個宏任務(wù)的。

二、setTimeout運行機制

setTimeout 和 setInterval的運行機制是將指定的代碼移出本次執(zhí)行,等到下一輪 Event Loop 時,再檢查是否到了指定時間。如果到了,就執(zhí)行對應(yīng)的代碼;如果不到,就等到再下一輪 Event Loop 時重新判斷。

這意味著,setTimeout指定的代碼,必須等到本次執(zhí)行的所有同步代碼都執(zhí)行完,才會執(zhí)行。

優(yōu)先關(guān)系:異步任務(wù)要掛起,先執(zhí)行同步任務(wù),同步任務(wù)執(zhí)行完畢才會響應(yīng)異步任務(wù)。

三、進階題

console.log('A');
setTimeout(function () {
    console.log('B');
}, 0);
while (1) {}

大家再猜一下這段程序輸出的結(jié)果會是什么?

答:A

注:建議先注釋掉while循環(huán)代碼塊的代碼,執(zhí)行后強制刪除進程,不然會造成“假死”。

同步隊列輸出A之后,陷入while(true){}的死循環(huán)中,異步任務(wù)不會被執(zhí)行。

類似的,有時addEventListener()方法監(jiān)聽點擊事件click,用戶點了某個按鈕會卡死,就是因為當前JS正在處理同步隊列,無法將click觸發(fā)事件放入執(zhí)行棧,不會執(zhí)行,出現(xiàn)“假死”。

四、定時獲取接口更新數(shù)據(jù)

for (var i = 0; i < 4; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

輸出結(jié)果為,隔1s后一起輸出:4 4 4 4

for循環(huán)是一個同步任務(wù),為什么連續(xù)輸出四個4?

答:因為有隊列插入的時間,即使執(zhí)行時間從1000改成0,還是輸出四個4。

那么這個問題是如何產(chǎn)生和解決的呢?請接著閱讀

異步隊列執(zhí)行的時間

執(zhí)行到異步任務(wù)的時候,會直接放到異步隊列中嗎?
答案是不一定的。

因為瀏覽器有個定時器(timer)模塊,定時器到了執(zhí)行時間才會把異步任務(wù)放到異步隊列

for循環(huán)體執(zhí)行的過程中并沒有把setTimeout放到異步隊列中,只是交給定時器模塊了。4個循環(huán)體執(zhí)行速度非常快(不到1毫秒)。定時器到了設(shè)置的時間才會把setTimeout語句放到異步隊列中。

即使setTimeout設(shè)置的執(zhí)行時間為0毫秒,也按4毫秒算

這就解釋了上題為什么會連續(xù)輸出四個4的原因。

HTML5 標準規(guī)定了setTimeout()的第二個參數(shù)的最小值,即最短間隔,不得低于4毫秒。如果低于這個值,就會自動增加。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。

利用閉包實現(xiàn) setTimeout 間歇調(diào)用

for (let i = 0; i < 4; i++) {
    (function (j) {
        setTimeout(function () {
            console.log(j);
        }, 1000 * i)
    })(i);
}

執(zhí)行后,會隔1s輸出一個值,分別是:0 1 2 3

  • 此方法巧妙利用IIFE聲明即執(zhí)行的函數(shù)表達式(立即執(zhí)行函數(shù))來解決閉包造成的問題。
  • 將var改為let,使用了ES6語法。

這里也可以用setInterval()方法來實現(xiàn)間歇調(diào)用。

詳見:setTimeout和setInterval的區(qū)別

利用JS中基本類型的參數(shù)傳遞是按值傳遞的特征實現(xiàn)

var output = function (i) {
    setTimeout(function () {
        console.log(i);

    }, 1000 * i)
}
for (let i = 0; i < 4; i++) {
    output(i);
}

執(zhí)行后,會隔1s輸出一個值,分別是:0 1 2 3

實現(xiàn)原理:傳過去的i值被復(fù)制了。

基于Promise的解決方案

const tasks = [];

const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(i);
        resolve();
    }, 1000 * i);

});

//生成全部的異步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}
//同步操作完成后,輸出最后的i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(i);
    }, 1000)
})

執(zhí)行后,會隔1s輸出一個值,分別是:0 1 2 3 4 5

優(yōu)點:提高了代碼的可讀性。

注意:如果沒有處理Promise的reject,會導(dǎo)致錯誤被丟進黑洞。

使用ES7中的async await特性的解決方案(推薦)

const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => { //聲明即執(zhí)行的async
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(i);
    }

    await sleep(1000);
    console.log(i);

})();

執(zhí)行后,會隔1s輸出一個值,分別是:0 1 2 3 4 5

五、事件循環(huán) Event Loop

圖解Event Loop

主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為Event Loop

有時候 setTimeout明明寫的延時3秒,實際卻5,6秒才執(zhí)行函數(shù),這又是因為什么?

答:setTimeout 并不能保證執(zhí)行的時間,是否及時執(zhí)行取決于 JavaScript 線程是擁擠還是空閑

瀏覽器的JS引擎遇到setTimeout,拿走之后不會立即放入異步隊列,同步任務(wù)執(zhí)行之后,timer模塊會到設(shè)置時間之后放到異步隊列中。js引擎發(fā)現(xiàn)同步隊列中沒有要執(zhí)行的東西了,即運行棧空了就從異步隊列中讀取,然后放到運行棧中執(zhí)行。所以setTimeout可能會多了等待線程的時間。

這時setTimeout函數(shù)體就變成了運行棧中的執(zhí)行任務(wù),運行棧空了,再監(jiān)聽異步隊列中有沒有要執(zhí)行的任務(wù),如果有就繼續(xù)執(zhí)行,如此循環(huán),就叫Event Loop。

六、總結(jié)

JavaScript通過事件循環(huán)和瀏覽器各線程協(xié)調(diào)共同實現(xiàn)異步。同步可以保證順序一致,但是容易導(dǎo)致阻塞;異步可以解決阻塞問題,但是會改變順序性。

知識點梳理:

  • 理解JS的單線程的概念:一段時間內(nèi)做一件事
  • 理解任務(wù)隊列:同步任務(wù)、異步任務(wù)
  • 理解 Event Loop
  • 理解哪些語句會放入異步任務(wù)隊列
  • 理解語句放入異步任務(wù)隊列的時機

最后,希望大家閱后有所收獲。

參考文獻

The Node.js Event Loop, Timers, and process.nextTick()

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內(nèi)容