深入理解JavaScript運(yùn)行機(jī)制

JavaScript單線程機(jī)制

JavaScript的一個(gè)語(yǔ)言特性(也是這門(mén)語(yǔ)言的核心)就是單線程蚂维。什么是單線程呢呜魄?簡(jiǎn)單地說(shuō)就是同一時(shí)間只能做一件事昼接,當(dāng)有多個(gè)任務(wù)時(shí),只能按照一個(gè)順序一個(gè)完成了再執(zhí)行下一個(gè)

為什么JS是單線程的呢绑咱?

  • JS最初被設(shè)計(jì)用在瀏覽器中绰筛,作為瀏覽器腳本語(yǔ)言,JavaScript的主要用途是與用戶互動(dòng)描融,以及操作DOM
  • 如果瀏覽器中的JS是多線程的铝噩,會(huì)帶來(lái)很復(fù)雜的同步問(wèn)題
    • 比如,假定JavaScript同時(shí)有兩個(gè)線程稼稿,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容薄榛,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn)讳窟,這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)让歼?
  • 所以為了避免復(fù)雜性,JavaScript從誕生起就是單線程

為了提高CPU的利用率丽啡,HTML5提出Web Worker標(biāo)準(zhǔn)谋右,允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制补箍,且不得操作DOM改执。所以這個(gè)標(biāo)準(zhǔn)并沒(méi)有改變JavaScript單線程的本質(zhì)

任務(wù)隊(duì)列

同步和異步

同步和異步關(guān)注的是消息通知機(jī)制

  • 同步:發(fā)出調(diào)用后,沒(méi)有得到結(jié)果之前坑雅,該調(diào)用不返回辈挂,一旦調(diào)用返回,就得到返回值了裹粤。 簡(jiǎn)而言之就是調(diào)用者主動(dòng)等待這個(gè)調(diào)用的結(jié)果

  • 異步:調(diào)用者在發(fā)出調(diào)用后這個(gè)調(diào)用就直接返回了终蒂,所以沒(méi)有返回結(jié)果。換句話說(shuō)當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后遥诉,調(diào)用者不會(huì)立刻得到結(jié)果拇泣,而是調(diào)用發(fā)出后,被調(diào)用者通過(guò)狀態(tài)矮锈、通知或回調(diào)函數(shù)處理這個(gè)調(diào)用霉翔。

阻塞和非阻塞

阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時(shí)的狀態(tài)

  • 阻塞調(diào)用是指調(diào)用結(jié)果返回之前苞笨,當(dāng)前線程會(huì)被掛起债朵。調(diào)用線程只有在得到結(jié)果之后才會(huì)返回
  • 非阻塞調(diào)用指在不能立刻得到結(jié)果之前子眶,該調(diào)用不會(huì)阻塞當(dāng)前線程

單線程意味著同一時(shí)間只能進(jìn)行一件事情,前面的事情結(jié)束才能執(zhí)行后面的事件.當(dāng)碰到需要時(shí)間的IO事件的時(shí)候問(wèn)題就來(lái)了,必須等到這些結(jié)束后才往下進(jìn)行,但這時(shí)CPU是閑著的.這樣浪費(fèi)了很多計(jì)算機(jī)的性能.

JavaScript語(yǔ)言的設(shè)計(jì)者意識(shí)到,這時(shí)主線程完全可以不管IO設(shè)備序芦,掛起處于等待中的任務(wù)壹店,先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果芝加,再回過(guò)頭硅卢,把掛起的任務(wù)繼續(xù)執(zhí)行下去.

于是,所有任務(wù)可以分成兩種藏杖,一種是同步任務(wù)(synchronous)将塑,另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是蝌麸,在主線程上排隊(duì)執(zhí)行的任務(wù)点寥,只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)来吩;異步任務(wù)指的是敢辩,不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列"(task queue)的任務(wù)弟疆,只有"任務(wù)隊(duì)列"通知主線程戚长,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行怠苔。
(1)所有同步任務(wù)都在主線程上執(zhí)行同廉,形成一個(gè)執(zhí)行棧(execution context stack)
(2)主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)柑司。只要異步任務(wù)有了運(yùn)行結(jié)果迫肖,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列"攒驰,看看里面有哪些事件蟆湖。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài)玻粪,進(jìn)入執(zhí)行棧隅津,開(kāi)始執(zhí)行
(4)主線程不斷重復(fù)上面的第三步

Event Loop

主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的奶段,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))


eventloop.png

上圖中饥瓷,主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack)痹籍,堆中可存放對(duì)象呢铆, 棧中可存放變量,函數(shù)蹲缠,函數(shù)指針棺克,代碼語(yǔ)句等

棧中的代碼調(diào)用各種外部API悠垛,它們?cè)?任務(wù)隊(duì)列"中加入各種事件(click,load娜谊,done)
WebAPIs都是單獨(dú)線程确买,跟組件中的不一樣,不會(huì)阻塞主線程執(zhí)行纱皆,比如獲取后臺(tái)數(shù)據(jù)湾趾,若同步就阻塞了,比如HTTP請(qǐng)求又開(kāi)辟了一個(gè)線程

當(dāng)執(zhí)行棧中的任務(wù)完成后派草,主線程會(huì)去讀取事件隊(duì)列(先進(jìn)先出)搀缠,執(zhí)行相應(yīng)的回調(diào)函數(shù)

舉個(gè)例子,查看以下代碼

function read(){
    console.log(1);
    setTimeout(function (){
    console.log(2);
    setTimeout(function (){
    console.log(4)
    });
    });
    setTimeout(function (){
    console.log(5)
    })
    console.log(3);
}
read();

代碼執(zhí)行結(jié)果:1 3 2 5 4

先執(zhí)行同步代碼打印1近迁,3艺普,setTimeout異步代碼放到事件隊(duì)列中,先放的先執(zhí)行鉴竭,后放的后執(zhí)行

定時(shí)器

"任務(wù)隊(duì)列"可以放置定時(shí)事件歧譬,即指定某些代碼在多少時(shí)間之后執(zhí)行

定時(shí)器功能主要由setTimeout()和setInterval()這兩個(gè)函數(shù)來(lái)完成,它們的內(nèi)部運(yùn)行機(jī)制完全一樣搏存,區(qū)別在于前者指定的代碼是一次性執(zhí)行瑰步,后者則為反復(fù)執(zhí)行,主要以setTimeout舉例說(shuō)明

setTimeout()接受兩個(gè)參數(shù)祭埂,第一個(gè)是回調(diào)函數(shù)面氓,第二個(gè)是推遲執(zhí)行的毫秒數(shù)

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

執(zhí)行結(jié)果是:1 3 2

setTimeout()將事件放到等待任務(wù)隊(duì)里中兵钮,當(dāng)主任務(wù)隊(duì)列的任務(wù)執(zhí)行完后蛆橡,再執(zhí)行等待任務(wù)隊(duì)列,等待任務(wù)隊(duì)里中先返回的先執(zhí)行

setTimeout()有時(shí)候明明寫(xiě)的延時(shí)3秒掘譬,實(shí)際卻5泰演,6秒才執(zhí)行函數(shù),這是怎么回事呢葱轩?

  • setTimeout()只是將事件插入了“任務(wù)隊(duì)列”睦焕,必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)靴拱。要是當(dāng)前代碼耗時(shí)很長(zhǎng)垃喊,有可能要等很久,所以并沒(méi)有辦法保證回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行

Promise與process.nextTick(callback)

除了廣義的同步任務(wù)和異步任務(wù)袜炕,我們對(duì)任務(wù)有更精細(xì)的定義:

  • macro-task(宏任務(wù)):包括整體代碼script本谜,setTimeout,setInterval
  • micro-task(微任務(wù)):Promise偎窘,process.nextTick
- process.nextTick:在事件循環(huán)的下一次循環(huán)中調(diào)用 callback 回調(diào)函數(shù)乌助。效果是將一個(gè)函數(shù)推遲到代碼書(shū)寫(xiě)的下一個(gè)同步方法執(zhí)行完畢時(shí)或異步方法的事件回調(diào)函數(shù)開(kāi)始執(zhí)行時(shí)溜在;與setTimeout(fn, 0) 函數(shù)的功能類(lèi)似,但它的效率高多了

不同類(lèi)型的任務(wù)會(huì)進(jìn)入對(duì)應(yīng)的Event Queue他托,比如 setTimeout 和 setInterval 會(huì)進(jìn)入相同的Event Queue

事件循環(huán)的順序掖肋,決定js代碼的執(zhí)行順序。進(jìn)入整體代碼(宏任務(wù))后赏参,開(kāi)始第一次循環(huán)志笼。接著執(zhí)行所有的微任務(wù)。然后再次從宏任務(wù)開(kāi)始把篓,找到其中一個(gè)任務(wù)隊(duì)列執(zhí)行完畢籽腕,再執(zhí)行所有的微任務(wù)。

事件循環(huán)纸俭,宏任務(wù)皇耗,微任務(wù)的關(guān)系如下所示:

  • 宏任務(wù)=>執(zhí)行結(jié)束=>有可執(zhí)行的微任務(wù)=>執(zhí)行所有微任務(wù)=>開(kāi)始新的宏任務(wù)
  • 宏任務(wù)=>執(zhí)行結(jié)束=>沒(méi)有可執(zhí)行的微任務(wù)=>開(kāi)始新的宏任務(wù)

我們用一段代碼說(shuō)明:

setTimeout(function () {
    console.log('setTimeout');
});
new Promise(function (resolve) {
    console.log('promise');
}).then(function () {
    console.log('then');
});
console.log('console');
  • 這段代碼作為宏任務(wù),進(jìn)入主線程
  • 先遇到 setTimeout 揍很,那么將其回調(diào)函數(shù)注冊(cè)后分發(fā)到宏任務(wù)Event Queue
  • 接下來(lái)遇到了 Promise 郎楼, new Promise 立即執(zhí)行, then 函數(shù)分發(fā)到微任務(wù)Event Queue
  • 遇到 console.log() 窒悔,立即執(zhí)行
    -整體代碼script作為第一個(gè)宏任務(wù)執(zhí)行結(jié)束呜袁,看看有哪些微任務(wù)?我們發(fā)現(xiàn)了 then 在微任務(wù)Event Queue里面執(zhí)行
  • 第一輪事件循環(huán)結(jié)束了简珠,我們開(kāi)始第二輪循環(huán)阶界,當(dāng)然要從宏任務(wù)Event Queue開(kāi)始。我們發(fā)現(xiàn)了宏任務(wù)Event Queue中 setTimeout 對(duì)應(yīng)的回調(diào)函數(shù)聋庵,立即執(zhí)行
  • 結(jié)束

我們?cè)倏聪乱欢未a說(shuō)明:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)

以上代碼執(zhí)行結(jié)果:1 2 TIMEOUT FIRED

上面代碼中膘融,由于process.nextTick方法指定的回調(diào)函數(shù),總是在當(dāng)前"執(zhí)行棧"的尾部觸發(fā)祭玉,所以不僅函數(shù)A比setTimeout指定的回調(diào)函數(shù)timeout先執(zhí)行氧映,而且函數(shù)B也比timeout先執(zhí)行。這說(shuō)明脱货,如果有多個(gè)process.nextTick語(yǔ)句(不管它們是否嵌套)岛都,將全部在當(dāng)前"執(zhí)行棧"執(zhí)行

我們?cè)倏聪乱欢未a說(shuō)明:

function a() {
    setTimeout(function () {
        console.log('a2');
    }, 0);
    process.nextTick(function () {
        console.log('a1')
    });
}
function b() {
    process.nextTick(function () {
        console.log('b1');
    })
}
a();
b();

一個(gè)函數(shù)執(zhí)行會(huì)形成一個(gè)執(zhí)行棧,任務(wù)隊(duì)列里的回調(diào)函數(shù)每次只取一個(gè)振峻,它執(zhí)行的時(shí)候會(huì)形成一個(gè)執(zhí)行棧臼疫,當(dāng)你第一次運(yùn)行這個(gè)腳本的時(shí)候,這個(gè)腳本的里所有的同步代碼都會(huì)在一個(gè)執(zhí)行棧里

  • a的執(zhí)行和b的執(zhí)行在一個(gè)執(zhí)行棧里扣孟,它們共同在第一個(gè)宏任務(wù)中
  • a執(zhí)行時(shí)候烫堤,會(huì)把a(bǔ)2放入宏任務(wù)隊(duì)列,把a(bǔ)1放入微任務(wù)隊(duì)列。
  • b執(zhí)行的時(shí)候塔逃,把b1放入微任務(wù)隊(duì)列
  • -------------------第一個(gè)宏任務(wù)執(zhí)行完畢-------------------------
  • 宏任務(wù)執(zhí)行完畢后會(huì)把微任務(wù)隊(duì)列清空讯壶,也就是把a(bǔ)1 和b1都執(zhí)行,輸出a1和b1
  • -------------------第一個(gè)微任務(wù)隊(duì)列清空--------------------------
  • 然后從宏任務(wù)隊(duì)列中取出下一個(gè)宏任務(wù),也就是a2執(zhí)行.輸出a2

為什么一個(gè)宏任務(wù)要搭配處理一個(gè)微任務(wù)
因?yàn)檫@樣最合理湾盗,微任務(wù)就是在有空時(shí)需要立即執(zhí)行的任務(wù)伏蚊,宏任務(wù)相比微任務(wù)可以滯后執(zhí)行。他們雖然都屬于異步任務(wù)格粪,但是通過(guò)這種優(yōu)先級(jí)的設(shè)置達(dá)到了控制異步回調(diào)執(zhí)行順序的目的躏吊。值得注意的是:同步代碼執(zhí)行完會(huì)先清空微任務(wù),然后取出宏任務(wù)隊(duì)列里的第一個(gè)事件對(duì)應(yīng)的回調(diào)到執(zhí)行棧執(zhí)行帐萎,然后再清空一次微任務(wù)比伏,如此循環(huán)...

通過(guò)以上三段代碼,您是否對(duì)JS的執(zhí)行順序有所了解呢

我們來(lái)分析一段較復(fù)雜的代碼疆导,看看你是否真的掌握了js的執(zhí)行機(jī)制

console.log('main1');
setTimeout(function () {
    console.log('setTimeout');
    process.nextTick(function () {
        console.log('process.nextTick2');
    });
}, 0);
new Promise(function (resolve, reject) {
    console.log('promise');
    resolve();
}).then(function () {
    console.log('promise then');
});
process.nextTick(function () {
    console.log('process.nextTick1');
});
console.log('main2');

以上代碼的執(zhí)行結(jié)果是:main1=>promise=>main2=>process.nextTick1=>promise then=>setTimeout=>process.nextTick2

  • 系統(tǒng)啟動(dòng)執(zhí)行腳本赁项,這個(gè)腳本就是一個(gè)宏任務(wù),執(zhí)行代碼塊中所有的同步代碼澈段,輸出main1
  • next1放入微任務(wù)悠菜,setTimeout+nextTick2(下一輪)放入宏任務(wù)隊(duì)列
  • promise構(gòu)造函數(shù)部分是同步的,立刻執(zhí)行輸出promise败富,promise then放入微任務(wù)
  • 下面同步代碼輸出main2
  • 接下來(lái)執(zhí)行微任務(wù)輸出nextTick1悔醋,promise then
  • 接下來(lái)執(zhí)行宏任務(wù)輸出setTimeout,將nexttick2放入微任務(wù)隊(duì)列
  • 接下來(lái)執(zhí)行微任務(wù)nexttick2
  • nextTick是由node自己定義并實(shí)現(xiàn)的概念兽叮,它的回調(diào)調(diào)用入口在event loop過(guò)程中MakeCallback函數(shù)的末尾芬骄,驅(qū)動(dòng)調(diào)用清空js層的queue,最后再執(zhí)行microtasks鹦聪,適當(dāng)處理下可能觸發(fā)的promise账阻,明顯 process.nextTick1> promise.then
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市椎麦,隨后出現(xiàn)的幾起案子宰僧,更是在濱河造成了極大的恐慌,老刑警劉巖观挎,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異段化,居然都是意外死亡嘁捷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)显熏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雄嚣,“玉大人,你說(shuō)我怎么就攤上這事』荷” “怎么了鼓鲁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)港谊。 經(jīng)常有香客問(wèn)我骇吭,道長(zhǎng),這世上最難降的妖魔是什么歧寺? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任燥狰,我火速辦了婚禮,結(jié)果婚禮上斜筐,老公的妹妹穿的比我還像新娘龙致。我一直安慰自己,他們只是感情好顷链,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布目代。 她就那樣靜靜地躺著,像睡著了一般嗤练。 火紅的嫁衣襯著肌膚如雪像啼。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天潭苞,我揣著相機(jī)與錄音忽冻,去河邊找鬼。 笑死此疹,一個(gè)胖子當(dāng)著我的面吹牛僧诚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蝗碎,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼湖笨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蹦骑?” 一聲冷哼從身側(cè)響起慈省,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎眠菇,沒(méi)想到半個(gè)月后边败,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捎废,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年笑窜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片登疗。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡排截,死狀恐怖嫌蚤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情断傲,我是刑警寧澤脱吱,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布村斟,位于F島的核電站蝙斜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蹂窖。R本人自食惡果不足惜猜年,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一抡锈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乔外,春花似錦床三、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至差购,卻和暖如春四瘫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欲逃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工找蜜, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稳析。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓洗做,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親彰居。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诚纸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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