你是否遭受到這樣的恐嚇?
你是否有過每個表示式前面都console一遍值去找執行順序?
看了很多js執行機制的文章似乎都是似懂非懂,到技術面問的時候,理不清思緒。總結了眾多文章的例子和精華,希望能幫到你們
JavaScript 怎麼執行的?
執行機制——事件迴圈(Event Loop)
通常所說的 JavaScript Engine
(JS引擎)負責執行一個個 chunk
(可以理解為事件塊
)的程式,每個 chunk
通常是以 function
為單位,一個 chunk
執行完成後,才會執行下一個 chunk
。下一個 chunk
是什麼呢?取決於當前 Event Loop Queue
(事件迴圈佇列)中的隊首。
通常聽到的JavaScript Engine
和JavaScript runtime
是什麼?
- Javascript Engine :Js引擎,負責解釋並編譯程式碼,讓它變成能交給機器執行的程式碼(runnable commands)
- Javascript runtime :Js執行環境,主要提供一些對外呼叫的介面 。比如瀏覽器環境:
window
、DOM
。還有Node.js環境:require
、export
Event Loop Queue
(事件迴圈佇列)中存放的都是訊息,每個訊息關聯著一個函式,JavaScript Engine
(以下簡稱JS引擎)就按照佇列中的訊息順序執行它們,也就是執行 chunk
。
例如
setTimeout( function() {
console.log('timeout')
}, 1000)複製程式碼
當JS引擎執行的時候,可以分為3步chunk
- 由
setTimeout
啟動定時器(1000毫秒)執行 - 執行完畢後,得到機會將
callback
放入Event Loop Queue
- 此 callback 執行
每一步都是一個chunk
,可以發現,第2步,得到機會很重要,所以說即使延遲1000ms也不一定準的原因。因為如果有其他任務在前面,它至少要等其他訊息對應的程式都完成後才能將callback
推入佇列,後面我們會舉個?
像這個一個一個執行chunk
的過程就叫做Event Loop(事件迴圈)
。
按照阮老師的說法:
總體角度:主執行緒執行的時候產生棧(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
中有微任務佇列
和巨集任務佇列
- 微任務佇列只有一個
- 巨集任務可以有若干個
根據目前,我們先大概畫個草圖
具體部分後面會講,那先說說同步和非同步
執行機制——同步任務(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
佇列裡的任務。除非主線任務執行完畢。所有我們通常說的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怎麼算
那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結果仍未返回...
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。
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");
複製程式碼
列印結果
運用剛剛說說的,分析一遍
- 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()
執行,這次沒有回撥 - 繼續到微任務隊首拿回撥執行,重複輪詢列印出
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:
process.nextTick
node方法,process.nextTick
可以把當前任務新增到執行棧的尾部,也就是在下一次Event Loop(主執行緒讀取"任務佇列")之前執行。也就是說,它指定的任務一定會發生在所有非同步任務之前。和setTimeout(fn,0)
很像。
process.nextTick(callback)
複製程式碼
setImmediate
Node.js0.8以前是沒有setImmediate的,在當前"任務佇列"的尾部新增事件,官方稱setImmediate
指定的回撥函式,類似於setTimeout(callback,0)
,會將事件放到下一個事件迴圈中,所以也會比nextTick
慢執行,有一點——需要了解setImmediate
和nextTick
的區別。nextTick
雖然非同步執行,但是不會給其他io事件執行的任何機會,而setImmediate
是執行於下一個event loop
。總之process.nextTick()
的優先順序高於setImmediate
setImmediate(callback)複製程式碼
MutationObserver
一定發生在setTimeout
之前,你可以把它看成是setImmediate
。MutationObserver
是一個構造器,接受一個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執行
第一遍完畢 1
、7
當前佇列
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
。
第二遍輪詢完畢,列印出了 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
- 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,解決回撥地獄