
关于 Node.js 异步,绕不开两点:非阻塞 I/O 和事件循环。也正是因为这两点,Node.js 才能被称为高性能并运用到线上环境中,那么下面来了解一下 Node.js 的异步机制和使用方式吧!【推荐学习:《nodejs 教程》】
Node.js 的非阻塞 I/O
- I/O 即 Input/Output,一个系统的输入和输出。
- 阻塞 I/O 和非阻塞 I/O 的区别就在于系统接收输入再到输出期间,能不能接收其他输入。
以点菜吃饭为例子:去饭堂点菜吃饭需要排队等待,在这个过程中,阿姨每次只能接待一个人,“点菜-阿姨抖勺装菜-把饭菜给到你”这个过程中阿姨并不能接收其他人的点菜,这个就是阻塞 I/O;而去餐馆点菜吃饭,去到餐馆就可以跟服务员你要吃番茄炒蛋,服务员记下来之后交给后厨,这时候来了另一桌人就把服务员招呼过去说想吃小龙虾,也就是说,在把菜给你上上来之前服务员接收了其他人的点菜,那这个就是非阻塞型 I/O。
理解非阻塞 I/O 的要点在于
- 确定一个进行 Input/Output的系统。
- 思考在 I/O 过程中,能不能进行其他 I/O。
那在点菜吃饭这个例子中,一个进行 Input/Output 的系统就是点餐-后厨(阿姨)处理-上菜这样一个能让你吃上饭的系统;点餐就是 Input,上菜就是 Output,在这个例子中判断两者是非阻塞型还是阻塞型的关键就在于在点菜上菜这个过程中能不能接受其它的点菜上菜。就好比你点了个佛跳墙,等上菜可能就要好久了,然后来的人都是点一些简单的菜品,一分钟炒一份炒粉的那种,可能就是来来回回几波人之后都还没能给你上菜。
而 Node.js 它是用来操纵计算机的,一些如读取文件之类的操作是非常耗时的,要是不能进行其它的 I/O,那么处理效率就很会很低了,这也是 Node.js 是非阻塞型 I/O 的一个原因。
Node.js 的事件循环
Node.js 启动的时候会初始化由 libuv 提供的事件循环,每次的事件循环都包含6个阶段,这6个阶段会在每一次的事件循环当中按照下图当中的顺序反复执行,如下图:

- timers阶段:这个阶段执行- timer(- setTimeout、- setInterval)的回调
- I/O callbacks阶段 :处理一些上一轮循环中的少数未执行的 I/O 回调
- idle,- prepare阶段 :仅 Node 内部使用
- poll阶段 :获取新的 I/O 事件, 适当的条件下 Node 将阻塞在这里
- check阶段 :执行- setImmediate()的回调
- close callbacks阶段:执行- socket的- close事件回调
每个阶段都有一个先入先出的(FIFO)的用于执行回调的队列,事件循环运行到每个阶段,都会从对应的回调队列中取出回调函数去执行,直到队列当中的内容耗尽,或者执行的回调数量达到了最大。
然后事件循环就会进入下一个阶段,然后又从下一个阶段对应的队列中取出回调函数执行,这样反复直到事件循环的最后一个阶段。而事件循环也会一个一个按照循环执行,直到进程结束。
事件循环当中的6个宏队列和微队列的关系如下:微队列(microtask)在事件循环的各个阶段之间执行,或者说在事件循环的各个阶段对应的宏队列(macrotask)之间执行。

这里有一个特别容易混淆的版本改变:
- 如果是 Node10 及其之前版本:宏队列当中的有几个宏任务,是要等到宏队列当中的所有宏任务全部执行完毕才会去执行微队列当中的微任务
- 如果是 Node11 及之后版本:一旦执行一个阶段里对应宏队列当中的一个宏任务(setTimeout,setInterval和setImmediate三者其中之一,不包括I/O)就立刻执行微任务队列,执行完微队列当中的所有微任务再回到刚才的宏队列执行下一个宏任务。这就跟浏览器端运行一致了。
Node.js 异步编程 - callback
- 回调函数格式规范- error-first callback
- node-style callback
 
- 第一个参数是 error,后面的参数才是结果。
// 第一个参数是错误捕获interview(function (err, res) {  if (err) {    console.log('cry')    return;  }  console.log('smile')})function interview(callback) {  setTimeout(() => {    if (Math.random() > 0.2) {      callback(null, 'success')    } else {      callback(new Error('fail'))    }  }, 500)}异步流程控制:回调地狱、异步并发等问题
- npm:- async.js;可以通过- async.js来控制异步流程
- thunk:一种编程方式
Node.js 异步编程 – Promise
- 可以通过字面意思理解,Promise是承诺的意思;当前事件循环得不到的结果,但未来的事件循环会给到你结果
- 它是一个状态机,状态一旦确定为 resolved或rejected就不会改变- pending:初始状态,还没得到结果的状态
- fulfilled/- resolved:成功状态
- rejected:失败状态
 
链式调用:.then 和 .catch
- resolved状态的- Promise会回调后面的第一个- .then
- rejected状态的- Promise会回调后面的第一个- .catch
- 任何一个 rejected状态且后面没有.catch的Promise,都会造成浏览器/ Node 环境的全局错误
// promise的状态转换以及通过then获取内容const promise = new Promise((resolve, reject) => {  setTimeout(function () {    resolve(3);    // reject(new Error(4))  }, 500)})promise.then(function (result) {  console.log(result)}).catch(function (err) {  console.log(err)})setTimeout(() => {  console.log(promise)}, 800)执行 then 和 catch 会返回一个新 Promise,该 Promise 最终状态根据 then 和 catch 的回调函数的执行结果决定
- 如果回调函数最终是 throw,该Promise是rejected状态
- 如果回调函数最终是 return,该Promise是resolved状态
- 但如果回调函数最终 return了一个Promise,该Promise会和回调函数return的Promise状态保持一致
Node.js 异步编程 – async/await
- async function是- Promise的语法糖封装
- 异步编程的终极方案 – 以同步的方式写异步- await关键字可以“暂停”- async function的执行
- await关键字可以以同步的写法获取- Promise的执行结果
- try-catch可以获取- await所得到的错误
 
(async function () {  await findJob()  console.log('trip')})()async function findJob() {  try {    // 进行三轮面试    await interview(1);    await interview(2);    await interview(3);    console.log('smile')  } catch (e) {    console.log('cry at ' + e.round)  }}// 进行第round轮面试function interview(round) {  return new Promise((resolve, reject) => {    setTimeout(() => {      if (Math.random() < 0.2) {        const error = new Error('failed');        error.round = round;        reject(error);      } else {        resolve('success');      }    }, 500)  })}这是一个穿越事件循环存在的 function。
总结
- 理解非阻塞 I/O 主要在于确定一个进行 I/O 的系统,然后思考判断能不能进行其它 I/O。
- Node.js 的事件循环在 Node11 版本及之后是和浏览器的事件循环运行一致的,要注意区分。
- Node.js 异步编程的规范是第一个参数是 error,后面的才是结果。
- Promise是一个状态机,初始状态为- pending,一旦确定状态为- resolved或- rejected就不会改变,可以通过- .then和- .catch进行链式调用。
- async/- await以同步的方式写异步,是异步编程的终极解决方案。
更多编程相关知识,请访问:编程视频!!
以上就是深入浅析Node.js中的异步的知识。速戳>>知识兔学习精品课!
