我的博客html
看了不少js執行機制的文章彷佛都是似懂非懂,到技術面問的時候,理不清思緒。總結了衆多文章的例子和精華,但願能幫到大家node
一般所說的 JavaScript Engine
(JS引擎)負責執行一個個 chunk
(能夠理解爲事件塊
)的程序,每一個 chunk
一般是以 function
爲單位,一個 chunk
執行完成後,纔會執行下一個 chunk
。下一個 chunk
是什麼呢?取決於當前 Event Loop Queue
(事件循環隊列)中的隊首。ajax
一般聽到的JavaScript Engine
和JavaScript runtime
是什麼?編程
window
、 DOM
。還有Node.js環境:require
、export
Event Loop Queue
(事件循環隊列)中存放的都是消息,每一個消息關聯着一個函數,JavaScript Engine
(如下簡稱JS引擎)就按照隊列中的消息順序執行它們,也就是執行 chunk
。segmentfault
例如數組
setTimeout( function() {
console.log('timeout')
}, 1000)複製代碼
當JS引擎執行的時候,能夠分爲3步chunkpromise
setTimeout
啓動定時器(1000毫秒)執行callback
放入 Event Loop Queue
每一步都是一個chunk
,能夠發現,第2步,獲得機會很重要,因此說即便延遲1000ms也不必定準的緣由。由於若是有其餘任務在前面,它至少要等其餘消息對應的程序都完成後才能將callback
推入隊列,後面咱們會舉個🌰瀏覽器
像這個一個一個執行chunk
的過程就叫作Event Loop(事件循環)
。bash
按照阮老師的說法:網絡
整體角度:主線程執行的時候產生棧(stack)和堆(heap),棧中的代碼負責調用各類API,在任務隊列中加入事件(click,load,done),只要棧中的代碼執行完畢後,就會去讀取任務隊列,依次執行那些事件所對應的回調函數。
執行的機制流程
同步直接進入主線程執行,若是是異步的,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。
咱們都知道,JS引擎 對 JavaScript
程序的執行是單線程的,爲了防止同時去操做一個數據形成衝突或者是沒法判斷,可是 JavaScript Runtime
(整個運行環境)並非單線程的;並且幾乎全部的異步任務都是併發的,例如多個 Job Queue
、Ajax
、Timer
、I/O(Node)
等等。
而Node.js會略有不一樣,在node.js
啓動時,建立了一個相似while(true)
的循環體,每次執行一次循環體稱爲一次tick
,每一個tick
的過程就是查看是否有事件等待處理,若是有,則取出事件極其相關的回調函數並執行,而後執行下一次tick
。node的Event Loop
和瀏覽器有所不一樣。Event Loop
每次輪詢:先執行完主代碼,期中遇到異步代碼會交給對應的隊列,而後先執行完全部nextTick(),而後在執行其它全部微任務。
任務隊列task queue
中有微任務隊列
和宏任務隊列
根據目前,咱們先大概畫個草圖
具體部分後面會講,那先說說同步和異步
事件分爲同步和異步
同步任務
同步任務直接進入主線程進行執行
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');複製代碼
分析一下
Event Table
,註冊完事件setTimeout
後進入Event Queue
,等待主線程執行完畢無論for循環計算多久,只要主線程一直被佔用,就不會執行Event Queue
隊列裏的任務。除非主線任務執行完畢。全部咱們一般說的setTimeout
的time
是不標準的,準確的說,應該是大於等於這個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怎麼算,做爲平常使用最多的一種異步,咱們必須搞清楚它的運行機制。
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的callback
是success()
,也就是隻有當請求成功後,觸發了對應的callback success()
纔會被放入任務隊列(Event Queue)等待主線程執行。而在請求結果返回的期間,後者的setTimeout
頗有可能已經達到了指定的條件(執行100毫秒延時完畢
)將它的回調函數放入了任務隊列等主線程執行。這時候可能ajax結果仍未返回...
再加點料
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是ES6新增的概念。
Job Queue和Event Loop Queue有什麼區別?
then
就是一種
Job Queue
。
分析流程:
"執行開始"
setTimeout
異步任務放入Event Table執行,知足條件後放入Event Queue的宏任務隊列等待主線程執行Promise
,放入Job Queue
優先執行,執行同步任務打印出"進入"
resolve()
觸發then回調函數,放入Event Queue微任務隊列
等待主線程執行"執行結束"
Event Queue
的微任務隊列
取出任務開始執行。打印出"Promise執行完畢"
"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");
複製代碼
打印結果
運用剛剛說說的,分析一遍
resolve()
,觸發A1 then
回調函數放入微任務隊列中等待主線程執行B1 then
回調函數放入微任務隊列"end"
A1 then()
回調函數開始執行,打印出"A1"
,返回promise
觸發A2 then()
回調函數,添加到微任務隊首。此時隊首是B1 then()
B1 then
回調函數,開始執行,返回promise觸發B2 then()
回調函數,添加到微任務隊首,此時隊首是A2 then()
,再取出A2 then()
執行,此次沒有回調B2
和B3
。setTimeout
的回調函數放入主線程執行,打印出"setTimeout"
。這樣的話,Promise應該是搞懂了,可是微任務和宏任務?不少人對這個可能有點陌生,可是看完這個應該對這二者區別有所瞭解
宏任務(macrotasks): setTimeout, setInterval, setImmediate(node.js), I/O, UI rendering
微任務(microtasks):process.nextTick(node.js), Promises, Object.observe, MutationObserver
先看一下具備特殊性的API:
node方法,process.nextTick
能夠把當前任務添加到執行棧的尾部,也就是在下一次Event Loop(主線程讀取"任務隊列")以前執行。也就是說,它指定的任務必定會發生在全部異步任務以前。和setTimeout(fn,0)
很像。
process.nextTick(callback)
複製代碼
Node.js0.8之前是沒有setImmediate的,在當前"任務隊列"的尾部添加事件,官方稱setImmediate
指定的回調函數,相似於setTimeout(callback,0)
,會將事件放到下一個事件循環中,因此也會比nextTick
慢執行,有一點——須要瞭解setImmediate
和nextTick
的區別。nextTick
雖然異步執行,可是不會給其餘io事件執行的任何機會,而setImmediate
是執行於下一個event loop
。總之process.nextTick()
的優先級高於setImmediate
setImmediate(callback)複製代碼
必定發生在setTimeout
以前,你能夠把它當作是setImmediate
。MutationObserver
是一個構造器,接受一個callback
參數,用來處理節點變化的回調函數,返回兩個參數
var observe = new MutationObserver(function(mutations,observer){
// code...
})複製代碼
在這不說過多,能夠去了解下具體用法
Object.observe方法用於爲對象指定監視到屬性修改時調用的回調函數
Object.observe(obj, function(changes){
changes.forEach(function(change) {
console.log(change,change.oldValue);
});
});複製代碼
什麼狀況下才會觸發?
來個大🌰
任務優先級
同步任務
>>> 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執行第一遍完畢 1
、7
當前隊列
Number two Ready Go!
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
setImmediate
打印出13
。第二遍輪詢完畢,打印出了 6
、8
、9
、11
、10
、12
、13
當前沒有任務了,過了大概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
整體打印順序
1
7
6
8
9
11
10
12
13
2
4
3
5複製代碼
emmm...可能須要多看幾遍消化一下。
如今有了Web Worker
,它是一個獨立的線程,可是仍未改變原有的單線程,Web Worker
只是個額外的線程,有本身的內存空間(棧、堆)以及 Event Loop Queue
。要與這樣的不一樣的線程通訊,只能經過 postMessage
。一次 postMessage
就是在另外一個線程的 Event Loop Queue
中加入一條消息。說到postMessage
可能有些人會聯想到Service Work
,可是他們是兩個大相徑庭
Service Worker:
處理網絡請求的後臺服務。完美的離線狀況下後臺同步或推送通知的處理方案。不能直接與DOM交互。通訊(頁面和Service Worker之間)得經過postMessage
方法 ,有另外一篇文章是關於本地儲存,其中運用到頁面離線訪問Service Work of Google PWA,有興趣的能夠看下
Web Worker:
模仿多線程,容許複雜的腳本在後臺運行,因此它們不會阻止其餘腳本的運行。是保持您的UI響應的同時也執行處理器密集型功能的完美解決方案。不能直接與DOM交互。通訊必須經過postMessage
方法
若是意猶未盡能夠嘗試去深刻Promise另外一篇文章——一次性讓你懂async/await,解決回調地獄