js引擎執行過程主要分為三個階段,分別是語法分析,預編譯和執行階段,上篇文章我們介紹了語法分析和預編譯階段,那麼我們先做個簡單概括,如下:
文章首發:www.mwcxs.top/page/564.ht…
1、語法分析: 分別對載入完成的程式碼塊進行語法檢驗,語法正確則進入預編譯階段;不正確則停止該程式碼塊的執行,查詢下一個程式碼塊並進行載入,載入完成再次進入該程式碼塊的語法分析階段。
2、預編譯:通過語法分析階段後,進入預編譯階段,則建立變數物件(建立arguments物件(函式執行環境下),函式宣告提前解析,變數宣告提升),確定作用域鏈以及this指向。
如果對語法分析和預編譯,還有疑問:
本文主要分析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);複製程式碼
2、微任務(micro-task)
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,他們的執行順序如上。可能很多人對上面的分析並不理解,那麼我們接下來繼續對上面例子進行詳細分析。
巨集任務(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實現計時器的原理,對高頻觸發的事件進行優化,實現點在於將多個觸發事件合併成一個,這就是防抖和節流,本文先不做具體講解,大家可以自行研究,有機會我再另開文章分析。
微任務是在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的認識。