- pre-notify
- 術語
- 為什么JS要設計成單線程的?
- JS并非只有一個線程,而只是主線程是單線程的
- 異步任務以及事件循環
- Node.js中のEventLoop
- 微任務和宏任務
- 其它的微任務
- Q
- 1. setTimeout,setImmediate誰先誰后?
- 2. nextTick和promise.then誰快?
- 3. nextTick和其它的定時器嵌套
- 4. 定時器指定的回調函數一定會在指定的時間內執行嗎?
- 關于瀏覽器模型
## pre-notify
本文主要根據網上資源總結而來,如有不對,請斧正。
>參考
- [Node 定時器詳解](http://www.ruanyifeng.com/blog/2018/02/node-event-loop.html)
- [進程與線程的一個簡單解釋](http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html)
- [setImmediate() vs nextTick() vs setTimeout(fn,0) - in depth explanation](http://voidcanvas.com/setimmediate-vs-nexttick-vs-settimeout/)
- [The Node.js Event Loop, Timers, and process.nextTick()](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)
- [不要混淆nodejs和瀏覽器中的event loop](https://cnodejs.org/topic/5a9108d78d6e16e56bb80882)
## 術語
synchronous:同步任務
asynchronous:異步任務
task queue/callback queue:任務隊列
execution context stack:執行棧
heap:堆
stack:棧
macro-task:宏任務
micro-task:微任務
## 為什么JS要設計成單線程的?
在進入正式的主題之前我們來探究探究這個歷史問題,嗯,還是有點價值的哈,因為JS若不是單線程的也就不會衍生出后來的異步任務以及事件環了嘛。
---
js最開始時是跑在瀏覽器端的,主要的作用是與用戶互動(接收用戶輸入并給出自定義的響應)以及操作DOM(各種特效,控制輸入輸出)。
So網上有一種說法,舉了個栗子說,兩個線程,有一個線程在添加一個dom元素 `a`,還有一個線程在刪除一個dom元素`a`,那么瀏覽器就需要決策該聽誰的,這樣的話就增加了語言設計的復雜性。
嗯。。。如果這例子以及說法還不能令你信服,我覺得你可以再想想javascript當初開發時的情景
其實我覺的這也是很重要的一個原因之一,嗯,這是我比較能代入的,那就是
我大javascript當初10天就給弄出來了。。。so,你還想咋地?
## JS并非只有一個線程,而只是主線程是單線程的
h5有一個新api,它叫`webworker`,利用它,能幫助我們創建**子線程**。
```
//index.html
let worker = new Worker('./worker.js');
//把內容發給 工作線程,工作線程可以把結果丟回來
worker.postMessage('向你雇傭的工人發送指令');
worker.onmessage = function(e){
console.log(e.data); //數據在data屬性下
}
//worker.js
window.onmessage = function(e){
console.log(e);
this.postMessage('你的工人告訴你他收到命令開始干活了!');
};
```

---
**注意:**
他和js主線程不是平級的,主線程可以控制webworker,webworker不能操作dom,不能獲取document以及window。
## 異步任務以及事件循環
單線程雖然簡單,不容易出錯,但是有一個問題,這貨一直是一個人在干活,假若涉及到讀取寫入這種I/O行為,那么不僅CPU資源是妥妥的浪費(webworker的出現其實可能是為了嘗試解決這部分問題),我們后面的代碼還要等待它讀寫完畢才能執行,這就是所謂的站著那撒不拉那撒了- -!
So,為了解決這個問題,Javascript將任務的執行方式分為兩種:`同步/synchronous` 和 `異步/asynchronous`,遇到像上面那種需要長時間等待的I/O操作,我們就將它作為一個異步任務分發出去,等待它執行完畢后再通知我們。
**我們說遇到異步任務會分發出去,那么分發分發究竟分發給了誰呢?**
在瀏覽器內核中,除了`JS線程`和`用戶界面后端/UI Backend線程`,還有一些其它的線程,比如說`瀏覽器事件觸發線程`、`定時器觸發線程`、`異步HTTP請求線程`,而異步任務就是分發給這些線程來處理的。(如果有不大了解這方面的同學可以查看本文的最后部分)
當這些異步任務在對應的線程中處理完成得到結果后,這些任務的回調就會被加入到`callback queue`隊列當中。**另一方面**,JS主線程的執行棧中一旦所有同步代碼執行完畢后就會開始不停的檢測`callback queue`,只要隊列中存在任務,就會被提取到執行棧中執行。
下圖是網上流傳很廣的一張示意圖

其中JS主線程從`callback queue`中不斷讀取事件到執行棧中執行的這種循環的**過程**又被稱之為`EventLoop`,即事件循環。
### Node.js中のEventLoop
node中的事件環和瀏覽器中的是不一樣的,node的事件環分為六個階段,每個階段都一有一個`callbcak queue`,只有當一個階段的queue清理干凈后才會進入到下一個階段。

- timer(計時器),執行setTimeout以及setInterval的回調
- I/O callbcacks,處理網絡、流、tcp的callbcak以及錯誤
- idle,prepare node內部使用
- poll(輪詢),會等待I/O執行直到得到cb
- check,處理setImmediate回調
- close callbcaks,處理關閉的回調例如socke.on('close')
### 微任務和宏任務
異步任務主要分為兩種,
一種稱之為`macro task`,即宏任務,像setTimeout、I/O讀寫、AJAX這類耗時灰常長的。
另外一種則稱之為`micro task`,即微任務,例如`nextTick`、`promise`。(即使定時器的delayt時間設置為0,也是宏任務,會在本輪的微任務執行完畢后再執行)
宏任務和微任務的區別在于,微任務是會被加入`本輪循環的`,而宏任務都是在`次輪循環`中被執行。
本輪循環是指什么呢?JS主線程會從任務隊列中提取任務到執行棧中執行,每一次執行都可能會再產生一個新的任務,對于這些任務來說這次執行到下一次從任務隊列中提取新的任務到執行棧之前就是這些新生任務的**本輪**。
>[danger] **注意:** 在node中微任務的觸發時機是在進入事件環之前以及當狀態轉換的時才會觸發,這意味著如果一個狀態中的callbcak queue中的cb還沒有全部清空完畢,那么微任務**并不會**像瀏覽器中一樣加入本輪后在一個回調執行完畢后就會立即執行,而會等待queue清空后才執行。
#### 其它的微任務
- MutationObserve(不兼容的),
- MessageChannel
## Q
>### 1. setTimeout,setImmediate誰先誰后?
```
setTimeout(function(){
console.log('Timeout');
})
setImmediate(function(){
console.log('Immediate');
})
```
網上有有一種說法,因為setTimeout雖然在node事件循環中的第一個階段,但setTimeout即使將`delay`設置為0也會有1ms+(node做不到那么精確),那么當第一次事件循環時,setTimeout可能還沒有準備好,就將會讓setImmediate先執行。
但,真會發生這種情況嗎?
嗯。。。我沒事點了幾十下,全是setTimeout。
另外還有如下一種情況,一定是setImmediate會先走的
```
fs.readFile('./1.txt',function(){
console.log('fs');
setTimeout(function(){
console.log('timeout');
});
setImmediate(function(){
console.log('setImmediate');
});
})
```
因為當fs的I/O回調執行執行時是處于事件循環中的`poll`階段,而下一個階段為`check`是存放`setImmediate`的階段。
>### 2. nextTick和promise.then誰快?
nextTick快,就是這么設計的
>### 3. nextTick和其它的定時器嵌套
```
setImmediate(function(){
console.log(1);
process.nextTick(function(){
console.log(4);
})
})
process.nextTick(function(){
console.log(2);
setImmediate(function(){
console.log(3);
})
})
<<<
2134
```
原因在于nextTick在node中的執行實際和瀏覽器中不完全一樣,雖然它們在第一次進入事件環時都會先執行,但如果后續還有nextTick加入,node中只會在階段轉換時才會去執行,而瀏覽器中則是一有nextTick加入就會立即執行。
造成這樣區別的原因在于,node中的事件環是有6種狀態的,每種狀態都是一個`callbcak queue`,只有當一個狀態的`callback queue`中存放的回調都清空后才會執行nextTick。
>### 4. 定時器指定的回調函數一定會在指定的時間內執行嗎?
不一定,先不說node中事件環六中狀態之間轉化時的貓膩,光是瀏覽器中的事件環也可能因為本輪循環的執行時間過長,長得比定時器指定的事件還長從而導致定時器的回調觸發被延誤。
## 關于瀏覽器模型
嗯,先上圖。。也是很火的一張

瀏覽器是`多進程`的,每個進程管理著瀏覽器不同的部分,主要分為以下幾種
- 用戶界面:包括地址欄、前進/后退按鈕、書簽菜單等
- 瀏覽器引擎:在用戶界面和呈現引擎之間傳送指令
- 呈現引擎,又稱渲染引擎,在線程方面又稱為UI線程,這是最為核心的部分,So也被稱之為瀏覽器內核
- GPU:用于提高網頁瀏覽的體驗
- 插件:一個插件對應一個進程(第三方插件進程)
其中渲染引擎內部有三個`線程`是我們注重需要關注的
- Networking:用于網絡調用,比如HTTP請求
- Javascript解釋器:用于解析和執行Javascript代碼
- UI Backend
其中js線程和ui線程是互斥的,
當js執行的時候可能ui還在渲染,那么這時ui線程會把更改放到隊列中 當js線程空閑下來 ui線程再繼續渲染
除此之外還有一些其它的線程,這也是我們分發異步任務時用到的線程
- 瀏覽器事件觸發線程
- 定時觸發器線程
- 異步HTTP請求線程
---
--- End ---