前言
Event Loop
是計(jì)算機(jī)系統(tǒng)的一種運(yùn)行機(jī)制遭赂,是個(gè)很重要的概念淹冰。而Javascript
用這種機(jī)制來(lái)解決單線程運(yùn)行帶來(lái)的問(wèn)題逢净。理解很熟悉將會(huì)有利于我們更容易理解Vue
的異步事件毁习。
JavaScript是單線程的
1烤送、什么是單線程寒随?
單線程在程序執(zhí)行時(shí),所走的程序路徑按照連續(xù)順序排下來(lái)帮坚,前面的必須處理好妻往,后面的才會(huì)執(zhí)行。簡(jiǎn)單來(lái)說(shuō)试和,即同一時(shí)間只能做一件事件讯泣。
2、Js為什么是單線程阅悍?
Js
是一種運(yùn)行在網(wǎng)頁(yè)的簡(jiǎn)單的腳本語(yǔ)言好渠,由于設(shè)計(jì)的初衷是作為瀏覽器腳本語(yǔ)言,用于與用戶互動(dòng)溉箕,以及操作DOM
晦墙。這決定它是單線程的。
3肴茄、單線程帶來(lái)的問(wèn)題晌畅?
單線程就意味著,所有任務(wù)都需要排隊(duì)寡痰,前一個(gè)任務(wù)結(jié)束抗楔,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng)拦坠,后一個(gè)任務(wù)就需要一直等著连躏。這就會(huì)導(dǎo)致IO
操作(耗時(shí)但cpu閑置)時(shí)造成性能浪費(fèi)的問(wèn)題。
4贞滨、如何解決單線程的性能問(wèn)題入热?
采用異步可以解決拍棕。主線程完全可以不管IO
操作,暫時(shí)掛起處于等待中的任務(wù)勺良,先運(yùn)行排在后面的任務(wù)绰播。等到IO
操作返回了結(jié)果,再回過(guò)頭尚困,把掛起的任務(wù)繼續(xù)執(zhí)行下去蠢箩。于是,所有任務(wù)可以分成兩種事甜,一種是同步任務(wù)谬泌,另一種是異步任務(wù)。
執(zhí)行棧
當(dāng)Javascript
代碼執(zhí)行的時(shí)候會(huì)將不同的變量存于內(nèi)存中的不同位置:堆(heap)和棧(stack)中來(lái)加以區(qū)分逻谦。其中掌实,堆里存放著一些對(duì)象。而棧中則存放著一些基礎(chǔ)類型變量以及對(duì)象的指針邦马。但是我們這里說(shuō)的執(zhí)行棧和上面這個(gè)棧的意義卻有些不同潮峦。
js 在執(zhí)行可執(zhí)行的腳本時(shí),會(huì)經(jīng)過(guò)以下步驟:
- 首先會(huì)創(chuàng)建一個(gè)全局可執(zhí)行上下文
globalContext
勇婴,每當(dāng)執(zhí)行到一個(gè)函數(shù)調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)可執(zhí)行上下文(execution context)EC
忱嘹。 - 可執(zhí)行程序可能會(huì)存在很多函數(shù)調(diào)用,那么就會(huì)創(chuàng)建很多
EC
耕渴,所以JavaScript
引擎創(chuàng)建了執(zhí)行上下文棧(Execution context stack拘悦,ECS)來(lái)管理執(zhí)行上下文。 - 當(dāng)函數(shù)調(diào)用完成橱脸,
Js
會(huì)退出這個(gè)執(zhí)行環(huán)境并把這個(gè)執(zhí)行環(huán)境銷毀础米,回到上一個(gè)方法的執(zhí)行環(huán)境。 這個(gè)過(guò)程反復(fù)進(jìn)行添诉,直到執(zhí)行棧中的代碼全部執(zhí)行完畢屁桑。
實(shí)例
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
當(dāng)執(zhí)行一個(gè)函數(shù)的時(shí)候,就會(huì)創(chuàng)建一個(gè)執(zhí)行上下文栏赴,并且壓入執(zhí)行上下文棧蘑斧,當(dāng)函數(shù)執(zhí)行完畢的時(shí)候,就會(huì)將函數(shù)的執(zhí)行上下文從棧中彈出须眷。知道了這樣的工作原理竖瘾,讓我們來(lái)看看如何處理上面這段代碼:
1.執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文花颗,全局上下文被壓入執(zhí)行上下文棧
ECStack = [
globalContext
];
- 全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
- 初始化的同時(shí)捕传,
fun1
函數(shù)被創(chuàng)建,保存作用域鏈到函數(shù)的內(nèi)部屬性[[scope]]
fun1.[[scope]] = [
globalContext.VO
];
- 執(zhí)行
fun1
函數(shù)扩劝,創(chuàng)建fun1
函數(shù)執(zhí)行上下文庸论,fun1
函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
ECStack = [
fun1,
globalContext
];
-
fun1
函數(shù)執(zhí)行上下文初始化:1.復(fù)制函數(shù)
[[scope]]
屬性創(chuàng)建作用域鏈职辅。2.用
arguments
創(chuàng)建活動(dòng)對(duì)象。3.初始化活動(dòng)對(duì)象聂示,即加入形參罐农、函數(shù)聲明、變量聲明催什。
4.將活動(dòng)對(duì)象壓入
fun1
作用域鏈頂端。
同時(shí)f
函數(shù)被創(chuàng)建宰睡,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性[[scope]]
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
- 執(zhí)行
fun2()
函數(shù)蒲凶,重復(fù)步驟2。 - 最終形成這樣的執(zhí)行棧:
ECStack = [
fun3
fun2,
fun1,
globalContext
];
-
fun3
執(zhí)行完畢拆内,從執(zhí)行棧中彈出...一直到fun1
事件循環(huán)(Event Loop)
JavaScript內(nèi)存模型
在了解事件循環(huán)之前旋圆,先要弄明白Js
的內(nèi)存模型,這有助于更好的理解事件循環(huán)麸恍。
- 調(diào)用棧(Call Stack):用于主線程任務(wù)的執(zhí)行灵巧。
- 堆(Heap):用于存放非結(jié)構(gòu)數(shù)據(jù),如程序分配的變量和對(duì)象抹沪。
- 任務(wù)隊(duì)列(Queue): 用于存放異步任務(wù)刻肄。
Js異步執(zhí)行的運(yùn)行機(jī)制
- 所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧融欧。
- 主線程之外敏弃,還存在一個(gè)任務(wù)隊(duì)列。只要異步任務(wù)有了運(yùn)行結(jié)果噪馏,就在任務(wù)隊(duì)列之中放置一個(gè)事件麦到。
- 一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列欠肾,看看里面有哪些事件瓶颠。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài)刺桃,進(jìn)入執(zhí)行棧粹淋,開始執(zhí)行。
- 主線程不斷重復(fù)上面的第三步瑟慈。
任務(wù)
異步任務(wù)存放在任務(wù)隊(duì)列里廓啊,異步任務(wù)分為 宏任務(wù)(macrotask)與微任務(wù)(microtask),不同的API注冊(cè)的任務(wù)會(huì)依次進(jìn)入自身對(duì)應(yīng)的隊(duì)列中封豪,然后等待Event Loop將它們依次壓入執(zhí)行棧中執(zhí)行谴轮。
宏任務(wù)主要包含:
-
script
(整體代碼) setTimeout
setInterval
-
I/O
、UI
交互事件 -
setImmediate
(Node.js 環(huán)境)
微任務(wù)主要包含:
Promise
MutaionObserver
-
process.nextTick
(Node.js 環(huán)境)
我們的JavaScript
的執(zhí)行過(guò)程是單線程的吹埠,所有的任務(wù)可以看做存放在兩個(gè)隊(duì)列中——執(zhí)行隊(duì)列和事件隊(duì)列第步。
執(zhí)行隊(duì)列里面是所有同步代碼的任務(wù)谴忧,事件隊(duì)列里面是所有異步代碼的宏任務(wù)聚谁,而我們的微任務(wù),是處在兩個(gè)隊(duì)列之間。
當(dāng)JavaScript
執(zhí)行時(shí)迄委,優(yōu)先執(zhí)行完所有同步代碼,遇到對(duì)應(yīng)的異步代碼窝革,就會(huì)根據(jù)其任務(wù)類型存到對(duì)應(yīng)隊(duì)列(宏任務(wù)放入事件隊(duì)列姻蚓,微任務(wù)放入執(zhí)行隊(duì)列之后,事件隊(duì)列之前)堆生;當(dāng)執(zhí)行完同步代碼之后专缠,就會(huì)執(zhí)行位于執(zhí)行隊(duì)列和事件隊(duì)列之間的微任務(wù),然后再執(zhí)行事件隊(duì)列中的宏任務(wù)淑仆。
實(shí)例
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => {
// t2
console.log(2)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3);
這段代碼的流程大致如下:
-
script
任務(wù)先運(yùn)行涝婉。首先遇到Promise
實(shí)例,構(gòu)造函數(shù)首先執(zhí)行蔗怠,所以首先輸出了 4墩弯。此時(shí)microtask
的任務(wù)有t2
和t1
-
script
任務(wù)繼續(xù)運(yùn)行,輸出3
寞射。至此渔工,第一個(gè)宏任務(wù)執(zhí)行完成。 - 執(zhí)行所有的微任務(wù)桥温,先后取出
t2
和t1
涨缚,分別輸出2
和1
- 代碼執(zhí)行完畢
綜上,上述代碼的輸出是:4321
事件循環(huán)
主線程從任務(wù)隊(duì)列中讀取事件策治,這個(gè)過(guò)程是循環(huán)不斷的脓魏,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop
(事件循環(huán))。
從上圖我們可以看出:
- 主線程運(yùn)行的時(shí)候通惫,產(chǎn)生堆(heap)和棧(stack)茂翔。
- 棧中的代碼調(diào)用各種外部API,它們?cè)?任務(wù)隊(duì)列"中加入各種事件(click履腋,load珊燎,done)。
- 棧中的代碼執(zhí)行完畢遵湖,主線程就會(huì)去讀取任務(wù)隊(duì)列悔政,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
小結(jié)
事件循環(huán)其實(shí)并不難延旧,多查閱資料谋国,多看看相關(guān)例子就ok。希望一知半解的童鞋抓緊學(xué)習(xí)迁沫。