JavaScript的學習零散而龐雜,所以不少時候咱們學到了一些東西,可是卻沒辦法感覺到本身的進步,甚至過了不久,就把學到的東西給忘了。爲了解決本身的這個困擾,在學習的過程當中,我一直試圖在尋找一條核心的線索,只要我根據這條線索,我就可以一點一點的進步。html
前端基礎進階正是圍繞這條線索慢慢展開,而事件循環機制(Event Loop),則是這條線索的最關鍵的知識點。因此,我就快馬加鞭的去深刻的學習了事件循環機制,並總結出了這篇文章跟你們分享。前端
事件循環機制從總體上的告訴了咱們所寫的JavaScript代碼的執行順序。可是在我學習的過程當中,找到的許多國內博客文章對於它的講解淺嘗輒止,不得其法,不少文章在圖中畫個圈就表示循環了,看了以後也沒感受明白了多少。可是他又如此重要,以至於當咱們想要面試中高級崗位時,事件循環機制老是繞不開的話題。特別是ES6中正式加入了Promise對象以後,對於新標準中事件循環機制的理解就變得更加劇要。這就很尷尬了。html5
最近有兩篇比較火的文章也表達了這個問題的重要性。node
這個前端面試在搞事
80% 應聘者都不及格的 JS 面試題web可是很遺憾的是,大神們告訴了你們這個知識點很重要,卻並無告訴你們爲何會這樣。因此當咱們在面試時遇到這樣的問題時,就算你知道告終果,面試官再進一步問一下,咱們依然懵逼。面試
在學習事件循環機制以前,我默認你已經懂得了以下概念,若是仍然有疑問,能夠回過頭去看看我之前的文章。設計模式
執行上下文(Execution context)api
隊列數據結構(queue)
Promise(我會在下一篇文章專門總結Promise的詳細使用與自定義封裝)
由於chrome瀏覽器中新標準中的事件循環機制與nodejs幾乎同樣,所以此處就以整合nodejs一塊兒來理解,其中會介紹到幾個nodejs有,可是瀏覽器中沒有的API,你們只須要了解就好,不必定非要知道她是如何使用。好比process.nextTick,setImmediate
OK,那我就先拋出結論,而後以例子與圖示詳細給你們演示事件循環機制。
咱們知道JavaScript的一大特色就是單線程,而這個線程中擁有惟一的一個事件循環。
固然新標準中的web worker涉及到了多線程,我對它瞭解也很少,這裏就不討論了。
JavaScript代碼的執行過程當中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列(task queue)來搞定另一些代碼的執行。
一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。
任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。
macro-task大概包括:script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
setTimeout/Promise等咱們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。
1
2
3
4
|
// setTimeout中的回調函數纔是進入任務隊列的任務
setTimeout(
function
() {
console.log(
'xxxx'
);
})
|
來自不一樣任務源的任務會進入到不一樣的任務隊列。其中setTimeout與setInterval是同源的。
事件循環的順序,決定了JavaScript代碼的執行順序。它從script(總體代碼)開始第一次循環。以後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),而後執行全部的micro-task。當全部可執行的micro-task執行完畢以後。循環再次從macro-task開始,找到其中一個任務隊列執行完畢,而後再執行全部的micro-task,這樣一直循環下去。
其中每個任務的執行,不管是macro-task仍是micro-task,都是藉助函數調用棧來完成。
純文字表述確實有點乾澀,所以,這裏咱們經過2個例子,來逐步理解事件循環的具體順序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// demo01 出自於上面我引用文章的一個例子,咱們來根據上面的結論,一步一步分析具體的執行過程。
// 爲了方便理解,我以打印出來的字符做爲當前的任務名稱
setTimeout(
function
() {
console.log(
'timeout1'
);
})
new
Promise(
function
(resolve) {
console.log(
'promise1'
);
for
(
var
i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log(
'promise2'
);
}).then(
function
() {
console.log(
'then1'
);
})
console.log(
'global1'
);
|
首先,事件循環從宏任務隊列開始,這個時候,宏任務隊列中,只有一個script(總體代碼)任務。每個任務的執行順序,都依靠函數調用棧來搞定,而當遇到任務源時,則會先分發任務到對應的隊列中去,因此,上面例子的第一步執行以下圖所示。
第二步:script任務執行時首先遇到了setTimeout,setTimeout爲一個宏任務源,那麼他的做用就是將任務分發到它對應的隊列中。
1
2
3
|
setTimeout(
function
() {
console.log(
'timeout1'
);
})
|
第三步:script執行時遇到Promise實例。Promise構造函數中的第一個參數,是在new的時候執行,所以不會進入任何其餘的隊列,而是直接在當前任務直接執行了,然後續的.then則會被分發到micro-task的Promise隊列中去。
所以,構造函數執行時,裏面的參數進入函數調用棧執行。for循環不會進入任何隊列,所以代碼會依次執行,因此這裏的promise1和promise2會依次輸出。
script任務繼續往下執行,最後只有一句輸出了globa1,而後,全局任務就執行完畢了。
第四步:第一個宏任務script執行完畢以後,就開始執行全部的可執行的微任務。這個時候,微任務中,只有Promise隊列中的一個任務then1,所以直接執行就好了,執行結果輸出then1,固然,他的執行,也是進入函數調用棧中執行的。
第五步:當全部的micro-tast執行完畢以後,表示第一輪的循環就結束了。這個時候就得開始第二輪的循環。第二輪循環仍然從宏任務macro-task開始。
這個時候,咱們發現宏任務中,只有在setTimeout隊列中還要一個timeout1的任務等待執行。所以就直接執行便可。
這個時候宏任務隊列與微任務隊列中都沒有任務了,因此代碼就不會再輸出其餘東西了。
那麼上面這個例子的輸出結果就顯而易見。你們能夠自行嘗試體會。
這個例子比較簡答,涉及到的隊列任務並很少,所以讀懂了它還不能全面的瞭解到事件循環機制的全貌。因此我下面弄了一個複製一點的例子,再給你們解析一番,相信讀懂以後,事件循環這個問題,再面試中再次被問到就難不倒你們了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
// demo02
console.log(
'golb1'
);
setTimeout(
function
() {
console.log(
'timeout1'
);
process.nextTick(
function
() {
console.log(
'timeout1_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'timeout1_promise'
);
resolve();
}).then(
function
() {
console.log(
'timeout1_then'
)
})
})
setImmediate(
function
() {
console.log(
'immediate1'
);
process.nextTick(
function
() {
console.log(
'immediate1_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'immediate1_promise'
);
resolve();
}).then(
function
() {
console.log(
'immediate1_then'
)
})
})
process.nextTick(
function
() {
console.log(
'glob1_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'glob1_promise'
);
resolve();
}).then(
function
() {
console.log(
'glob1_then'
)
})
setTimeout(
function
() {
console.log(
'timeout2'
);
process.nextTick(
function
() {
console.log(
'timeout2_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'timeout2_promise'
);
resolve();
}).then(
function
() {
console.log(
'timeout2_then'
)
})
})
process.nextTick(
function
() {
console.log(
'glob2_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'glob2_promise'
);
resolve();
}).then(
function
() {
console.log(
'glob2_then'
)
})
setImmediate(
function
() {
console.log(
'immediate2'
);
process.nextTick(
function
() {
console.log(
'immediate2_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'immediate2_promise'
);
resolve();
}).then(
function
() {
console.log(
'immediate2_then'
)
})
})
|
這個例子看上去有點複雜,亂七八糟的代碼一大堆,不過不用擔憂,咱們一步一步來分析一下。
第一步:宏任務script首先執行。全局入棧。glob1輸出。
第二步,執行過程遇到setTimeout。setTimeout做爲任務分發器,將任務分發到對應的宏任務隊列中。
1
2
3
4
5
6
7
8
9
10
11
12
|
setTimeout(
function
() {
console.log(
'timeout1'
);
process.nextTick(
function
() {
console.log(
'timeout1_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'timeout1_promise'
);
resolve();
}).then(
function
() {
console.log(
'timeout1_then'
)
})
})
|
第三步:執行過程遇到setImmediate。setImmediate也是一個宏任務分發器,將任務分發到對應的任務隊列中。setImmediate的任務隊列會在setTimeout隊列的後面執行。
1
2
3
4
5
6
7
8
9
10
11
12
|
setImmediate(
function
() {
console.log(
'immediate1'
);
process.nextTick(
function
() {
console.log(
'immediate1_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'immediate1_promise'
);
resolve();
}).then(
function
() {
console.log(
'immediate1_then'
)
})
})
|
第四步:執行遇到nextTick,process.nextTick是一個微任務分發器,它會將任務分發到對應的微任務隊列中去。
1
2
3
|
process.nextTick(
function
() {
console.log(
'glob1_nextTick'
);
})
|
第五步:執行遇到Promise。Promise的then方法會將任務分發到對應的微任務隊列中,可是它構造函數中的方法會直接執行。所以,glob1_promise會第二個輸出。
1
2
3
4
5
6
|
new
Promise(
function
(resolve) {
console.log(
'glob1_promise'
);
resolve();
}).then(
function
() {
console.log(
'glob1_then'
)
})
|
第六步:執行遇到第二個setTimeout。
1
2
3
4
5
6
7
8
9
10
11
12
|
setTimeout(
function
() {
console.log(
'timeout2'
);
process.nextTick(
function
() {
console.log(
'timeout2_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'timeout2_promise'
);
resolve();
}).then(
function
() {
console.log(
'timeout2_then'
)
})
})
|
第七步:前後遇到nextTick與Promise
1
2
3
4
5
6
7
8
9
|
process.nextTick(
function
() {
console.log(
'glob2_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'glob2_promise'
);
resolve();
}).then(
function
() {
console.log(
'glob2_then'
)
})
|
第八步:再次遇到setImmediate。
1
2
3
4
5
6
7
8
9
10
11
12
|
setImmediate(
function
() {
console.log(
'immediate2'
);
process.nextTick(
function
() {
console.log(
'immediate2_nextTick'
);
})
new
Promise(
function
(resolve) {
console.log(
'immediate2_promise'
);
resolve();
}).then(
function
() {
console.log(
'immediate2_then'
)
})
})
|
這個時候,script中的代碼就執行完畢了,執行過程當中,遇到不一樣的任務分發器,就將任務分發到各自對應的隊列中去。接下來,將會執行全部的微任務隊列中的任務。
其中,nextTick隊列會比Promie先執行。nextTick中的可執行任務執行完畢以後,纔會開始執行Promise隊列中的任務。
當全部可執行的微任務執行完畢以後,這一輪循環就表示結束了。下一輪循環繼續從宏任務隊列開始執行。
這個時候,script已經執行完畢,因此就從setTimeout隊列開始執行。
setTimeout任務的執行,也依然是藉助函數調用棧來完成,而且遇到任務分發器的時候也會將任務分發到對應的隊列中去。
只有當setTimeout中全部的任務執行完畢以後,纔會再次開始執行微任務隊列。而且清空全部的可執行微任務。
setTiemout隊列產生的微任務執行完畢以後,循環則回過頭來開始執行setImmediate隊列。仍然是先將setImmediate隊列中的任務執行完畢,再執行所產生的微任務。
當setImmediate隊列執行產生的微任務所有執行以後,第二輪循環也就結束了。
你們須要注意這裏的循環結束的時間節點。
當咱們在執行setTimeout任務中遇到setTimeout時,它仍然會將對應的任務分發到setTimeout隊列中去,可是該任務就得等到下一輪事件循環執行了。例子中沒有涉及到這麼複雜的嵌套,你們能夠動手添加或者修改他們的位置來感覺一下循環的變化。
OK,到這裏,事件循環我想我已經表述得很清楚了,能不能理解就看讀者老爺們有沒有耐心了。我估計不少人會理解不了循環結束的節點。
固然,這些順序都是v8的一些實現。咱們也能夠根據上面的規則,來嘗試實現一下事件循環的機制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 用數組模擬一個隊列
var
tasks = [];
// 模擬一個事件分發器
var
addFn1 =
function
(task) {
tasks.push(task);
}
// 執行全部的任務
var
flush
=
function
() {
tasks.map(
function
(task) {
task();
})
}
// 最後利用setTimeout/或者其餘你認爲合適的方式丟入事件循環中
setTimeout(
function
() {
flush
();
})
// 固然,也能夠不用丟進事件循環,而是咱們本身手動在適當的時機去執行對應的某一個方法
var
dispatch =
function
(name) {
tasks.map(
function
(item) {
if
(item.name == name) {
item.handler();
}
})
}
// 固然,咱們把任務丟進去的時候,多保存一個name便可。
// 這時候,task的格式就以下
demoTask = {
name:
'demo'
,
handler:
function
() {}
}
// 因而,一個訂閱-通知的設計模式就這樣輕鬆的被實現了
|
這樣,咱們就模擬了一個任務隊列。咱們還能夠定義另一個隊列,利用上面的各類方式來規定他們的優先級。
所以,在老的瀏覽器沒有支持Promise的時候,就能夠利用setTimeout等方法,來模擬實現Promise,具體如何作到的,下一篇文章咱們慢慢分析。