深刻理解JavaScript運行機制

JavaScript單線程機制

JavaScript的一個語言特性(也是這門語言的核心)就是單線程。什麼是單線程呢?簡單地說就是同一時間只能作一件事,當有多個任務時,只能按照一個順序一個完成了再執行下一個node

爲何JS是單線程的呢?promise

  • JS最初被設計用在瀏覽器中,做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM
  • 若是瀏覽器中的JS是多線程的,會帶來很複雜的同步問題
    • 好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?
  • 因此爲了不復雜性,JavaScript從誕生起就是單線程

爲了提升CPU的利用率,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此這個標準並無改變JavaScript單線程的本質瀏覽器

任務隊列

同步和異步
同步和異步關注的是消息通知機制多線程

  • 同步:發出調用後,沒有獲得結果以前,該調用不返回,一旦調用返回,就獲得返回值了。 簡而言之就是調用者主動等待這個調用的結果
  • 異步:調用者在發出調用後這個調用就直接返回了,因此沒有返回結果。換句話說當一個異步過程調用發出後,調用者不會馬上獲得結果,而是調用發出後,被調用者經過狀態、通知或回調函數處理這個調用。

阻塞和非阻塞
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態異步

  • 阻塞調用是指調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果以後纔會返回
  • 非阻塞調用指在不能馬上獲得結果以前,該調用不會阻塞當前線程

單線程意味着同一時間只能進行一件事情,前面的事情結束才能執行後面的事件.當碰到須要時間的IO事件的時候問題就來了,必須等到這些結束後才往下進行,但這時CPU是閒着的.這樣浪費了不少計算機的性能.async

JavaScript語言的設計者意識到,這時主線程徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去.函數

因而,全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行
(4)主線程不斷重複上面的第三步oop

Event Loop

主線程從任務隊列中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)
圖片描述

上圖中,主線程運行的時候,產生堆(heap)和棧(stack),堆中可存放對象, 棧中可存放變量,函數,函數指針,代碼語句等性能

棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)
WebAPIs都是單獨線程,跟組件中的不同,不會阻塞主線程執行,好比獲取後臺數據,若同步就阻塞了,好比HTTP請求又開闢了一個線程spa

當執行棧中的任務完成後,主線程會去讀取事件隊列(先進先出),執行相應的回調函數

舉個例子,查看如下代碼

function read(){
    console.log(1);
    setTimeout(function (){
    console.log(2);
    setTimeout(function (){
    console.log(4)
    });
    });
    setTimeout(function (){
    console.log(5)
    })
    console.log(3);
}
read();
代碼執行結果:1 3 2 5 4

先執行同步代碼打印1,3,setTimeout異步代碼放到事件隊列中,先放的先執行,後放的後執行

定時器

"任務隊列"能夠放置定時事件,即指定某些代碼在多少時間以後執行

定時器功能主要由setTimeout()和setInterval()這兩個函數來完成,它們的內部運行機制徹底同樣,區別在於前者指定的代碼是一次性執行,後者則爲反覆執行,主要以setTimeout舉例說明

setTimeout()接受兩個參數,第一個是回調函數,第二個是推遲執行的毫秒數

setTimeout(function () {
    console.log(3)
}, 2000);
setTimeout(function () {
    console.log(1);
    setTimeout(function () {
        console.log(2);
    }, 1000);
}, 1000);

執行結果是:1 3 2

setTimeout()將事件放到等待任務隊裏中,當主任務隊列的任務執行完後,再執行等待任務隊列,等待任務隊裏中先返回的先執行

setTimeout()有時候明明寫的延時3秒,實際卻5,6秒才執行函數,這是怎麼回事呢?

  • setTimeout()只是將事件插入了「任務隊列」,必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等好久,因此並無辦法保證回調函數必定會在setTimeout()指定的時間執行

Promise與process.nextTick(callback)

除了廣義的同步任務和異步任務,咱們對任務有更精細的定義:

  • macro-task(宏任務):包括總體代碼script,setTimeout,setInterval
  • micro-task(微任務):Promise,process.nextTick
- process.nextTick:在事件循環的下一次循環中調用 callback 回調函數。效果是將一個函數推遲到代碼書寫的下一個同步方法執行完畢時或異步方法的事件回調函數開始執行時;與setTimeout(fn, 0) 函數的功能相似,但它的效率高多了

不一樣類型的任務會進入對應的Event Queue,好比 setTimeout 和 setInterval 會進入相同的Event Queue

事件循環的順序,決定js代碼的執行順序。進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。

事件循環,宏任務,微任務的關係以下所示:

  • 宏任務=>執行結束=>有可執行的微任務=>執行全部微任務=>開始新的宏任務
  • 宏任務=>執行結束=>沒有可執行的微任務=>開始新的宏任務

咱們用一段代碼說明:

setTimeout(function () {
    console.log('setTimeout');
});
new Promise(function (resolve) {
    console.log('promise');
}).then(function () {
    console.log('then');
});
console.log('console');
  • 這段代碼做爲宏任務,進入主線程
  • 先遇到 setTimeout ,那麼將其回調函數註冊後分發到宏任務Event Queue
  • 接下來遇到了 Promise , new Promise 當即執行, then 函數分發到微任務Event Queue
  • 遇到 console.log() ,當即執行

-總體代碼script做爲第一個宏任務執行結束,看看有哪些微任務?咱們發現了 then 在微任務Event Queue裏面執行

  • 第一輪事件循環結束了,咱們開始第二輪循環,固然要從宏任務Event Queue開始。咱們發現了宏任務Event Queue中 setTimeout 對應的回調函數,當即執行
  • 結束

咱們再看下一段代碼說明:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
以上代碼執行結果:1 2 TIMEOUT FIRED

上面代碼中,因爲process.nextTick方法指定的回調函數,老是在當前"執行棧"的尾部觸發,因此不只函數A比setTimeout指定的回調函數timeout先執行,並且函數B也比timeout先執行。這說明,若是有多個process.nextTick語句(無論它們是否嵌套),將所有在當前"執行棧"執行

咱們再看下一段代碼說明:

function a() {
    setTimeout(function () {
        console.log('a2');
    }, 0);
    process.nextTick(function () {
        console.log('a1')
    });
}
function b() {
    process.nextTick(function () {
        console.log('b1');
    })
}
a();
b();

一個函數執行會造成一個執行棧,任務隊列裏的回調函數每次只取一個,它執行的時候會造成一個執行棧,當你第一次運行這個腳本的時候,這個腳本的裏全部的同步代碼都會在一個執行棧裏

  • a的執行和b的執行在一個執行棧裏,它們共同在第一個宏任務中
  • a執行時候,會把a2放入宏任務隊列,把a1放入微任務隊列。
  • b執行的時候,把b1放入微任務隊列
  • -------------------第一個宏任務執行完畢-------------------------
  • 宏任務執行完畢後會把微任務隊列清空,也就是把a1 和b1都執行,輸出a1和b1
  • -------------------第一個微任務隊列清空--------------------------
  • 而後從宏任務隊列中取出下一個宏任務,也就是a2執行.輸出a2

爲何一個宏任務要搭配處理一個微任務
由於這樣最合理,微任務就是在有空時須要當即執行的任務,宏任務相比微任務能夠滯後執行。他們雖然都屬於異步任務,可是經過這種優先級的設置達到了控制異步回調執行順序的目的。值得注意的是:同步代碼執行完會先清空微任務,而後取出宏任務隊列裏的第一個事件對應的回調到執行棧執行,而後再清空一次微任務,如此循環...

經過以上三段代碼,您是否對JS的執行順序有所瞭解呢

咱們來分析一段較複雜的代碼,看看你是否真的掌握了js的執行機制

console.log('main1');
setTimeout(function () {
    console.log('setTimeout');
    process.nextTick(function () {
        console.log('process.nextTick2');
    });
}, 0);
new Promise(function (resolve, reject) {
    console.log('promise');
    resolve();
}).then(function () {
    console.log('promise then');
});
process.nextTick(function () {
    console.log('process.nextTick1');
});
console.log('main2');

以上代碼的執行結果是:main1=>promise=>main2=>process.nextTick1=>promise then=>setTimeout=>process.nextTick2

  • 系統啓動執行腳本,這個腳本就是一個宏任務,執行代碼塊中全部的同步代碼,輸出main1
  • next1放入微任務,setTimeout+nextTick2(下一輪)放入宏任務隊列
  • promise構造函數部分是同步的,馬上執行輸出promise,promise then放入微任務
  • 下面同步代碼輸出main2
  • 接下來執行微任務輸出nextTick1,promise then
  • 接下來執行宏任務輸出setTimeout,將nexttick2放入微任務隊列
  • 接下來執行微任務nexttick2
  • nextTick是由node本身定義並實現的概念,它的回調調用入口在event loop過程當中MakeCallback函數的末尾,驅動調用清空js層的queue,最後再執行microtasks,適當處理下可能觸發的promise,明顯 process.nextTick1> promise.then
相關文章
相關標籤/搜索