javascript
JS专题之事件循环
準備知識
1. 進程(process)
進程是系統(tǒng)資源分配一個獨立單位,一個程序至少有一個進程。比方說:一個工廠代表一個 CPU, 一個車間就是一個進程,任一時刻,只能有一個進程在運行,其他進程處于非運行狀態(tài)。
2. 線程(Thread)
線程是CPU調(diào)度和分派的基本單位,一個線程只能屬于一個進程,一個進程可以有多個線程且至少有一個。比方說一個車間的工人,可以有多個工人一起工作。
生活中常常能看到,某某電腦 CPU 的 4 核 4 線程,其意思是指,這款 CPU 同一時間最多只能運行 4 個線程,所以有些線程會處于工作狀態(tài),有的線程會處于中斷,堵塞,睡眠狀態(tài)。
經(jīng)常看到有很多任務同時在進行,一邊工作,一邊聽歌,還一邊下載電影。那是因為這些線程在以閃電般的速度不斷的切換主要的幾個線程,所以,人的體驗上感覺是很多很多任務在同時進行。
3. 棧(stack)
棧是一種數(shù)據(jù)結構,具有后進先出的特點,最開始進入棧結構的數(shù)據(jù)反而最后才能出來。
4. 隊列(queue)
隊列也是一種數(shù)據(jù)結構,數(shù)據(jù)只能從一邊進,一邊出,先進去的自然就先出來。
5. 同步和異步(sync async)
同步和異步關注的消息通信機制,同步在函數(shù)調(diào)用時,如果調(diào)用者沒有拿到響應結果,程序會繼續(xù)等待,知道拿到結果為止。而異步會執(zhí)行其后的代碼,等到有響應結果后,才處理響應。
6. 阻塞和非阻塞(blocking & non-blocking)
阻塞和非阻塞關注的是程序等待調(diào)用結果時的狀態(tài),阻塞的意思是,在調(diào)用結果返回響應前,線程會被掛起占用,程序無法繼續(xù)往下走,而非阻塞的線程則不會掛起,后面的代碼能夠繼續(xù)往下執(zhí)行。
比方說:我去超市買包薯片,老板告訴我貨架上沒貨了,馬上去庫房拿,這過程中,老板要我站著等他,直到他拿到貨出來給我。這個過程就是阻塞。
如果老板告訴我,可以先回去,他一會去庫房拿,拿到了之后打電話給我。這個過程,就是非阻塞的,我不用等待,還可以干其他的事情。
7. 執(zhí)行棧(execution stack)
js 代碼在執(zhí)行代碼時,JS 會給調(diào)用代碼生成一個執(zhí)行上下文對象,并將其壓入執(zhí)行上下文棧,首先進入棧底的是全局上下文,然后是函數(shù)的執(zhí)行上下文(Execution Context),函數(shù)執(zhí)行完之后,函數(shù)上下文從棧中彈出,直到退出瀏覽器,全局上下文才從棧底彈出。
用代碼舉個例子:
var globalName = "window";var foo1 = function() {console.log("foo1"); }var foo2 = function() {console.log("foo2");foo1(); }foo2(); 復制代碼上面的圖片大致能夠描述執(zhí)行上下文棧的實現(xiàn)邏輯,有關執(zhí)行上下文的知識,大家可以翻看我之前的文章 - 《JavaScript 之執(zhí)行上下文》
二、為什么 JS 是單線程模型?
JavaScript 的一個非常有趣的特性是事件循環(huán)模型,與許多其他語言不同,它永不阻塞。 處理 I/O 通常通過事件和回調(diào)來執(zhí)行 -- MDN
瀏覽器主要任務是給用戶是視覺和交互上的體驗,如果頁面使用過程中,偶爾出現(xiàn)阻塞、掛起、無響應的體驗一定是非常糟糕的。同時,如果采用多線程同步的模型,那么如何保證同一時間修改了 DOM, 到底是哪個線程先生效呢。
瀏覽器執(zhí)行環(huán)境的核心思想在于任務調(diào)度方式的特別:
哪個任務的優(yōu)先級高,先來就先運行,直到執(zhí)行完了才執(zhí)行下一個,并且同一時刻只能執(zhí)行一個代碼片段,即所謂的單線程模型。
比方說,銀行的柜臺只開啟了一個柜臺,每個人想要辦理業(yè)務,就得先拿號排隊,叫到了你的號碼,你才能上去辦理業(yè)務。不能多個人同時在一個柜臺辦理業(yè)務,不然就很容易出差錯。
三、事件循環(huán)
事件循環(huán)是 JS 處理各種事件的核心,由于多個線程同時操作 DOM, 造成不可控的問題,所以 JS 采用了單線程模型。另外,由于所有的事件同步執(zhí)行,執(zhí)行完一個才能執(zhí)行下一個,會造成頁面渲染的堵塞。JS 中存在異步事件,用戶可以在點擊頁面的時候,請求網(wǎng)絡響應的同事,還可以進行其他的點擊操作,保證了頁面不會因為網(wǎng)絡請求,多種 IO 接口響應慢造成代碼執(zhí)行的堵塞和掛起。
事件循環(huán)的順序是:
接下來我們用代碼來解釋:
console.log("script start!");function foo1() {console.log("foo1"); }foo1();setTimeout(function () {console.log("setTimeout!"); }, 1000);function foo2() {console.log("foo2"); }foo2();console.log("script end!");打印: // script start! // foo1 // foo2 // script end!// setTimeout! 復制代碼那我們嘗試把 setTimeout 的延遲時間改為 0,想要立即執(zhí)行,看會不會立即執(zhí)行:
console.log("script start!");function foo1() {console.log("foo1"); }foo1();setTimeout(function () {console.log("setTimeout!"); }, 0);function foo2() {console.log("foo2"); }foo2();console.log("script end!");打印: // script start! // foo1 // foo2 // script end! // setTimeout! 復制代碼可以看出 setTimeout 屬于異步事件,總是會在主線程的任務執(zhí)行完后才開始執(zhí)行。
順便說一下事件循環(huán)幾個原則:
這兩個原則保證了瀏覽器任務單元的完整性,事件調(diào)用的有序性。
四、宏任務和微任務
事件循環(huán)的實現(xiàn)本來應該由一個用于宏任務的隊列和一個用于微任務的隊列進行完成,這使得事件循環(huán)要根據(jù)任務類型來進行優(yōu)先處理。
宏任務:
宏任務包括:
宏任務代表一個個離散、獨立的工作單元,運行完任務后,瀏覽器可以進行其他的任務調(diào)度,如更新渲染或執(zhí)行垃圾回收。宏任務需要多次事件循環(huán)才能執(zhí)行完。
微任務:
微任務包括:
微任務是更小的任務,微任務需要盡可能地、通過異步方式執(zhí)行,微任務更新瀏覽器的狀態(tài),但必須在瀏覽器執(zhí)行其他任務之前執(zhí)行。微任務使得我們避免不必要的 UI 重繪。微任務在一次事件循環(huán)中必須全部執(zhí)行完。
宏任務和微任務的執(zhí)行優(yōu)先級原則是:
完成一個宏任務后,執(zhí)行余下的微任務
同一次事件循環(huán)中,宏任務永遠在微任務之前執(zhí)行。
ok,知道了優(yōu)先級原則后,我們來看一段代碼:
console.log(1);setTimeout(function() {console.log(2);new Promise(resolve => {console.log(3);resolve(4);console.log(5);}).then(data => {console.log(data);}); }, 0);new Promise(resolve => {console.log(6);resolve(7);console.log(8); }).then(data => {console.log(data); });setTimeout(function() {console.log(9); }, 0);console.log(10);output: 第一次循環(huán): // 1 // 6 // 8 // 10 // 7第二次循環(huán): // 2 // 3 // 5 // 4第三次循環(huán) // 9 復制代碼我們一起來分析以上代碼:
關于事件循環(huán)宏任務和微任務的執(zhí)行過程:
五、web worker
盡管 HTML5 新標準加入了 web worker 的多線程技術,但是 web worker 只能用于計算,并且 JS 的多線程 worker 無法操作 DOM, 不然就無法控制頁面是在被誰操作的了。
主線程傳給子線程的數(shù)據(jù)是通過拷貝復制,同樣子線程傳給主線程的數(shù)據(jù)也是通過拷貝復制,而不是共享同一個內(nèi)存空間。
以上說明,JS 不存在線程同步,所以還是可以把 JS 看做單線程模型,把 web worker 當做 JS 的一種回調(diào)機制。
總結
事件循環(huán)是 JS 和 Nodejs 事件調(diào)用機制的核心,保證了頁面可以有序無阻塞的進行。
事件循環(huán)的主要邏輯是先執(zhí)行調(diào)用棧,直到清空調(diào)用棧只剩下全局上下文。
然后 JS 檢查宏任務隊列,如果有任務則取出一個進行調(diào)用,進行頁面渲染和垃圾回收。
同時將所有的微任務源派發(fā)的任務加入微任務事件隊列,最后執(zhí)行余下的所有微任務。微任務執(zhí)行后完,進行頁面渲染和垃圾回收后進行下一輪事件循環(huán)。
歡迎關注我的個人公眾號“謝南波”,專注分享原創(chuàng)文章。
掘金專欄 JavaScript 系列文章
總結
- 上一篇: JavaScript实现创建自定义对象的
- 下一篇: FlyWay简单使用