一次性搞懂JavaScript 執行機制

我的博客html

看了不少js執行機制的文章彷佛都是似懂非懂,到技術面問的時候,理不清思緒。總結了衆多文章的例子和精華,但願能幫到大家node

JavaScript 怎麼執行的?

執行機制——事件循環(Event Loop)

一般所說的 JavaScript Engine (JS引擎)負責執行一個個 chunk (能夠理解爲事件塊)的程序,每一個 chunk 一般是以 function 爲單位,一個 chunk 執行完成後,纔會執行下一個 chunk。下一個 chunk 是什麼呢?取決於當前 Event Loop Queue (事件循環隊列)中的隊首。ajax

一般聽到的JavaScript Engine JavaScript runtime 是什麼?編程

  • Javascript Engine  :Js引擎,負責解釋並編譯代碼,讓它變成能交給機器運行的代碼(runnable commands)
  • Javascript runtime :Js運行環境,主要提供一些對外調用的接口 。好比瀏覽器環境:windowDOM。還有Node.js環境:require 、export

Event Loop Queue (事件循環隊列)中存放的都是消息,每一個消息關聯着一個函數,JavaScript Engine (如下簡稱JS引擎)就按照隊列中的消息順序執行它們,也就是執行 chunksegmentfault

例如數組

setTimeout( function() {
    console.log('timeout')
}, 1000)複製代碼

當JS引擎執行的時候,能夠分爲3步chunkpromise

  1. setTimeout 啓動定時器(1000毫秒)執行
  2. 執行完畢後,獲得機會將 callback 放入 Event Loop Queue
  3. 此 callback 執行

每一步都是一個chunk,能夠發現,第2步,獲得機會很重要,因此說即便延遲1000ms也不必定準的緣由。由於若是有其餘任務在前面,它至少要等其餘消息對應的程序都完成後才能將callback推入隊列,後面咱們會舉個🌰瀏覽器


像這個一個一個執行chunk的過程就叫作Event Loop(事件循環)bash

按照阮老師的說法:網絡

整體角度:主線程執行的時候產生棧(stack)和堆(heap),棧中的代碼負責調用各類API,在任務隊列中加入事件(click,load,done),只要棧中的代碼執行完畢後,就會去讀取任務隊列,依次執行那些事件所對應的回調函數。

執行的機制流程

同步直接進入主線程執行,若是是異步的,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。

咱們都知道,JS引擎 對 JavaScript 程序的執行是單線程的,爲了防止同時去操做一個數據形成衝突或者是沒法判斷,可是 JavaScript Runtime(整個運行環境)並非單線程的;並且幾乎全部的異步任務都是併發的,例如多個 Job QueueAjaxTimerI/O(Node)等等。

而Node.js會略有不一樣,在node.js啓動時,建立了一個相似while(true)的循環體,每次執行一次循環體稱爲一次tick,每一個tick的過程就是查看是否有事件等待處理,若是有,則取出事件極其相關的回調函數並執行,而後執行下一次tick。node的Event Loop和瀏覽器有所不一樣。Event Loop每次輪詢:先執行完主代碼,期中遇到異步代碼會交給對應的隊列,而後先執行完全部nextTick(),而後在執行其它全部微任務。

任務隊列

任務隊列task queue中有微任務隊列宏任務隊列

  • 微任務隊列只有一個
  • 宏任務能夠有若干個

根據目前,咱們先大概畫個草圖


具體部分後面會講,那先說說同步和異步

執行機制——同步任務(synchronous)和異步任務(asynchronous)

事件分爲同步和異步

同步任務

同步任務直接進入主線程進行執行
console.log('1');

var sub = 0;
for(var i = 0;i < 1000000000; i++) {
    sub++
}
console.log(sub);

console.log('2');
.....複製代碼

會點編程的都知道,在打印出sub的值以前,系統是不會打印出2的。按照先進先出的順序執行chunk。

若是是Execution Context Stack(執行上下文堆棧)

function log(str) {
    console.log(str);
}
log('a');複製代碼

從執行順序上,首先log('a')入棧,而後console.log('a')再入棧,執行console.log('a')出棧,log('a')再出棧。

異步任務

異步任務必須指定回調函數,所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務進入 Event Table後,當指定的事情完成了,就將異步任務加入 Event Queue,等待主線程上的任務完成後,就執行Event Queue裏的異步任務,也就是執行對應的回調函數。

指定的事情能夠是setTimeout的time🌰

var value = 1;
setTimeout(function(){
    value = 2;
}, 0)
console.log(value);  // 1

複製代碼

從這個例子很容易理解,即便設置時間再短,setTimeout仍是要等主線程執行完再執行,致使引用仍是最初的value

🌰

console.log('task1');

setTimeout(()=>{ console.log('task2') },0);

var sub = 0;
for(var i = 0;i < 1000000000;i++) {
    sub++
}
console.log(sub);
console.log('task3');複製代碼


分析一下

  • task1進入主線程當即執行
  • task2進入Event Table,註冊完事件setTimeout後進入Event Queue,等待主線程執行完畢
  • sub賦值後進入for循環自增,主線程一直被佔用
  • 計算完畢後打印出sub,主線程繼續chunk
  • task3進入主線程當即執行
  • 主線程隊列已清空,到Event Queue中執行任務,打印task2

無論for循環計算多久,只要主線程一直被佔用,就不會執行Event Queue隊列裏的任務。除非主線任務執行完畢。全部咱們一般說的setTimeouttime是不標準的,準確的說,應該是大於等於這個time

來個🌰體驗一下結果

var sub = 0;
(function setTime(){
	let start = (new Date()).valueOf();//開始時間
	console.log('執行開始',start)
	setTimeout(()=>{ 
	   console.log('定時器結束',sub,(new Date()).valueOf()-start);//計算差別
	},0);
})();
for(var i = 0;i < 1000000000;i++) {
    sub++
}
console.log('執行結束')複製代碼

實際上,延遲會遠遠大於預期,達到了3004毫秒


最後的計算結果是根據瀏覽器的運行速度和電腦配置差別而定,這也是setTimeout最容易被坑的一點。

AJAX怎麼算

那ajax怎麼算,做爲平常使用最多的一種異步,咱們必須搞清楚它的運行機制。

console.log('start');

$.ajax({
    url:'xxx.com?user=123',
    success:function(res){
        console.log('success')
    }
})
setTimeout(() => {
    console.log('timeout')
},100);

console.log('end');複製代碼

答案是不願定的,多是

start
end
timeout
success複製代碼

也有多是

start
end
success
timeout複製代碼

前兩步沒有疑問,都是做爲同步函數執行,問題緣由出在ajax身上

前面咱們說過,異步任務必須有callback,ajax的callbacksuccess(),也就是隻有當請求成功後,觸發了對應的callback success()纔會被放入任務隊列(Event Queue)等待主線程執行。而在請求結果返回的期間,後者的setTimeout頗有可能已經達到了指定的條件(執行100毫秒延時完畢)將它的回調函數放入了任務隊列等主線程執行。這時候可能ajax結果仍未返回...

Promise的執行機制

再加點料

console.log('執行開始');

setTimeout(() => {
 console.log('timeout') 
}, 0);

new Promise(function(resolve) {
    console.log('進入')
    resolve();
}).then(res => console.log('Promise執行完畢') )

console.log('執行結束');複製代碼

先別繼續往下看,假設你是瀏覽器,你會怎麼運行,自我思考十秒鐘


這裏要注意,嚴格的來講,Promise 屬於 Job Queue,只有then纔是異步。

Job Queue是什麼

Job Queue是ES6新增的概念。

Job Queue和Event Loop Queue有什麼區別?

  • JavaScript runtime(JS運行環境)能夠有多個Job Queue,可是隻能有一個Event Loop Queue。
  • JS引擎將當前chunk執行完會優先執行全部Job Queue,再去執行Event Loop Queue。
Promise 中的一個個 then 就是一種 Job Queue

分析流程:

  1. 遇到同步任務,進入主線程直接執行,打印出"執行開始"
  2. 遇到setTimeout異步任務放入Event Table執行,知足條件後放入Event Queue的宏任務隊列等待主線程執行
  3. 執行Promise,放入Job Queue優先執行,執行同步任務打印出"進入"
  4. 返回resolve()觸發then回調函數,放入Event Queue微任務隊列等待主線程執行
  5. 執行同步任務打印出"執行結束"
  6. 主線程清空,到Event Queue微任務隊列取出任務開始執行。打印出"Promise執行完畢"
  7. 微任務隊列清空,到宏任務隊列取出任務執行,打印出"timeout"

🌰 plus 

console.log("start");

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

new Promise((resolve) => {
  resolve();
})
.then(() => {
  return console.log("A1");
})
.then(() => {
  return console.log("A2");
});

new Promise((resolve) => {
  resolve();
})
.then(() => {
  return console.log("B1");
})
.then(() => {
  return console.log("B2");
})
.then(() => {
  return console.log("B3");
});

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

打印結果


運用剛剛說說的,分析一遍

  • setTimeout異步任務,到Event Table執行完畢後將callback放入Event Queue宏任務隊列等待主線程執行
  • Promise 放入Job Queue優先進入主線程執行,返回resolve(),觸發A1 then回調函數放入微任務隊列中等待主線程執行
  • 到第二個Promise,同上,放入Job Queue執行,將B1 then回調函數放入微任務隊列
  • 執行同步函數,直接進入主線程執行,打印出"end"
  • 無同步任務,開始從task Queue 也就是 Event Queue裏取出異步任務開始執行
  • 首先取出隊首的A1 then()回調函數開始執行,打印出"A1",返回promise觸發A2 then()回調函數,添加到微任務隊首。此時隊首是B1 then()
  • 從微任務隊首取出B1 then回調函數,開始執行,返回promise觸發B2 then()回調函數,添加到微任務隊首,此時隊首是A2 then(),再取出A2 then()執行,此次沒有回調
  • 繼續到微任務隊首拿回調執行,重複輪詢打印出B2B3
  • 微任務執行完畢,到宏任務隊首取出setTimeout的回調函數放入主線程執行,打印出"setTimeout"

這樣的話,Promise應該是搞懂了,可是微任務和宏任務?不少人對這個可能有點陌生,可是看完這個應該對這二者區別有所瞭解

異步任務分爲宏任務和微任務

宏任務(macrotasks): setTimeout, setInterval, setImmediate(node.js), I/O, UI rendering
微任務(microtasks):process.nextTick(node.js), Promises, Object.observe, MutationObserver

先看一下具備特殊性的API:

process.nextTick

node方法,process.nextTick能夠把當前任務添加到執行棧的尾部,也就是在下一次Event Loop(主線程讀取"任務隊列")以前執行。也就是說,它指定的任務必定會發生在全部異步任務以前。和setTimeout(fn,0)很像。

process.nextTick(callback)
複製代碼

setImmediate

Node.js0.8之前是沒有setImmediate的,在當前"任務隊列"的尾部添加事件,官方稱setImmediate指定的回調函數,相似於setTimeout(callback,0),會將事件放到下一個事件循環中,因此也會比nextTick慢執行,有一點——須要瞭解setImmediatenextTick的區別。nextTick雖然異步執行,可是不會給其餘io事件執行的任何機會,而setImmediate是執行於下一個event loop。總之process.nextTick()的優先級高於setImmediate

setImmediate(callback)複製代碼

MutationObserver

必定發生在setTimeout以前,你能夠把它當作是setImmediateMutationObserver是一個構造器,接受一個callback參數,用來處理節點變化的回調函數,返回兩個參數

  • mutations:節點變化記錄列表(sequence<MutationRecord>)
  • observer:構造MutationObserver對象。
var observe = new MutationObserver(function(mutations,observer){
        // code...
})複製代碼

在這不說過多,能夠去了解下具體用法

Object.observe

Object.observe方法用於爲對象指定監視到屬性修改時調用的回調函數

Object.observe(obj, function(changes){
   changes.forEach(function(change) {
        console.log(change,change.oldValue);
    });
});複製代碼
什麼狀況下才會觸發?
  • 原始JavaScript對象中的變化
  • 當屬性被添加、改變、或者刪除時的變化
  • 當數組中的元素被添加或者刪除時的變化
  • 對象的原型發生的變化

來個大🌰

總結:

任務優先級

同步任務 >>>  process.nextTick >>> 微任務(ajax/callback) >>> setTimeout = 宏任務 ??? setImmediate

setImmediate是要等待下一次事件輪詢,也就是本次結束後執行,因此須要畫???

沒有把Promise的Job Queue放進去是由於能夠當成同步任務來進行處理。要明確的一點是,它是嚴格按照這個順序去執行的,每次執行都會把以上的流程走一遍,都會再次輪詢走一遍,而後把處理對應的規則。

拿個別人的🌰加點料,略微作一下修改,給你們分析一下

console.log('1');

setTimeout(function() {
    console.log('2');

    process.nextTick(function() {
        console.log('3');
    })

    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
}, 1000); //添加了1000ms
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

setImmediate(function(){//添加setImmediate函數
    console.log('13')
})複製代碼

第一遍Event Loop 

  • 走到1的時候,同步任務直接打印
  • 遇到setTimeout,進入task 執行1000ms延遲,此時未達到,無論它,繼續往下走。
  • 遇到process.nextTick,放入執行棧隊尾(將於異步任務執行前執行)。
  • 遇到Promise 放入 Job Queue,JS引擎當前無chunk,直接進入主線程執行,打印出7
  • 觸發resolve(),將then 8 放入微任務隊列等待主線程執行,繼續往下走
  • 遇到setTimeout,執行完畢,將setTimeout 9 的 callback 其放入宏任務隊列
  • 遇到setImmediate,將其callback放入Event Table,等待下一輪Event Loop執行

第一遍完畢  17

當前隊列 


Number two  Ready Go!

  • 無同步任務,準備執行異步任務,JS引擎一看:"嘿!好傢伙,還有個process",而後取出process.nextTick的回調函數執行,打印出6
  • 再繼續去微任務隊首取出then 8,打印出8
  • 微任務隊列清空了,就到宏任務隊列取出setTimeout 9 callback執行,打印出9
  • 繼續往下執行,又遇到process.nextTick 10,放入Event Queue等待執行
  • 遇到Promise ,將callback 放入 Job Queue,當前無chunk,執行打印出 11
  • 觸發resolve(),添加回調函數then 12,放入微任務隊列

本次Event Loop尚未結束,同步任務執行完畢,目前任務隊列


  • 再取出process.nextTick 10,打印出10
  • 去微任務隊列,取出then 12 執行,打印出12
  • 本次Event Loop輪詢結束 ,取出setImmediate打印出13

第二遍輪詢完畢,打印出了 68911101213

當前沒有任務了,過了大概1000ms,以前的setTimeout 延遲執行完畢了,放入宏任務

  • setTimeout進入主線程開始執行。
  • 遇到同步任務,直接執行,打印出2
  • 遇到process.nextTick,callback放入Event Queue,等待同步任務執行完畢
  • 遇到Promise,callback放入Job Queue,當前無chunk,進入主線程執行,打印出4
  • 觸發resolve(), 將then 5放入微任務隊列

同步執行完畢,先看下目前的隊列


剩下的就很輕鬆了

  • 取出process.nextTick 3 callback執行,打印出3
  • 取出微任務 then 5,打印出 5
  • over

整體打印順序

1
7
6
8
9
11
10
12
13
2
4
3
5複製代碼

emmm...可能須要多看幾遍消化一下。

Web Worker

如今有了Web Worker,它是一個獨立的線程,可是仍未改變原有的單線程,Web Worker只是個額外的線程,有本身的內存空間(棧、堆)以及 Event Loop Queue。要與這樣的不一樣的線程通訊,只能經過 postMessage。一次 postMessage 就是在另外一個線程的 Event Loop Queue 中加入一條消息。說到postMessage可能有些人會聯想到Service Work,可是他們是兩個大相徑庭

Web Worker和Service Worker的區別

Service Worker:
處理網絡請求的後臺服務。完美的離線狀況下後臺同步或推送通知的處理方案。不能直接與DOM交互。通訊(頁面和Service Worker之間)得經過postMessage方法 ,有另外一篇文章是關於本地儲存,其中運用到頁面離線訪問Service Work of  Google PWA,有興趣的能夠看下

Web Worker:
模仿多線程,容許複雜的腳本在後臺運行,因此它們不會阻止其餘腳本的運行。是保持您的UI響應的同時也執行處理器密集型功能的完美解決方案。不能直接與DOM交互。通訊必須經過postMessage方法

若是意猶未盡能夠嘗試去深刻Promise另外一篇文章——一次性讓你懂async/await,解決回調地獄

相關文章
相關標籤/搜索