圖文細說JavaScript 的運行機制

echa攻城獅 發佈 2020-06-10T15:48:38+00:00

作者: 卡特轉發連結:https://mp.weixin.qq.com/s/7g6hajYAvPBUQvGnPR3aag前言前一段小編也詳細講過關於JavaScript 的運行機制:「硬核JS」一次搞懂JS運行機制,純文字化的理論,對於初學者來說很難去理解它。


作者: 卡特

轉發連結:https://mp.weixin.qq.com/s/7g6hajYAvPBUQvGnPR3aag

前言

前一段小編也詳細講過關於JavaScript 的運行機制:「硬核JS」一次搞懂JS運行機制,純文字化的理論,對於初學者來說很難去理解它。這次小編一起重溫並且圖形化+案例細說。

1. 單線程的JavaScript

js是單線程的,基於事件循環,非阻塞IO的。特點: 處理I/O型的應用,不適合CPU運算密集型的應用。說明: 事件循環中使用一個事件隊列,在每個時間點上,系統只會處理一個事件,即使電腦有多個CPU核心,也無法同時並行的處理多個事件。因此,node.js在I/O型的應用中,給每一個輸入輸出定義一個回調函數,node.js會自動將其加入到事件輪詢的處理隊列里,當I/O操作完成後,這個回調函數會被觸發,系統會繼續處理其他的請求。

  • 然而單線程不應該是自上而下按照順序執行的嗎?
  • 下面的代碼輸出順序就被打亂了
function fn(){
    console.log('start');
    setTimeout(()=>{
        console.log('setTimeout');
    },0);
    console.log('end');
}

fn() // 輸出 start end setTimeout

2. JavaScript中的同步異步

js的同步異步是如何實現的?js中包含諸多創建異步的函數如:seTimeout,setInterval,dom事件,ajax,Promise,process.nextTick等函數

  1. 因為單線程,所以代碼自上而下執行,所有代碼被放到執行棧中執行;
  2. 遇到異步函數將回調函數添加到一個任務隊列裡面;
  3. 當執行棧中的代碼執行完以後,會去循環任務隊列里的函數;
  4. 將任務隊列里的函數放到執行棧中執行;
  5. 如此往復,稱為事件循環;
  • 這樣分析,上一段的代碼就得到了合理的解釋;
  • 再來看一下這段代碼;
function fn() {
    setTimeout(()=>{
        console.log('a');
    },0);
    new Promise((resolve)=>{
        console.log('b');
        resolve();
    }).then(()=>{
        console.log('c')
    });
}
fn() // b c a

3. 宏任務和微任務

兩任務在同步異步中處於什麼地位?兩個任務分別處於任務隊列中的宏隊列與微隊列中;宏隊列與微隊列組成了任務隊列;任務隊列將任務放入執行棧中執行

宏任務:

宏隊列,macrotask,也叫tasks。異步任務的回調會依次進入macro task queue,等待後續被調用,這些異步任務包括:

  • setTimeout
  • setInterval
  • setImmediate (Node獨有)
  • requestAnimationFrame (瀏覽器獨有)
  • I/O
  • UI rendering (瀏覽器獨有)

微任務:

微隊列,microtask,也叫jobs。異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包括:

  • process.nextTick (Node獨有)
  • Promise
  • Object.observe
  • MutationObserver

執行全局Script同步代碼,這些同步代碼有一些是同步語句,有一些是異步語句(比如setTimeout等);

全局Script代碼執行完畢後,執行棧Stack會清空;

從微隊列中取出位於隊首的回調任務,放入執行棧Stack中執行,執行完後微隊列長度減1;

繼續循環取出位於微隊列的任務,放入執行棧Stack中執行,以此類推,直到直到把微任務執行完畢。注意,如果在執行微任務的過程中,又產生了微任務,那麼會加入到微隊列的末尾,也會在這個周期被調用執行;

微隊列中的所有微任務都執行完畢,此時微隊列為空隊列,執行棧Stack也為空;

取出宏隊列中的任務,放入執行棧Stack中執行;

執行完畢後,執行棧Stack為空;

重複第3-7個步驟;

以上才是一個完整的事件循環

  • 來一個稍微複雜點的代碼
function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3);
        });
    },0);
    
    new Promise((resolve, reject) => {
        console.log(4);
        resolve(5);
    }).then(data => {
        console.log(data);
    });
    
    setTimeout(() => {
        console.log(6);
    },0);
    
    console.log(7);
}
fn(); //

流程重現

  1. 執行函數同步語句;
  • step1console.log(1);執行棧:[ console ]宏任務:[]微任務:[]列印結果:1
  • step2setTimeout(() => {
    // 這個回調函數叫做callback1,setTimeout屬於宏任務,所以放到宏隊列中
    console.log(2);
    Promise.resolve().then(() => {
    console.log(3)
    });
    });執行棧:[ setTimeout ]宏任務:[ callback1 ]微任務:[]列印結果:1
  • step3new Promise((resolve, reject) => {
    // 注意,這裡是同步執行的
    console.log(4);
    resolve(5)
    }).then((data) => {
    // 這個回調函數叫做callback2,promise屬於微任務,所以放到微隊列中
    console.log(data);
    });執行棧:[ promise ]宏任務:[ callback1 ]微任務:[ callback2 ]列印結果:14
  • step4setTimeout(() => {
    // 這個回調函數叫做callback3,setTimeout屬於宏任務,所以放到宏隊列中
    console.log(6);
    })執行棧:[ setTimeout ]宏任務:[ callback1 , callback3 ]微任務:[ callback2 ]列印結果:14
  • step5console.log(7)執行棧:[ console ]宏任務:[ callback1 , callback3 ]微任務:[ callback2 ]列印結果:147
  1. 同步語句執行完畢,從微隊列中依次取出任務執行,直到微隊列為空
  • step6console.log(data) // 這裡data是Promise的成功參數為5執行棧:[ callback2 ]宏任務:[ callback1 , callback3 ]微任務:[]列印結果:1475
  1. 這裡微隊列中只有一個任務,執行完後開始從宏隊列中取任務執行
  • step7console.log(2);執行棧:[ callback1 ]宏任務:[ callback3 ]微任務:[]列印結果:14752但是執行callback1的時候遇到另一個Promise,Promise異步執行完畢以後在微隊列中又註冊了一個callback4函數
  • step8Promise.resolve().then(() => {
    // 這個回調函數叫做callback4,promise屬於微任務,所以放到微隊列中
    console.log(3);
    });執行棧:[ Promise ]宏任務:[ callback3 ]微任務:[ callback4 ]列印結果:14752
  1. 取出一個宏任務macrotask執行完畢,然後再去微任務隊列microtask queue中依次取出執行
  • step9console.log(3)執行棧:[ callback4 ]宏任務:[ callback3 ]微任務:[]列印結果:147523
  1. 微隊列全部執行完,再去宏隊列中取第一個任務執行
  • step10console.log(3)執行棧:[ callback3 ]宏任務:[]微任務:[]列印結果:1475236
  1. 以上全部執行完畢,執行棧,宏隊列,微隊列均為空執行棧:[]宏任務:[]微任務:[]列印結果:1475236
  • 再來一段複雜代碼
function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3)
        });
    });
    
    new Promise((resolve, reject) => {
        console.log(4)
        resolve(5)
    }).then((data) => {
        console.log(data);
        
        Promise.resolve().then(() => {
            console.log(6)
        }).then(() => {
            console.log(7)
            
            setTimeout(() => {
                console.log(8)
            }, 0);
        });
    })
    
    setTimeout(() => {
        console.log(9);
    })
    
    console.log(10);
}
fn();

4. NodeJS中的事件循環

NodeJS中的宏任務和微任務

NodeJS的Event Loop中,執行宏隊列的回調任務有6個階段,如下圖:

各個階段執行的任務如下:

  • timers階段:這個階段執行setTimeout和setInterval預定的callback
  • I/O callback階段:執行除了close事件的callbacks、被timers設定的callbacks、setImmediate()設定的callbacks這些之外的callbacks idle, prepare階段:僅node內部使用
  • poll階段:獲取新的I/O事件,適當的條件下node將阻塞在這裡
  • check階段:執行setImmediate()設定的callbacks
  • close callbacks階段:執行socket.on('close', ....)這些callbacks

NodeJS的宏隊列:

  • Timers Queue
  • IO Callbacks Queue
  • Check Queue
  • Close Callbacks Queue

這4個都屬於宏隊列,但是在瀏覽器中,可以認為只有一個宏隊列,所有的宏任務都會被加到這一個宏隊列中,但是在NodeJS中,不同的宏任務會被放置在不同的宏隊列中

NodeJS的微隊列:

  • Next Tick Queue:是放置process.nextTick(callback)的回調任務的
  • Other Micro Queue:放置其他微任務,比如Promise等

在瀏覽器中,也可以認為只有一個微隊列,所有的微任務都會被加到這一個微隊列中,但是在NodeJS中,不同的微任務會被放置在不同的微隊列中

NodeJS中的事件循環過程

  1. 執行全局的同步代碼;
  2. 執行微任務先執行next tick queue所有任務,再執行other micro tasks queue中的所有任務;
  3. 開始執行宏任務,共6個階段,從第1個階段開始執行相應每一個階段宏隊列中的所有任務,注意,這裡是所有每個階段宏任務隊列的所有任務,在瀏覽器的Event Loop中是只取宏隊列的第一個任務出來執行,每一個階段的宏任務執行完畢後,開始執行微任務,回到步驟2;

Timers Queue -> 步驟2 ->I/O Queue -> 步驟2 ->Check Queue -> 步驟2 ->Close Callback Queue -> 步驟2 ->Timers Queue

  • 再看兩張圖

  • 代碼又來了
function fn(){

    console.log('start');

    setTimeout(() => {              // callback1
        console.log(111);
        
        setTimeout(() => {          // callback2
            console.log(222);
        }, 0);
        
        setImmediate(() => {        // callback3
            console.log(333);
        });
        
        process.nextTick(() => {    // callback4
            console.log(444);
        });
        
    }, 0);
    
    setImmediate(() => {            // callback5
        console.log(555);
        
        process.nextTick(() => {    // callback6
           console.log(666);
        });
    });
    
    setTimeout(() => {              // callback7
        console.log(777);
        
        process.nextTick(() => {    // callback8
            console.log(888);
        });
    }, 0);
    
    process.nextTick(() => {        // callback9
        console.log(999);
    });
    
    console.log('end');
}
fn();

//  before version 11.0.0  start end 999 111 777 444 888 555 333 666 222
//  after  version 11.0.0  start end 999 111 444 777 888 555 666 333 222

PS:版本不同導致運行結果不同

總結:

  • 瀏覽器的Event Loop和NodeJS的Event Loop是不同的,實現機制也不一樣,不要混為一談。
  • NodeJS可以理解成有4個宏任務隊列和2個微任務隊列,但是執行宏任務時有6個階段。先執行全局Script代碼,執行完同步代碼調用棧清空後,先從微任務隊列Next Tick Queue中依次取出所有的任務放入調用棧中執行,再從微任務隊列Other Microtask Queue中依次取出所有的任務放入調用棧中執行。然後開始宏任務的6個階段,每個階段都將該宏任務隊列中的所有任務都取出來執行(注意,這裡和瀏覽器不一樣,瀏覽器只取一個),每個宏任務階段執行完畢後,開始執行微任務,再開始執行下一階段宏任務,以此構成事件循環。
  • MacroTask包括:setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering。
  • Microtask包括:process.nextTick(Node)、Promise、Object.observe、MutationObserver。
  • v11以前 是上面說的那樣;v11以後將Node環境的事件循環和瀏覽器的統一了。
  • process.nextTick 上限是1000?
  • 寫一個休眠函數 達到阻塞目的

練習:

  1. 宏任務執行順序;
  2. 微任務執行順序;
  3. 宏任務微任務結合執行順序;

推薦JavaScript經典實例學習資料文章

《一個輕量級 JavaScript 全文搜索庫,輕鬆實現站內離線搜索》

《推薦Web程式設計師常用的15個原始碼編輯器》

《10個實用的JS技巧「值得收藏」》

《細品269個JavaScript小函數,讓你少加班熬夜(一)「值得收藏」》

《細品269個JavaScript小函數,讓你少加班熬夜(二)「值得收藏」》

《細品269個JavaScript小函數,讓你少加班熬夜(三)「值得收藏」》

《細品269個JavaScript小函數,讓你少加班熬夜(四)「值得收藏」》

《細品269個JavaScript小函數,讓你少加班熬夜(五)「值得收藏」》

《細品269個JavaScript小函數,讓你少加班熬夜(六)「值得收藏」》

《深入JavaScript教你內存泄漏如何防範》

《手把手教你7個有趣的JavaScript 項目-上「附源碼」》

《手把手教你7個有趣的JavaScript 項目-下「附源碼」》

《JavaScript 使用 mediaDevices API 訪問攝像頭自拍》

《手把手教你前端代碼如何做錯誤上報「JS篇」》

《一文讓你徹底搞懂移動前端和Web 前端區別在哪裡》

《63個JavaScript 正則大禮包「值得收藏」》

《提高你的 JavaScript 技能10 個問答題》

《JavaScript圖表庫的5個首選》

《一文徹底搞懂JavaScript 中Object.freeze與Object.seal的用法》

《可視化的 JS:動態圖演示 - 事件循環 Event Loop的過程》

《教你如何用動態規劃和貪心算法實現前端瀑布流布局「實踐」》

《可視化的 js:動態圖演示 Promises & Async/Await 的過程》

《原生JS封裝拖動驗證滑塊你會嗎?「實踐」》

《如何實現高性能的在線 PDF 預覽》

《細說使用字體庫加密數據-仿58同城》

《Node.js要完了嗎?》

《Pug 3.0.0正式發布,不再支持 Node.js 6/8》

《純JS手寫輪播圖(代碼邏輯清晰,通俗易懂)》

《JavaScript 20 年 中文版之創立標準》

《值得收藏的前端常用60餘種工具方法「JS篇」》

《箭頭函數和常規函數之間的 5 個區別》

《通過發布/訂閱的設計模式搞懂 Node.js 核心模塊 Events》

《「前端篇」不再為正則煩惱》

《「速圍」Node.js V14.3.0 發布支持頂級 Await 和 REPL 增強功能》

《深入細品瀏覽器原理「流程圖」》

《JavaScript 已進入第三個時代,未來將何去何從?》

《前端上傳前預覽文件 image、text、json、video、audio「實踐」》

《深入細品 EventLoop 和瀏覽器渲染、幀動畫、空閒回調的關係》

《推薦13個有用的JavaScript數組技巧「值得收藏」》

《前端必備基礎知識:window.location 詳解》

《不要再依賴CommonJS了》

《犀牛書作者:最該忘記的JavaScript特性》

《36個工作中常用的JavaScript函數片段「值得收藏」》

《Node + H5 實現大文件分片上傳、斷點續傳》

《一文了解文件上傳全過程(1.8w字深度解析)「前端進階必備」》

《【實踐總結】關於小程序掙脫枷鎖實現批量上傳》

《手把手教你前端的各種文件上傳攻略和大文件斷點續傳》

《字節跳動面試官:請你實現一個大文件上傳和斷點續傳》

《談談前端關於文件上傳下載那些事【實踐】》

《手把手教你如何編寫一個前端圖片壓縮、方向糾正、預覽、上傳插件》

《最全的 JavaScript 模塊化方案和工具》

《「前端進階」JS中的內存管理》

《JavaScript正則深入以及10個非常有意思的正則實戰》

《前端面試者經常忽視的一道JavaScript 面試題》

《一行JS代碼實現一個簡單的模板字符串替換「實踐」》

《JS代碼是如何被壓縮的「前端高級進階」》

《前端開發規範:命名規範、html規範、css規範、js規範》

《【規範篇】前端團隊代碼規範最佳實踐》

《100個原生JavaScript代碼片段知識點詳細匯總【實踐】》

《關於前端174道 JavaScript知識點匯總(一)》

《關於前端174道 JavaScript知識點匯總(二)》

《關於前端174道 JavaScript知識點匯總(三)》

《幾個非常有意思的javascript知識點總結【實踐】》

《都2020年了,你還不會JavaScript 裝飾器?》

《JavaScript實現圖片合成下載》

《70個JavaScript知識點詳細總結(上)【實踐】》

《70個JavaScript知識點詳細總結(下)【實踐】》

《開源了一個 JavaScript 版敏感詞過濾庫》

《送你 43 道 JavaScript 面試題》

《3個很棒的小眾JavaScript庫,你值得擁有》

《手把手教你深入鞏固JavaScript知識體系【思維導圖】》

《推薦7個很棒的JavaScript產品步驟引導庫》

《Echa哥教你徹底弄懂 JavaScript 執行機制》

《一個合格的中級前端工程師需要掌握的 28 個 JavaScript 技巧》

《深入解析高頻項目中運用到的知識點匯總【JS篇】》

《JavaScript 工具函數大全【新】》

《從JavaScript中看設計模式(總結)》

《身份證號碼的正則表達式及驗證詳解(JavaScript,Regex)》

《瀏覽器中實現JavaScript計時器的4種創新方式》

《Three.js 動效方案》

《手把手教你常用的59個JS類方法》

《127個常用的JS代碼片段,每段代碼花30秒就能看懂-【上】》

《深入淺出講解 js 深拷貝 vs 淺拷貝》

《手把手教你JS開發H5遊戲【消滅星星】》

《深入淺出講解JS中this/apply/call/bind巧妙用法【實踐】》

《手把手教你全方位解讀JS中this真正含義【實踐】》

《書到用時方恨少,一大波JS開發工具函數來了》

《乾貨滿滿!如何優雅簡潔地實現時鐘翻牌器(支持JS/Vue/React)》

《手把手教你JS 異步編程六種方案【實踐】》

《讓你減少加班的15條高效JS技巧知識點匯總【實踐】》

《手把手教你JS開發H5遊戲【黃金礦工】》

《手把手教你JS實現監控瀏覽器上下左右滾動》

《JS 經典實例知識點整理匯總【實踐】》

《2.6萬字JS乾貨分享,帶你領略前端魅力【基礎篇】》

《2.6萬字JS乾貨分享,帶你領略前端魅力【實踐篇】》

《簡單幾步讓你的 JS 寫得更漂亮》

《恭喜你獲得治療JS this的詳細藥方》

《談談前端關於文件上傳下載那些事【實踐】》

《面試中教你繞過關於 JavaScript 作用域的 5 個坑》

《Jquery插件(常用的插件庫)》

《【JS】如何防止重複發送ajax請求》

《JavaScript+Canvas實現自定義畫板》

《Continuation 在 JS 中的應用「前端篇」》

作者: 卡特

轉發連結:https://mp.weixin.qq.com/s/7g6hajYAvPBUQvGnPR3aag

關鍵字: