摘要:如果對語法分析和預編譯,還有疑問引擎執行的過程的理解語法分析和預編譯階段。參與執行過程的線程分別是引擎線程也稱為內核,負責解析執行腳本程序的主線程例如引擎。以上便是引擎執行宏任務的整個過程。
一、概述
js引擎執行過程主要分為三個階段,分別是語法分析,預編譯和執行階段,上篇文章我們介紹了語法分析和預編譯階段,那么我們先做個簡單概括,如下:
1、語法分析: 分別對加載完成的代碼塊進行語法檢驗,語法正確則進入預編譯階段;不正確則停止該代碼塊的執行,查找下一個代碼塊并進行加載,加載完成再次進入該代碼塊的語法分析階段。
2、預編譯:通過語法分析階段后,進入預編譯階段,則創建變量對象(創建arguments對象(函數運行環境下),函數聲明提前解析,變量聲明提升),確定作用域鏈以及this指向。
如果對語法分析和預編譯,還有疑問:javascript引擎執行的過程的理解--語法分析和預編譯階段。
同步更新sau交流學習社區(nodeJSBlog):javascript引擎執行的過程的理解--執行階段(https://www.mwcxs.top/page/56...)
本文主要分析js引擎執行的第三個階段–執行階段,在分析之前我們先思考以下兩個問題:
1、js是單線程的,為了避免代碼解析阻塞使用了異步執行,那么它的異步執行機制是怎么樣的?
答:通過事件循環(Event Loop),理解了事件循環的原理就理解了js的異步執行機制,本文主要介紹。
2、js是單線程的,那么是否代表參與js執行過程的線程就只有一個?
答:不是的,會有四個線程參與該過程,但是永遠只有JS引擎線程在執行JS腳本程序,其他的三個線程只協助,不參與代碼解析與執行。參與js執行過程的線程分別是:
(1)JS引擎線程: 也稱為JS內核,負責解析執行Javascript腳本程序的主線程(例如V8引擎)。
(2)事件觸發線程: 歸屬于瀏覽器內核進程,不受JS引擎線程控制。主要用于控制事件(例如鼠標,鍵盤等事件),當該事件被觸發時候,事件觸發線程就會把該事件的處理函數推進事件隊列,等待JS引擎線程執行。
(3)定時器觸發線程:主要控制計時器setInterval和延時器setTimeout,用于定時器的計時,計時完畢,滿足定時器的觸發條件,則將定時器的處理函數推進事件隊列中,等待JS引擎線程執行。
注:W3C在HTML標準中規定setTimeout低于4ms的時間間隔算為4ms。
(4)HTTP異步請求線程:通過XMLHttpRequest連接后,通過瀏覽器新開的一個線程,監控readyState狀態變更時,如果設置了該狀態的回調函數,則將該狀態的處理函數推進事件隊列中,等待JS引擎線程執行。
注:瀏覽器對同一域名請求的并發連接數是有限制的,Chrome和Firefox限制數為6個,ie8則為10個。
總結:永遠只有JS引擎線程在執行JS腳本程序,其他三個線程只負責將滿足觸發條件的處理函數推進事件隊列,等待JS引擎線程執行。
二、執行階段先分析一個典型的例子(來自Tasks, microtasks, queues and schedules,建議英文基礎好的閱讀,非常不錯的文章):
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
console.log("script end");
直接劃分例子的代碼結構,簡單描述分析執行過程;
暫不解釋該過程中的概念和原理,概念和原理將會在下面具體講解如下:
1、宏任務(macro-task)宏任務(macro-task),宏任務又按執行順序分為同步任務和異步任務
(1)同步任務
console.log("script start");
console.log("script end");
(2)異步任務
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
在JS引擎執行過程中,進入執行階段后,代碼的執行順序如下:
宏任務(同步任務) --> 微任務 --> 宏任務(異步任務)
輸出結果:
script start
script end
promise1
promise2
setTimeout
進入ES6或Node環境中,JS的任務分為兩種,分別是宏任務(macro-task)和微任務(micro-task),在最新的ECMAScript中,微任務稱為jobs,宏任務稱為task,他們的執行順序如上。可能很多人對上面的分析并不理解,那么我們接下來繼續對上面例子進行詳細分析。
2.1宏任務宏任務(macro-task)可分為同步任務和異步任務:
1、同步任務指的是在JS引擎主線程上按順序執行的任務,只有前一個任務執行完畢后,才能執行后一個任務,形成一個執行棧(函數調用棧)。
2、異步任務指的是不直接進入JS引擎主線程,而是滿足觸發條件時,相關的線程將該異步任務推進任務隊列(task queue),等待JS引擎主線程上的任務執行完畢,空閑時讀取執行的任務,例如異步Ajax,DOM事件,setTimeout等。
理解宏任務中同步任務和異步任務的執行順序,那么就相當于理解了JS異步執行機制–事件循環(Event Loop)。
2.1.1事件循環
事件循環可以理解成由三部分組成,分別是:
1、主線程執行棧
2、異步任務等待觸發
3、任務隊列
任務隊列(task queue)就是以隊列的數據結構對事件任務進行管理,特點是先進先出,后進后出。
這里直接引用一張著名的圖片(參考自Philip Roberts的演講《Help, I’m stuck in an event-loop》),幫助我們理解,如下:
在JS引擎主線程執行過程中:
1、首先執行宏任務的同步任務,在主線程上形成一個執行棧,可理解為函數調用棧。
2、當執行棧中的函數調用到一些異步執行的API(例如異步Ajax,DOM事件,setTimeout等API),則會開啟對應的線程(Http異步請求線程,事件觸發線程和定時器觸發線程)進行監控和控制。
3、當異步任務的事件滿足觸發條件時,對應的線程則會把該事件的處理函數推進任務隊列(task queue)中,等待主線程讀取執行。
4、當JS引擎主線程上的任務執行完畢,則會讀取任務隊列中的事件,將任務隊列中的事件任務推進主線程中,按任務隊列順序執行
5、當JS引擎主線程上的任務執行完畢后,則會再次讀取任務隊列中的事件任務,如此循環,這就是事件循環(Event Loop)的過程。
如果還是不能理解,那么我們再次拿上面的例子進行詳細分析,該例子中宏任務的代碼部分是:
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
console.log("script end");
代碼執行過程如下:
1、JS引擎主線程按代碼順序執行,當執行到console.log("script start");,JS引擎主線程認為該任務是同步任務,所以立刻執行輸出script start,然后繼續向下執行。
2、JS引擎主線程執行到setTimeout(function() { console.log("setTimeout"); }, 0);,JS引擎主線程認為setTimeout是異步任務API,則向瀏覽器內核進程申請開啟定時器線程進行計時和控制該setTimeout任務。由于W3C在HTML標準中規定setTimeout低于4ms的時間間隔算為4ms,那么當計時到4ms時,定時器線程就把該回調處理函數推進任務隊列中等待主線程執行,然后JS引擎主線程繼續向下執行。
3、JS引擎主線程執行到console.log("script end");,JS引擎主線程認為該任務是同步任務,所以立刻執行輸出script end。
4、JS引擎主線程上的任務執行完畢(輸出script start和script end)后,主線程空閑,則開始讀取任務隊列中的事件任務,將該任務隊里的事件任務推進主線程中,按任務隊列順序執行,最終輸出setTimeout,所以輸出的結果順序為script start script end setTimeout。
以上便是JS引擎執行宏任務的整個過程。
理解該過程后,我們做一些拓展性的思考:
我們都知道setTimeout和setInterval是異步任務的定時器,需要添加到任務隊列等待主線程執行,那么使用setTimeout模擬實現setInterval,會有區別嗎?
答案是有區別的,我們不妨思考一下:
1、setTimeout實現setInterval只能通過遞歸調用。
2、setTimeout是在到了指定時間的時候就把事件推到任務隊列中,只有當在任務隊列中的setTimeout事件被主線程執行后,才會繼續再次在到了指定時間的時候把事件推到任務隊列,那么setTimeout的事件執行肯定比指定的時間要久,具體相差多少跟代碼執行時間有關。
3、setInterval則是每次都精確的隔一段時間就向任務隊列推入一個事件,無論上一個setInterval事件是否已經執行,所以有可能存在setInterval的事件任務累積,導致setInterval的代碼重復連續執行多次,影響頁面性能。
綜合以上的分析,使用setTimeout實現計時功能是比setInterval性能更好的。當然如果不需要兼容低版本的IE瀏覽器,使用requestAnimationFrame是更好的選擇。
我們繼續再做進一步的思考,如下:
高頻率觸發的事件(例如滾動事件)觸發頻率過高會影響頁面性能,甚至造成頁面卡頓,我們是否可以利用計時器的原理進行優化呢?
是可以的,我們可以利用setTimeout實現計時器的原理,對高頻觸發的事件進行優化,實現點在于將多個觸發事件合并成一個,這就是防抖和節流,本文先不做具體講解,大家可以自行研究,有機會我再另開文章分析。
2.2微任務微任務是在es6和node環境中出現的一個任務類型,如果不考慮es6和node環境的話,我們只需要理解宏任務事件循環的執行過程就已經足夠了,但是到了es6和node環境,我們就需要理解微任務的執行順序了。微任務(micro-task)的API主要有:Promise, process.nextTick
這里我們直接引用一張流程圖幫助我們理解,如下:
在宏任務中執行的任務有兩種,分別是同步任務和異步任務,因為異步任務會在滿足觸發條件時才會推進任務隊列(task queue),然后等待主線程上的任務執行完畢,再讀取任務隊列中的任務事件,最后推進主線程執行,所以這里將異步任務即任務隊列看作是新的宏任務。執行的過程如上圖所示:
1、執行宏任務中同步任務,執行結束。
2、檢查是否存在可執行的微任務,有的話執行所有微任務,然后讀取任務隊列的任務事件,推進主線程形成新的宏任務;沒有的話則讀取任務隊列的任務事件,推進主線程形成新的宏任務。
3、執行新宏任務的事件任務,再檢查是否存在可執行的微任務,如此不斷的重復循環。
這就是加入微任務后的詳細事件循環,如果還沒有理解,那么們對一開始的例子做一個全面的分析,如下:
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
console.log("script end");
執行過程如下:
1、代碼塊通過語法分析和預編譯后,進入執行階段,當JS引擎主線程執行到console.log("script start");,JS引擎主線程認為該任務是同步任務,所以立刻執行輸出script start,然后繼續向下執行。
2、JS引擎主線程執行到setTimeout(function() { console.log("setTimeout"); }, 0);,JS引擎主線程認為setTimeout是異步任務API,則向瀏覽器內核進程申請開啟定時器線程進行計時和控制該setTimeout任務。由于W3C在HTML標準中規定setTimeout低于4ms的時間間隔算為4ms,那么當計時到4ms時,定時器線程就把該回調處理函數推進任務隊列中等待主線程執行,然后JS引擎主線程繼續向下執行。
3、JS引擎主線程執行到Promise.resolve().then(function() { console.log("promise1"); }).then(function() { console.log("promise2"); });,JS引擎主線程認為Promise是一個微任務,這把該任務劃分為微任務,等待執行。
4、JS引擎主線程執行到console.log("script end");,JS引擎主線程認為該任務是同步任務,所以立刻執行輸出script end。
5、主線程上的宏任務執行完畢,則開始檢測是否存在可執行的微任務,檢測到一個Promise微任務,那么立刻執行,輸出promise1和promise2
6、微任務執行完畢,主線程開始讀取任務隊列中的事件任務setTimeout,推入主線程形成新宏任務,然后在主線程中執行,輸出setTimeout
最后輸出結果:
script start
script end
promise1
promise2
setTimeout
以上便是JS引擎執行的全部過程,JS引擎的執行過程其實并不復雜,只要多思考多研究就可以理解,理解該過程后可以在一定程度上提高對JS的認識。
四、參考文獻Tasks, microtasks, queues and schedules
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/101626.html
摘要:如果對語法分析和預編譯,還有疑問引擎執行的過程的理解語法分析和預編譯階段。參與執行過程的線程分別是引擎線程也稱為內核,負責解析執行腳本程序的主線程例如引擎。以上便是引擎執行宏任務的整個過程。一、概述 js引擎執行過程主要分為三個階段,分別是語法分析,預編譯和執行階段,上篇文章我們介紹了語法分析和預編譯階段,那么我們先做個簡單概括,如下: 1、語法分析: 分別對加載完成的代碼塊進行語法檢驗,語...
摘要:所以覺得把這個執行的詳細過程整理一下,幫助更好的理解。類似的語法報錯的如下圖所示三預編譯階段代碼塊通過語法分析階段之后,語法都正確的下回進入預編譯階段。另開出新文章詳細分析,主要介紹執行階段中的同步任務執行和異步任務執行機制事件循環。 一、概述 js是一種非常靈活的語言,理解js引擎的執行過程對于我們學習js是非常有必要的。看了很多這方便文章,大多數是講的是事件循環(event loo...
摘要:所以覺得把這個執行的詳細過程整理一下,幫助更好的理解。類似的語法報錯的如下圖所示三預編譯階段代碼塊通過語法分析階段之后,語法都正確的下回進入預編譯階段。另開出新文章詳細分析,主要介紹執行階段中的同步任務執行和異步任務執行機制事件循環。 一、概述 js是一種非常靈活的語言,理解js引擎的執行過程對于我們學習js是非常有必要的??戳撕芏噙@方便文章,大多數是講的是事件循環(event loo...
摘要:引擎會執行其執行環境位于堆棧頂部的函數。當函數執行完畢時,當前執行棧會從堆棧中彈出去,并且控件將會到達其在當前堆棧下面的那個執行環境中。當完成以后,它的執行環境會會從堆棧中移出,并且控件會到達全局執行環境。 如果你想成為一個Javascript開發者,那么你一定要知道Javascript程序的內部運行原理。理解執行環境和執行棧是非常重要的,其有助于理解其他Javascript的概念,比...
摘要:瀏覽器總是運行位于作用域鏈頂部的當前執行上下文。不同執行上下文之間的變量命名沖突通過攀爬作用域鏈解決,從局部直到全局。它將攀爬作用域鏈檢查每一個執行上下文的變量對象,尋找和變量名稱匹配的值。 1>什么是執行上下文 Javascript中代碼的運行環境分為以下三種:全局級別的代碼 - 這個是默認的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境。函數級別的代碼 - 當執行一個函數...
閱讀 3142·2021-11-11 16:54
閱讀 2315·2021-09-04 16:48
閱讀 3226·2019-08-29 16:08
閱讀 646·2019-08-29 15:13
閱讀 1350·2019-08-29 15:09
閱讀 2669·2019-08-29 12:45
閱讀 1932·2019-08-29 12:12
閱讀 455·2019-08-26 18:27