前言
相信大家都聽過一句話,js是單執行緒的語言,那麼這句話如何理解呢?今天就來討論一下js中的事件迴圈,理解完事件迴圈機制,你就會對這句話有更深刻的理解。
事件迴圈(event loop)
同步與非同步程式碼
let a = 1 console.log(a); setTimeout(function () { a++ }, 1000) console.log(a);
問:a列印多少? 答:1,1。因為定時器是一個耗時的程式碼,同步程式碼是不需要耗時執行的,非同步程式碼是需要耗時執行的,JSV8引擎會先執行同步程式碼再執行非同步程式碼
到這,你已經瞭解了什麼是同步程式碼什麼是非同步程式碼。
同步程式碼
:不需要耗時執行的程式碼。非同步程式碼
:需要耗時執行的程式碼。JS
V8引擎
會先執行同步程式碼再執行非同步程式碼。
程序與執行緒
大家大學階段應該都學習過一門課程叫做《作業系統》
,這門課程對程序與執行緒都有深刻的講解。
程序
:是作業系統中程式的一次執行過程,是資源分配的基本單位。它擁有獨立的地址空間、記憶體、檔案等資源。執行緒
:是程序中的一個執行單元,是作業系統能夠進行運算排程的最小單位。執行緒共享所屬程序的資源,但也有自己少量的私有資源,如程式計數器、棧等。多個執行緒可以在一個程序內併發執行,共同完成程序的任務,提高系統的併發性和效率。
例如:一個瀏覽器的tab頁面,就會有
渲染執行緒、js執行緒、http執行緒
,這些執行緒通力合作,共同完成程序任務。做一個假設,我們有一份html程式碼,有一個p標籤hello,但是在p標籤之前引入了一份js程式碼。這個hello想要展示出來一定是會等到js執行完了才輪到它執行,例如我們自己寫了一份js程式碼,這份js程式碼中用到了引用的js程式碼,如果不是前面的js程式碼先載入完的話,那我們這一份js程式碼就報錯了。
因此:js的載入會阻塞頁面的渲染,即:在我們的例子中,渲染執行緒與js執行緒是互斥的(不能同時工作),這就從側面說明了一個問題:js是單執行緒的
js的單執行緒
jd的v8引擎在執行js的過程中,只有一個執行緒會工作。
let a = 1 console.log(a); setTimeout(function () { a++ }, 1000) console.log(a);
還是剛纔的例子,為什麼js中會有同步程式碼和非同步程式碼
,如果第二行程式碼列印完了之後,到了第四行,第四行如果要你等一天時間,才能執行後面的程式碼,就不完蛋了嗎,因為js騰不出手,所以纔有了同步與非同步這個機制,先把非同步的比如這個定時器給他掛起來,騰出手來先去執行下面的同步程式碼列印a,然後全部執行完了,再執行非同步,這樣效率就提高了許多。
不論一個語言是單執行緒的還是多執行緒的,都各有各自的優勢,例如多執行緒的語言,毋庸置疑,執行效率高。 那麼單執行緒語言有什麼優勢呢?
js單執行緒的優勢
js打造之初就是想作為一個瀏覽器的指令碼語言,因此如果它佔用使用者過多的效能,這門語言會被嫌棄。
節約效能:不需要過多考慮多執行緒環境下資源競爭、死鎖等複雜情況,程式碼邏輯相對簡單清晰,降低了開發難度和出錯機率。
節約上下文切換的時間:例如有一個迴圈語句要執行1s,然後對a變數做一個操作,一個定時器也要執行1s,也對a變數做一個操作,多執行緒語言就會對其中一個上鎖,先執行完另一個然後切換回這個解鎖。這就存在上下文切換的耗時。
便於與瀏覽器互動:能更好地與瀏覽器的單執行緒模型相契合,確保頁面渲染和使用者互動的穩定性,不會因為多執行緒競爭而導致頁面顯示異常等問題。
微任務與宏任務
let count = 0; function a(){ setTimeout(()=>{ count++; },1000) } function b(){ console.log(count); } a(); b();
以上面程式碼為例子,還是一樣,執行到13行,呼叫a,發現a是一個非同步程式碼,因此先掛起了,執行b的呼叫,然後發現count還是0,因此列印0,然後過1s執行count++。倘若我們這個地方寫的不是一個定時器隔1s執行,而是一個http請求,但是這個請求耗時是說不準的,機器效能好,速度就快,效能差就慢。因此就會存在一些場景,你認為這個程式碼是耗時的但是它又不耗時,你說它耗時,但是它又幾乎不耗時。因此僅僅有非同步程式碼這個概念就沒辦法解決所有的應用場景了,那麼官方就在非同步下又區分了一個微任務與宏任務。
微任務與宏任務,都是非同步程式碼
微任務:promise.then(),proces.nextTick(),mutationObserver()
宏任務:script,setTimeout,setInterval,setImmediate,I/O,UI rendering
事件迴圈機制
執行同步程式碼(這屬於是宏任務)
同步執行完畢後,檢查是否有非同步需要執行
如果有,則執行微任務
微任務執行完畢後,如果有需要就會渲染頁面
執行非同步宏任務,也是開啟下一次事件迴圈(因為宏任務中,也一樣會有同步程式碼、非同步程式碼...)
面試題實戰1
console.log(1); new Promise((resolve, reject) => { console.log(2); resolve() }) .then(() => { console.log(3); }) .then(() => { console.log(4); }) setTimeout(function () { console.log(5); }) console.log(6); // 1 2 6 3 4 5(宏裡面也有周期,同步->微任務->渲染->宏)
分析過程:
第一行程式碼,執行列印1(同步程式碼)
第二行程式碼promise的呼叫,同步程式碼,列印2
第六行程式碼是非同步程式碼裡面的微任務,因此,加入微任務佇列掛起(then1)。
第九行程式碼是非同步程式碼裡面的微任務,因此,加入微任務佇列掛起(then2)。
第十二行程式碼是非同步程式碼裡的宏任務,因此,加入宏任務佇列掛起(set1)。
第十五行程式碼是同步任務,列印6。
至此,第一次事件迴圈機制裡的第一步,同步任務全部執行完畢,開始尋找是否有非同步程式碼需要執行
如果有,執行微任務,因此then1出佇列,也就是列印3,然後then2出佇列,列印4。
微任務執行完畢後,沒有發現渲染操作,因此接下來執行非同步中的宏任務,也是開啟了下一次事件迴圈,set1出佇列,執行列印5.
最終結果:1,2,6,3,4,5。
面試題實戰2
console.log(1); new Promise((resolve, reject) => { console.log(2); resolve() }) .then(() => { console.log(3); setTimeout(() => { console.log(4); }, 0) }) setTimeout(() => { console.log(5); setTimeout(() => { console.log(6); }, 0) }, 0) console.log(7);
分析過程:
第一行執行,同步程式碼,列印1
new了一個Promise,建構函式本身也不是非同步,同步程式碼,執行列印2
呼叫resolve,.then就能執行了,發現是一個非同步的微任務,因此入微任務佇列掛起then1(列印3掛起)
到達十二行,發現是一個定時器,非同步裡面的宏任務,入宏任務佇列set1
18行同步程式碼,列印7
至此,第一次事件迴圈的第一步同步程式碼執行結束
執行微任務,then1出佇列,then1中有同步和非同步,同步先執行,因此列印3,然後發現有個定時器,因此set2入宏任務佇列。微任務執行結束
開始執行宏任務,set1出佇列,宏任務開啟一次新的事件迴圈,同步任務先執行,列印5,然後第二次事件迴圈發現了一個定時器宏任務,set3入宏任務佇列,第二次事件迴圈的同步結束,然後去找微任務佇列,發現微任務佇列是空的,緊接著去宏任務佇列找宏任務,開啟第三次事件迴圈,set2出佇列,因此列印4,這也意味著第二次事件迴圈宏任務結束,第二次事件迴圈結束,列印4即是第二次時間迴圈的結束也是第三次事件迴圈的開始,緊接著去微任務佇列找,發現沒有,然後去宏任務佇列找,set3出佇列,列印6。
因此結果:1273546
面試題3實戰
console.log('script start'); async function async1() { await async2() console.log('async1 end'); } async function async2() { console.log('async2 end'); } async1() setTimeout(function () { console.log('setTimeout'); }, 0) new Promise(function (resolve, reject) { console.log('promise'); resolve() }) .then(() => { console.log('then1'); }) .then(() => { console.log('then2'); }) console.log('script end');
分析過程:注意:await會將後續程式碼阻塞進微任務佇列
第一行同步程式碼執行 列印script start
第10行帶了了async1的呼叫,async2前面有await,因此async2呼叫,async2裡面是同步,列印async2 end,然後輪到第五行程式碼執行,但是由於await將這行列印放到了微任務佇列,因此async1 end入微任務佇列
到達第十一行,是一個定時器,宏任務進宏任務佇列set
達到第十四行,同步任務列印promise
到達第十八行,進微任務佇列then1
達到21行,進微任務佇列then2(目前微任務佇列有3個,一個async1 end,then1,then2)
24行同步任務,列印script end
至此同步程式碼全部執行完畢,開始執行微任務,async1 end,then1,then2出佇列,列印async1 end,then1,then2
微任務執行結束,執行宏任務,set出佇列,列印setTimeout
小結
如果這三份面試實戰,你都能清楚明白什麼時候同步程式碼執行,什麼時候入微任務佇列什麼時候入宏任務佇列,什麼時候出微任務佇列,什麼時候出紅任務佇列,那麼事件迴圈機制就徹底搞明白了,明白了事件迴圈機制,你也就能夠更加理解js底層V8是如何執行的。