# 事件/異步
* [`[Basic]` Promise](sections/event-async.md#promise)
* [`[Doc]` Events (事件)](sections/event-async.md#events)
* [`[Doc]` Timers (定時器)](sections/event-async.md#timers)
* [`[Point]` 阻塞/異步](sections/event-async.md#阻塞異步)
* [`[Point]` 并行/并發](sections/event-async.md#并行并發)
## 簡述
異步還是不異步? 這是一個問題.
## Promise

相信很多同學在面試的時候都碰到過這樣一個問題, `如何處理 Callback Hell`. 在早些年的時候, 大家會看到有很多的解決方案例如 [Q](https://www.npmjs.com/package/q), [async](https://www.npmjs.com/package/async), [EventProxy](https://www.npmjs.com/package/eventproxy) 等等. 最后從流行程度來看 `Promise` 當之無愧的獨領風騷, 并且是在 ES6 的 Javascript 標準上贏得了支持.
關于它的基礎知識/概念推薦看阮一峰的 [Promise 對象](http://javascript.ruanyifeng.com/advanced/promise.html#toc9) 這里就不多不贅述.
> <a name="q-1"></a> Promise 中 .then 的第二參數與 .catch 有什么區別?
參見 [We have a problem with promises](https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html)
另外關于同步與異步, 有個問題希望大家看一下, 這是很簡單的 Promise 的使用例子:
```javascript
let doSth = new Promise((resolve, reject) => {
console.log('hello');
resolve();
});
doSth.then(() => {
console.log('over');
});
```
毫無疑問的可以得到一下輸出結果:
```
hello
over
```
但是首先的問題是, 該 Promise 封裝的代碼肯定是同步的, 那么這個 then 的執行是異步的嗎?
其次的問題是, 如下代碼, `setTimeout` 到 10s 之后再 `.then` 調用, 那么 `hello` 是會在 10s 之后在打印嗎, 還是一開始就打印?
```javascript
let doSth = new Promise((resolve, reject) => {
console.log('hello');
resolve();
});
setTimeout(() => {
doSth.then(() => {
console.log('over');
})
}, 10000);
```
以及理解如下代碼的執行順序 ([出處](https://zhuanlan.zhihu.com/p/25407758)):
```javascript
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
```
如果你不了解這些問題, 可以自己在本地嘗試研究一下打印的結果. 這里希望你掌握的是 Promise 的狀態轉換, 包括異步與 Promise 的關系, 以及 Promise 如何幫助你處理異步, 如果你研究過 Promise 的實現那就更好了.
## Events
`Events` 是 Node.js 中一個非常重要的 core 模塊, 在 node 中有許多重要的 core API 都是依賴其建立的. 比如 `Stream` 是基于 `Events` 實現的, 而 `fs`, `net`, `http` 等模塊都依賴 `Stream`, 所以 `Events` 模塊的重要性可見一斑.
通過繼承 EventEmitter 來使得一個類具有 node 提供的基本的 event 方法, 這樣的對象可以稱作 emitter, 而觸發(emit)事件的 cb 則稱作 listener. 與前端 DOM 樹上的事件并不相同, emitter 的觸發不存在冒泡, 逐層捕獲等事件行為, 也沒有處理事件傳遞的方法.
> <a name="q-2"></a> Eventemitter 的 emit 是同步還是異步?
Node.js 中 Eventemitter 的 emit 是同步的. 在官方文檔中有說明:
> The EventListener calls all listeners synchronously in the order in which they were registered. This is important to ensure the proper sequencing of events and to avoid race conditions or logic errors.
另外, 可以討論如下的執行結果是輸出 `hi 1` 還是 `hi 2`?
```javascript
const EventEmitter = require('events');
let emitter = new EventEmitter();
emitter.on('myEvent', () => {
console.log('hi 1');
});
emitter.on('myEvent', () => {
console.log('hi 2');
});
emitter.emit('myEvent');
```
或者如下情況是否會死循環?
```javascript
const EventEmitter = require('events');
let emitter = new EventEmitter();
emitter.on('myEvent', () => {
console.log('hi');
emitter.emit('myEvent');
});
emitter.emit('myEvent');
```
以及這樣會不會死循環?
```javascript
const EventEmitter = require('events');
let emitter = new EventEmitter();
emitter.on('myEvent', function sth () {
emitter.on('myEvent', sth);
console.log('hi');
});
emitter.emit('myEvent');
```
使用 emitter 處理問題可以處理比較復雜的狀態場景, 比如 TCP 的復雜狀態機, 做多項異步操作的時候每一步都可能報錯, 這個時候 .emit 錯誤并且執行某些 .once 的操作可以將你從泥沼中拯救出來.
另外可以注意一下的是, 有些同學喜歡用 emitter 來監控某些類的狀態, 但是在這些類釋放的時候可能會忘記釋放 emitter, 而這些類的內部可能持有該 emitter 的 listener 的引用從而導致內存泄漏.
## 阻塞/異步
> <a name="q-3"></a> 如何判斷接口是否異步? 是否只要有回調函數就是異步?
開放性問題, 每個寫 node 的人都有一套自己的判斷方式.
* 看文檔
* console.log 打印看看
* 看是否有 IO 操作
單純使用回調函數并不會異步, IO 操作才可能會異步, 除此之外還有使用 setTimeout 等方式實現異步.
> 有這樣一個場景, 你在線上使用 koa 搭建了一個網站, 這個網站項目中有一個你同事寫的接口 A, 而 A 接口中在特殊情況下會變成死循環. 那么首先問題是, 如果觸發了這個死循環, 會對網站造成什么影響?
Node.js 中執行 js 代碼的過程是單線程的. 只有當前代碼都執行完, 才會切入事件循環, 然后從事件隊列中 pop 出下一個回調函數開始執行代碼. 所以 ① 實現一個 sleep 函數, 只要通過一個死循環就可以阻塞整個 js 的執行流程. (關于如何避免坑爹的同事寫出死循環, 在后面的測試環節有寫到.)
> <a name="q-5"></a> 如何實現一個 sleep 函數? ①
```javascript
function sleep(ms) {
var start = Date.now(), expire = start + ms;
while (Date.now() < expire) ;
return;
}
```
而異步, 是使用 libuv 來實現的 (C/C++的同學可以參見 libev 和 libevent) 另一個線程里的事件隊列.
如果在線上的網站中出現了死循環的邏輯被觸發, 整個進程就會一直卡在死循環中, 如果沒有多進程部署的話, 之后的網站請求全部會超時, js 代碼沒有結束那么事件隊列就會停下等待不會執行異步, 整個網站無法響應.
> <a name="q-6"></a> 如何實現一個異步的 reduce? (注:不是異步完了之后同步 reduce)
需要了解 reduce 的情況, 是第 n 個與 n+1 的結果異步處理完之后, 在用新的結果與第 n+2 個元素繼續依次異步下去. 不貼答案, 期待諸君的版本.
## Timers
在筆者這里將 Node.js 中的異步簡單的劃分為兩種, 硬異步和軟異步.
硬異步是指由于 IO 操作或者外部調用走 libuv 而需要異步的情況. 當然, 也存在 readFileSync, execSync 等例外情況, 不過 node 由于是單線程的, 所以如果常規業務在普通時段執行可能比較耗時同步的 IO 操作會使得其執行過程中其他的所有操作都不能響應, 有點作死的感覺. 不過在啟動/初始化以及一些工具腳本的應用場景下是完全沒問題的. 而一般的場景下 IO 操作都是需要異步的.
軟異步是指, 通過 setTimeout 等方式來實現的異步. <a name="q-4"></a> 關于 nextTick, setTimeout 以及 setImmediate 三者的區別參見[該帖](https://cnodejs.org/topic/5556efce7cabb7b45ee6bcac)
**Event loop 示例**
```
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
```
關于事件循環, Timers 以及 nextTick 的關系詳見官方文檔 [The Node.js Event Loop, Timers, and process.nextTick() (英文)](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) 以及阮一峰的 [JavaScript 運行機制詳解:再談Event Loop (中文)](http://www.ruanyifeng.com/blog/2014/10/event-loop.html) 等.
## 并行/并發
并行 (Parallel) 與并發 (Concurrent) 是兩個很常見的概念.
可以看 Erlang 作者 Joe Armstrong 的博客 ([Concurrent and Parallel](http://joearms.github.io/2013/04/05/concurrent-and-parallel-programming.html))

并發 (Concurrent) = 2 隊列對應 1 咖啡機.
并行 (Parallel) = 2 隊列對應 2 咖啡機.
Node.js 通過事件循環來挨個抽取實踐隊列中的一個個 Task 執行, 從而避免了傳統的多線程情況下 `2個隊列對應 1個咖啡機` 的時候上線文切換以及資源爭搶/同步的問題, 所以獲得了高并發的成就.
至于在 node 中并行, 你可以通過 cluster 來再添加一個咖啡機.