【JS深淵】幹它!必定要完全弄懂javascript執行機制(二)

新年祝福

過年啦,過年啦!lotoze在這裏爲你們送上新年祝福:
祝@全部人,在新的一年裏個個身體健康,萬事如意!
祝@武漢人民,跨過一切困難險阻,打敗一切疫情!
祝@爸媽爺爺奶奶,幸福美滿,健康長壽!
祝@愛人,一直永遠開心下去,我將永遠愛你!
祝@我,可以讓我身邊的人在2020年更好!javascript

前言

【JS深淵】系列是我用來對前端原生javascript語言相關技術進行的深度探究或者筆記記錄,我我的是一個很愛去挖層的這麼一我的,但不幸的是覺悟的太晚,但我不會放棄。決定開始寫博客目的有兩個,一個是有儀式性的去持續提升本身的技術能力,另外一個就是但願向上再向上。不過本身能力也是有限的,若是寫的很差或者出現什麼紕漏又或者錯誤的地方,脫褲式歡迎你們的建議、指正。沒錯看着我,對,盯着你的屏幕,別眨眼,請記住個人這句話:
您的反饋是就是我持續進步的動力
好了,收!biu~html

正文

在當咱們寫完js代碼執行的時候,其內部的執行機制很是複雜,但主要會經歷三個過程:語法分析、預編譯、真正執行。這其中,在真正執行這一過程當中是最龐雜的。此篇幅就是爲真正執行這一過程的分析而誕生。 若是對前兩個過程有疑惑請點擊此處【JS深淵】幹它!必定要完全弄懂javascript執行機制(一)前端

我記得我當時研究js的執行機制,是由於一個問題:java

js是一個單線程語言,是否表明着參與js執行過程的線程只有一個?git

答案是否認的。在我回答爲何以前咱們須要去了解一些知識點。這是我爲了解決這個問題去百度的線程知識點。github

進程與線程

  • 進程
    進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。
  • 線程
    線程(英語:thread)是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。在UnixSystemV及SunOS中也被稱爲輕量進程(lightweightprocesses),但輕量進程更多指內核線程(kernelthread),而把用戶線程(userthread)稱爲線程。
    線程是獨立調度和分派的基本單位。線程能夠爲操做系統內核調度的內核線程,如Win32線程;由用戶進程自行調度的用戶線程,如Linux平臺的POSIX Thread;或者由內核與用戶進程,如Windows 7的線程,進行混合調度。
    同一進程中的多條線程將共享該進程中的所有系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧(call stack),本身的寄存器環境(register context),本身的線程本地存儲(thread-local storage)。 一個進程能夠有不少線程,每條線程並行執行不一樣的任務。

以上是摘自百度百科,由於我再總結也沒有百度總結的好啊。可是上邊的這些是給非人類看的。也就是看不懂的話——俗稱「廢話」。promise

對於咱們來講,無需把全部都記住,由於知識是學不完的,要只學本身須要的。
須要的是要對進程和線程的有個基本認識:

  1. 進程和線程都是計算機系統中重要概念。
  2. 進程是操做系統結構的基礎,是程序的實體,是系統資源分配的最小單位。
  3. 線程是cpu調度的最小單位,也是程序執行的最小單位。
  4. 進程和線程也有着不可分割的聯繫。進程是線程的容器。也就是說進程中能夠包含若干線程。

那麼回到一開始的問題上來,剛纔說答案是否認的,緣由是什麼?瀏覽器

咱們一般所說的js基本是依賴於瀏覽器的。瀏覽器也是咱們前端打交道最多的。咱們說瀏覽器是計算機系統上的一個軟件。進程是程序的實體,是系統資源分配的最小單位。也就是說當你啓動瀏覽器時,其實就是啓動了一個瀏覽器進程。進程是線程的容器,一個進程下能夠併發包含多個線程。瀏覽器能夠作的事情遠遠不止一件事,除了能夠解釋執行js代碼,還能夠渲染頁面、異步執行、計時器/定時器、事件監聽等。因此這確定不只僅是一個線程就能夠作到的。因此參與js執行過程的線程不止一個。bash

既然js執行過程參與的線程不止一個,那麼參與的線程有哪些呢?數據結構

參與的有4個線程,其中JS引擎線程只負責執行js,其餘三個線程進行配合。值得一提的是,GUI渲染線程負責渲染html元素,可是在js引擎線程執行腳本的時候,GUI渲染引擎是被掛起的。

  • JS引擎線程: 也稱爲JS內核,負責解析執行Javascript腳本程序的主線程(例如V8引擎)
  • 事件觸發線程: 歸屬於瀏覽器內核進程,不受JS引擎線程控制。主要用於控制事件(例如鼠標,鍵盤等事件),當該事件被觸發時候,事件觸發線程就會把該事件的處理函數推動事件隊列,等待JS引擎線程執行
  • 定時器觸發線程:主要控制計時器setInterval和延時器setTimeout,用於定時器的計時,計時完畢,知足定時器的觸發條件,則將定時器的處理函數推動事件隊列中,等待JS引擎線程執行。 注:W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms。
  • HTTP異步請求線程:經過XMLHttpRequest鏈接後,經過瀏覽器新開的一個線程,監控readyState狀態變動時,若是設置了該狀態的回調函數,則將該狀態的處理函數推動事件隊列中,等待JS引擎線程執行。 注:瀏覽器對通一域名請求的併發鏈接數是有限制的,Chrome和Firefox限制數爲6個,ie8則爲10個。

總結一下,都是javascript線程在運行js腳本,其餘三個線程都是進行在知足條件是將執行函數推入事件隊列中,等待JS引擎主線程執行。

執行階段

爲了更好的理解執行過程,須要引用個例子(英文原版),這個例子很是經典,建議英文基礎好的閱讀,很是不錯的文章。

console.log("script start");

setTimeout(function () {
    console.log("setTimeout");
}, 0)

Promise.reslove().then(function() {
    console.log("promise1");
}).then(function() {
    console.log("promise2");
})

console.log("script end");
複製代碼

這裏直接按照執行過程劃分代碼,這裏只簡單描述一下過程。

  1. 宏任務(macro-task),宏任務又按執行順序分爲同步任務和異步任務
    • 同步任務
    console.log("script start");
    console.log("script end");
    複製代碼
    • 異步任務
    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-tsk),在最新的ECMAScript中,微任務稱爲jobs,宏任務稱爲task,他們的執行順序如上。不少人對上面的執行順序分析不是很理解,那麼咱們接下來繼續對上面例子進行詳細分析。

宏任務

宏任務(macro-task)可分爲同步任務和異步任務。

  • 同步任務指的是在JS引擎上按順序執行的任務,只有前一個任務執行完畢後,才能執行後一個任務,造成一個棧(stack,這個棧能夠理解爲函數調用棧) 。
  • 異步任務指的是不直接進入JS引擎主線程,而是知足條件時,相關的線程將異步任務推動任務隊列(task queue),等待JS引擎主線程上的任務所有執行完畢,空閒時讀取這個任務隊列執行的任務,例如異步Ajax、Dom事件、setTimeout等。

理解宏任務中的同步任務和異步任務的執行順序,那麼就至關於理解了JS異步任務執行機制——事件輪詢(Event Loop)

事件輪詢

事件輪詢能夠理解爲由三個部分組成,分別是:

  • 主線程執行棧
  • 異步任務等待觸發
  • 任務隊列

任務隊列(task queue)就是以隊列的數據結構對事件任務進行管控,特色是先進先出,後進後出。

這裏直接引用一張著名的圖片(參考自Philip Roberts的演講《Help, I’m stuck in an event-loop》),幫助咱們理解:

在JS引擎主線程執行過程當中:

  • 首先執行宏任務的同步任務,在主線程上造成一個執行棧,能夠理解爲函數調用棧。
  • 當執行棧中的函數調用到一些異步的API(例如Ajax、Dom事件、setTimeout等),則會開啓對應線程(Http異步請求線程,事件觸發線程和定時器觸發線程)進行監控和控制。
  • 當JS引擎主線程上的任務執行完畢,則會讀取任務隊列中的事件,將任務隊列中的事件任務推動主線程中,按任務隊列順序執行。
  • 當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引擎執行宏任務的整個過程。

理解了宏任務中的執行過程後,跟lotoze放飛一下,拓展性思考幾個問題:

setTimeout和setInterval都是異步任務的定時器,須要添加到任務隊列中等待JS引擎主線程空閒時間讀取任務隊列執行,那麼使用setTimeout實現setInterval,會有區別嗎?

答案是有區別的。不妨思考一下:

  • setTimeout實現setInterval只能經過遞歸來實現。
  • setTimeout是到了指定的時間就把事件推到任務隊列中,只有當任務隊列中的setTimeout事件任務被主線程執行後,才能繼續再次到了指定時間的時候把事件推到任務隊列中,依次反覆,那麼setTimeout的事件執行確定比指定時間要久,具體相差多少跟代碼執行事件有關。
  • setInterval則是每次都精確的間隔一段時間就向任務隊列推入一個事件,不管上一個setInterval事件是否已經執行,因此有可能存在setInterval的事件任務累積,致使setInterval的代碼重複連續執行好屢次,影響頁面性能。

lotoze我在不久之前對於seTimeout和setInterval的認知還停留在setTimeout只執行一次,setInterval能夠按照時間間隔循環不停的執行。可是如今卻有了不少新的認識。

  1. 使用setTimeout實現計時器功能比setInterval要好一些,但setInterval的事件任務累積也是有辦法解決的,最有效的辦法就是加鎖。
  2. setTimeout模擬setInterval最本質的區別是setTimeout必須在任務隊列中的一個setTimeout事件任務被JS引擎主線程執行完後下一個才能再推動任務隊列中來,而setInterval不管你任務隊列中有沒有setInterval的事件任務或者有沒有已經執行完畢了在指定時間到了都會再次推動任務隊列中來,形成任務累積。
  3. 若是不考慮瀏覽器兼容性問題,使用requestAnimationFrame是更好的選擇。

高頻率觸發事件(例如滾動監聽、input輸出)觸發頻率太高會影響頁面性能,甚至形成頁面卡頓,咱們是否能夠利用計時器的原理來進行優化呢?

是能夠的。咱們能夠利用setTimeout實現計時器的原理,對高頻率的事件監聽進行優化,實現點在於多個事件合併成一個,這就是防抖節流。這裏我會在後面的文章中去詳細講解一下實現方式,這裏就很少說啦。

微任務

微任務是在ES6和Node環境中出現的一個任務類型,若是不考慮ES6和Node環境的話,咱們只須要理解宏任務中事件輪詢的執行過程就足夠了,可是到了ES6和Node環境,就須要理解摻雜了微任務的執行順序了。
微任務(micro-task0)的API主要有:Promise、process.nextTick。

這裏直接引用一張流程圖幫助咱們理解一下:

在宏任務中執行的任務有兩種,分別是 同步任務異步任務,由於異步任務會在知足觸發條件時纔會推動任務隊列(task queue),而後等待主線程上的任務執行完畢,再讀取任務隊列中的任務事件,最後推動主線程執行,因此這裏將異步任務即任務隊列看作是新的宏任務,執行過程如上圖所示:

  1. 執行宏任務中的同步任務,執行結束。
  2. 檢查是否存在可執行的微任務,有的話執行全部的微任務,而後讀取任務隊列的異步任務事件,推動主線程造成新的宏任務。沒有的話則讀取任務隊列的異步任務事件,推動主線程造成新的宏任務。
  3. 執行新宏任務(任務隊列中異步任務),再檢查是否存在可執行的微任務,如此反覆循環。

這就是加入微任務後的詳細事件輪詢,若是尚未理解,那麼再次對一開始的例子作一個全面的分析以及補充:

console.log("script start");

setTimeout(function () {
    console.log("setTimeout");
}, 0)

Promise.reslove().then(function() {
    console.log("promise1");
}).then(function() {
    console.log("promise2");
})

console.log("script end");
複製代碼

執行過程:

  1. 代碼塊先經過語法分析,通讀代碼,看一看有沒有低級的語法錯誤,若是有則拋出錯誤,若是沒有則直接進入預編譯階段。須要注意的是JS代碼是解釋一行執行一行。
  2. 預編譯過程是對代碼真正執行的前一刻準備工做,在這一過程當中會生成GO(Global Object)以及一些AO(Active Object)對象,造成彼此聯繫的做用域鏈。
  3. 程序進入執行階段,當JS引擎主線程按代碼自上而下順序依次解釋執行,當執行console.log("script start");,JS引擎主線程任務該任務是同步任務,因此馬上執行輸出script start,而後向下執行。
  4. JS引擎主線程執行到setTimeout(function () { console.log("setTimeout") }, 0),JS引擎主線程認爲setTimeout是異步任務API,則向瀏覽器內核進程申請開啓定時器線程進行計時和控制setTimeout任務。因爲W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms,那麼計時器到4ms時,定時器線程就把該回調處理函數推動任務隊列中等待主線程執行,而後JS引擎主線程繼續向下解釋執行。
  5. JS引擎主線程執行到Promise.resolve().then(function () { console.log("promise1") }).then(function () { console.log("promise2") });,JS引擎主線程認爲Promise這是一個微任務(micro-task),就把該任務劃分爲微任務,等待執行。
  6. JS引擎主線程執行到console.log("script end");,JS引擎主線程認爲該任務是同步任務,因此當即執行輸出script end。
  7. 主線程上的宏任務執行完畢,則開始檢查是否存在可執行的微任務, 檢測到一個Promise微任務,那麼馬上執行,輸出promise1和promise2。
  8. 微任務執行完畢,主線程開始讀取任務隊列中的定時器任務setTimeout,推入主線程造成新宏任務,而後在主線程中執行,輸出setTimeout。

最後的輸出結果是:

"script start"
"script end"
"promise1"
"promise2"
"setTimeout"
複製代碼

怎麼樣?你是否懂了?😁😁

參考:

lotoze | 【原創】
着重說明
裏面一些表情圖片並不是原創,只是爲了讀者讀起來不是那麼枯燥乏味。但若是原做者以爲有侵犯版權的意思,請使用下方聯繫方式與我聯繫,爲了尊重原創做者的辛苦創做,我將及時處理!
固然,沒事也能夠聯繫啦😘😘歡迎交流!

求贊/求關注

寫做不易,
若是您還以爲湊合,就給個贊!
若是以爲確實以爲: 「老傢伙,有你的啊!」就加個關注!
若是文章有任何的錯誤,脫褲式歡迎你們來進行批評指正!
每個鼓勵都是lotoze我持續拋頭顱,撒雞血的創做動力!
每個批評反饋也都是lotoze我持續成長的臺階!

相關文章
相關標籤/搜索