事件/异步

    Promise

    相信很多同学在面试的时候都碰到过这样一个问题, 如何处理 Callback Hell. 在早些年的时候, 大家会看到有很多的解决方案例如 Q, , EventProxy 等等. 最后从流行程度来看 Promise 当之无愧的独领风骚, 并且是在 ES6 的 JavaScript 标准上赢得了支持.

    关于它的基础知识/概念推荐看阮一峰的 这里就不多不赘述.

    参见 We have a problem with promises

    另外关于同步与异步, 有个问题希望大家看一下, 这是很简单的 Promise 的使用例子:

    毫无疑问的可以得到以下输出结果:

    1. hello
    2. over

    但是首先的问题是, 该 Promise 封装的代码肯定是同步的, 那么这个 then 的执行是异步的吗?

    其次的问题是, 如下代码, setTimeout 到 10s 之后再 .then 调用, 那么 hello 是会在 10s 之后在打印吗, 还是一开始就打印?

    1. let doSth = new Promise((resolve, reject) => {
    2. console.log('hello');
    3. resolve();
    4. });
    5. setTimeout(() => {
    6. doSth.then(() => {
    7. console.log('over');
    8. })
    9. }, 10000);

    以及理解如下代码的执行顺序 ():

    如果你不了解这些问题, 可以自己在本地尝试研究一下打印的结果. 这里希望你掌握的是 Promise 的状态转换, 包括异步与 Promise 的关系, 以及 Promise 如何帮助你处理异步, 如果你研究过 Promise 的实现那就更好了.

    Events 是 Node.js 中一个非常重要的 core 模块, 在 node 中有许多重要的 core API 都是依赖其建立的. 比如 Stream 是基于 Events 实现的, 而 fs, , http 等模块都依赖 Stream, 所以 Events 模块的重要性可见一斑.

    通过继承 EventEmitter 来使得一个类具有 node 提供的基本的 event 方法, 这样的对象可以称作 emitter, 而触发(emit)事件的 cb 则称作 listener. 与前端 DOM 树上的事件并不相同, emitter 的触发不存在冒泡, 逐层捕获等事件行为, 也没有处理事件传递的方法.

    Node.js 中 Eventemitter 的 emit 是同步的. 在官方文档中有说明:

    另外, 可以讨论如下的执行结果是输出 hi 1 还是 hi 2?

    1. let emitter = new EventEmitter();
    2. emitter.on('myEvent', () => {
    3. console.log('hi 1');
    4. });
    5. emitter.on('myEvent', () => {
    6. console.log('hi 2');
    7. });
    8. emitter.emit('myEvent');

    或者如下情况是否会死循环?

    1. const EventEmitter = require('events');
    2. let emitter = new EventEmitter();
    3. emitter.on('myEvent', () => {
    4. console.log('hi');
    5. emitter.emit('myEvent');
    6. emitter.emit('myEvent');

    以及这样会不会死循环?

    使用 emitter 处理问题可以处理比较复杂的状态场景, 比如 TCP 的复杂状态机, 做多项异步操作的时候每一步都可能报错, 这个时候 .emit 错误并且执行某些 .once 的操作可以将你从泥沼中拯救出来.

    另外可以注意一下的是, 有些同学喜欢用 emitter 来监控某些类的状态, 但是在这些类释放的时候可能会忘记释放 emitter, 而这些类的内部可能持有该 emitter 的 listener 的引用从而导致内存泄漏.

    阻塞/异步

    如何判断接口是否异步? 是否只要有回调函数就是异步?

    开放性问题, 每个写 node 的人都有一套自己的判断方式.

    • 看文档
    • 看是否有 IO 操作

    单纯使用回调函数并不会异步, IO 操作才可能会异步, 除此之外还有使用 setTimeout 等方式实现异步.

    Node.js 中执行 js 代码的过程是单线程的. 只有当前代码都执行完, 才会切入事件循环, 然后从事件队列中 pop 出下一个回调函数开始执行代码. 所以 ① 实现一个 sleep 函数, 只要通过一个死循环就可以阻塞整个 js 的执行流程. (关于如何避免坑爹的同事写出死循环, 在后面的测试环节有写到.)

    如何实现一个 sleep 函数? ①

    1. function sleep(ms) {
    2. var start = Date.now(), expire = start + ms;
    3. while (Date.now() < expire) ;
    4. return;
    5. }

    如果在线上的网站中出现了死循环的逻辑被触发, 整个进程就会一直卡在死循环中, 如果没有多进程部署的话, 之后的网站请求全部会超时, js 代码没有结束那么事件队列就会停下等待不会执行异步, 整个网站无法响应.

    需要了解 reduce 的情况, 是第 n 个与 n+1 的结果异步处理完之后, 在用新的结果与第 n+2 个元素继续依次异步下去. 不贴答案, 期待诸君的版本.

    在笔者这里将 Node.js 中的异步简单的划分为两种, 硬异步和软异步.

    硬异步是指由于 IO 操作或者外部调用走 libuv 而需要异步的情况. 当然, 也存在 readFileSync, execSync 等例外情况, 不过 node 由于是单线程的, 所以如果常规业务在普通时段执行可能比较耗时同步的 IO 操作会使得其执行过程中其他的所有操作都不能响应, 有点作死的感觉. 不过在启动/初始化以及一些工具脚本的应用场景下是完全没问题的. 而一般的场景下 IO 操作都是需要异步的.

    软异步是指, 通过 setTimeout 等方式来实现的异步. 关于 nextTick, setTimeout 以及 setImmediate 三者的区别参见

    Event loop 示例

    1. ┌───────────────────────┐
    2. ┌─>│ timers
    3. └──────────┬────────────┘
    4. ┌──────────┴────────────┐
    5. I/O callbacks
    6. └──────────┬────────────┘
    7. ┌──────────┴────────────┐
    8. idle, prepare
    9. └──────────┬────────────┘ ┌───────────────┐
    10. ┌──────────┴────────────┐ incoming:
    11. poll │<─────┤ connections,
    12. └──────────┬────────────┘ data, etc.
    13. ┌──────────┴────────────┐ └───────────────┘
    14. check
    15. └──────────┬────────────┘
    16. ┌──────────┴────────────┐
    17. └──┤ close callbacks

    关于事件循环, Timers 以及 nextTick 的关系详见官方文档 The Node.js Event Loop, Timers, and process.nextTick(): 英文, 以及 Tasks, microtasks, queues and schedules

    并行/并发

    并行 (Parallel) 与并发 (Concurrent) 是两个很常见的概念.

    可以看 Erlang 作者 Joe Armstrong 的博客 ()

    con_and_par

    并发 (Concurrent) = 2 队列对应 1 咖啡机.

    并行 (Parallel) = 2 队列对应 2 咖啡机.

    至于在 node 中并行, 你可以通过 cluster 来再添加一个咖啡机.