深入理解JavaScript的事件執(zhí)行機(jī)制
前言
熟悉事件循環(huán),了解瀏覽器運(yùn)行機(jī)制將對我們理解 JavaScript 的執(zhí)行過程和排查運(yùn)行問題有很大幫助。以下是總結(jié)的一些瀏覽器事件循環(huán)的一些原理和示例。
瀏覽器 JS 異步執(zhí)行的原理
- JS 是單線程的,也就是同一個時刻只能做一件事情,那么,為什么瀏覽器可以同時執(zhí)行異步任務(wù)呢?
- 因為瀏覽器是多線程的,當(dāng) JS 需要執(zhí)行異步任務(wù)時,瀏覽器會另外啟動一個線程去執(zhí)行該任務(wù)。
- 也就是說,JS 是單線程的指的是 執(zhí)行JS 代碼的線程只有一個,是瀏覽器提供的 JS 引擎線程(主線程)。瀏覽器中還有定時器線程和 HTTP 請求線程等,這些線程主要不是來跑 JS 代碼的。
- 比如主線程中需要發(fā)一個 AJAX 請求,就把這個任務(wù)交給另一個瀏覽器線程(HTTP 請求線程)去真正發(fā)送請求,待請求回來了,再將 callback 里需要執(zhí)行的 JS 回調(diào)交給 JS 引擎線程去執(zhí)行。
- 即瀏覽器才是真正執(zhí)行發(fā)送請求這個任務(wù)的角色,而 JS 只是負(fù)責(zé)執(zhí)行最后的回調(diào)處理。所以這里的異步不是 JS 自身實現(xiàn)的,其實是瀏覽器為其提供的能力。
瀏覽器中的事件循環(huán)
執(zhí)行棧與任務(wù)隊列
JS 在解析一段代碼時,會將同步代碼按順序排在某個地方,即執(zhí)行棧,然后依次執(zhí)行里面的函數(shù)。
當(dāng)遇到異步任務(wù)時就交給其他線程處理,待當(dāng)前執(zhí)行棧所有同步代碼執(zhí)行完成后,會從一個隊列中去取出已完成的異步任務(wù)的回調(diào)加入執(zhí)行棧繼續(xù)執(zhí)行。
遇到異步任務(wù)時又交給其他線程,.....,如此循環(huán)往復(fù)。
而其他異步任務(wù)完成后,將回調(diào)放入任務(wù)隊列中待執(zhí)行棧來取出執(zhí)行。
宏任務(wù)和微任務(wù)
根據(jù)任務(wù)的種類不同,可以分為微任務(wù)(micro task)隊列和宏任務(wù)(macro task)隊列。
事件循環(huán)的過程中,執(zhí)行棧在同步代碼執(zhí)行完成后,優(yōu)先檢查微任務(wù)隊列是否有任務(wù)需要執(zhí)行,如果沒有,再去宏任務(wù)隊列檢查是否有任務(wù)執(zhí)行,如此往復(fù)。
微任務(wù)一般在當(dāng)前循環(huán)就會優(yōu)先執(zhí)行,而宏任務(wù)會等到下一次循環(huán),因此,微任務(wù)一般比宏任務(wù)先執(zhí)行,并且微任務(wù)隊列只有一個,宏任務(wù)隊列可能有多個。另外我們常見的點(diǎn)擊和鍵盤等事件也屬于宏任務(wù)。
常見宏任務(wù):
- setTimeout()
- setInterval()
- UI交互事件
- postMessage
- setImmediate() -- nodeJs
常見微任務(wù):
- promise.then()、promise.catch()
- new MutaionObserver()
- process.nextTick() -- nodeJs
如下示例:
console.log('同步代碼1'); setTimeout(() => { console.log('setTimeout') }, 0) new Promise((resolve) => { console.log('同步代碼2') resolve() }).then(() => { console.log('promise.then') }) console.log('同步代碼3'); // 最終輸出"同步代碼1"、"同步代碼2"、"同步代碼3"、"promise.then"、"setTimeout"
具體分析如下:
- setTimeout 回調(diào)和 promise.then 都是異步執(zhí)行的,將在所有同步代碼之后執(zhí)行;
- 雖然 promise.then 寫在后面,但是執(zhí)行順序卻比 setTimeout 優(yōu)先,因為它是微任務(wù);
- new Promise 是同步執(zhí)行的,promise.then 里面的回調(diào)才是異步的。
注意: 在瀏覽器中 setTimeout 的延時設(shè)置為 0 的話,會默認(rèn)為 4ms,NodeJS 為 1ms。
微任務(wù)和宏任務(wù)的本質(zhì)區(qū)別:
- 宏任務(wù)特征:有明確的異步任務(wù)需要執(zhí)行和回調(diào);需要其他異步線程支持。
- 微任務(wù)特征:沒有明確的異步任務(wù)需要執(zhí)行,只有回調(diào);不需要其他異步線程支持。
Async/await的運(yùn)行順序
特點(diǎn)
- async聲明的函數(shù)只是把該函數(shù)的return包裝了,使得無論如何都會返回promise對象(非promise會轉(zhuǎn)化為promise{resolve})。
- await聲明只能用在async函數(shù)中。
- 當(dāng)執(zhí)行async函數(shù)時,遇到await聲明,會先將await后面的內(nèi)容按照'平常的執(zhí)行規(guī)則'執(zhí)行完(此處注意,當(dāng)執(zhí)行函數(shù)內(nèi)容中又遇到await聲明,此時接著執(zhí)行該await聲明內(nèi)容)。
- 執(zhí)行完后立馬跳出async函數(shù),去執(zhí)行主線程其他內(nèi)容,等到主線程執(zhí)行完再回到await處繼續(xù)執(zhí)行后面的內(nèi)容。
示例
const a = async () => { console.log("a"); await b(); await e(); }; const b = async () => { console.log("b start"); await c(); console.log("b end"); }; const c = async () => { console.log("c start"); await d(); console.log("c end"); }; const d = async () => { console.log("d"); }; const e = async () => { console.log("e"); }; console.log('start'); a(); console.log('end');
運(yùn)行結(jié)果
個人分析
- 在當(dāng)前同步環(huán)境中,先執(zhí)行console.log('start');輸出'start'。
- 遇到同步函數(shù)a(),執(zhí)行a()。
- a()是sync/await構(gòu)造,函數(shù)內(nèi)遇到console.log("a") 輸出 'a',遇到await聲明 await b(),為異步函數(shù),進(jìn)入函數(shù)b()內(nèi)執(zhí)行。(類似的操作,我自己想的)并將 await b()后的內(nèi)容推入微任務(wù)隊列中。我們可以記做[await b()后]。
- b()是sync/await構(gòu)造,順序執(zhí)行遇到console.log("c start") 輸出 'c start',遇到await聲明 await c(),為異步函數(shù),進(jìn)入函數(shù)c()內(nèi)執(zhí)行。并將 await c()后的內(nèi)容推入微任務(wù)隊列中。我們可以記做[await c()后, await b()后]。
- c()是sync/await構(gòu)造,順序執(zhí)行遇到console.log("b start") 輸出 'b start',遇到await聲明 await d(),為異步函數(shù),進(jìn)入函數(shù)d()內(nèi)執(zhí)行。并將 await d()后的內(nèi)容推入微任務(wù)隊列中。我們可以記做[await d()后, await c()后, await b()后]。
- d()中,順序執(zhí)行遇到console.log("") 輸出 'd',d()函數(shù)運(yùn)行結(jié)束。
- 這是執(zhí)行完 d()后,無可執(zhí)行異步函數(shù),此時進(jìn)入同步環(huán)境,執(zhí)行 a()后的內(nèi)容。
- 遇到console.log("end") 輸出 'end'。此時同步環(huán)境中主線程執(zhí)行完,檢查微任務(wù)隊列是否有微任務(wù)。
- 微任務(wù)隊列中[await d()后, await c()后, await b()后]有微任務(wù),執(zhí)行await d()后的內(nèi)容。
- wait d()后的內(nèi)容是,console.log("c end"), 輸出 'c end'。此時內(nèi)容執(zhí)行完畢,再從微任務(wù)隊列中[await c()后, await b()后]檢查,執(zhí)行await c()后的內(nèi)容。
- 執(zhí)行await c()后的內(nèi)容,console.log("b end");, 輸出 'b end'。此時內(nèi)容執(zhí)行完畢,再從微任務(wù)隊列中[await b()后]檢查,執(zhí)行await b()后的內(nèi)容。
- await d()后的內(nèi)容是,await e(),遇到await聲明,執(zhí)行e()。并判斷并將 await e()后無運(yùn)行代碼,不用入微任務(wù)隊列。
- 執(zhí)行e(),順序執(zhí)行console.log("e");,輸出 'e'。此時函數(shù)結(jié)束。
- 微任務(wù)隊列中[]無微任務(wù),執(zhí)行結(jié)束。進(jìn)入同步環(huán)境。
到此這篇關(guān)于深入理解JavaScript的事件執(zhí)行機(jī)制的文章就介紹到這了,更多相關(guān)JavaScript 事件執(zhí)行機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在小程序中集成redux/immutable/thunk第三方庫的方法
這篇文章主要介紹了在小程序中集成redux/immutable/thunk第三方庫的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08js實現(xiàn)的類marquee水平循環(huán)滾動
marquee (水平)循環(huán)滾動的js實現(xiàn) ,需要的朋友可以參考下。2010-03-03boostrap模態(tài)框二次彈出清空原有內(nèi)容的方法
今天小編就為大家分享一篇boostrap模態(tài)框二次彈出清空原有內(nèi)容的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08Javascript 使用function定義構(gòu)造函數(shù)
Javascript并不像Java、C#等語言那樣支持真正的類。但是在js中可以定義偽類。做到這一點(diǎn)的工具就是構(gòu)造函數(shù)和原型對象。首先介紹js中的構(gòu)造函數(shù)。2010-02-02