Javascript

一文搞懂Node.js异步编程|??奥力给??

本文主要是介绍一文搞懂Node.js异步编程|??奥力给??,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

如果你曾经在面试时答错了Node异步编程相关的问题,那么本文一定是你的菜。🥕🍅🍚

这篇文章主要整理了一下以前的学习笔记。写这篇文章的目的一方面是熟悉一下以前的知识,另一方面希望可以对不了解Node异步编程的同学有所帮助。

本文主要包含了Node异步API(非I/O)、事件循环、异步编程优势以及难点相关知识进行了整理。最后整理了Promise以及async/await是如何解决异步编程带来的的问题。

单线程

Node保持了JavaScript在浏览器中单线程的特点。在Node中,JavaScript与其余线程是无法共享任何状态的。单线程最大的好处是不用像多线程那样处处在意状态同步的问题,不存在死锁,也没有线程上下文交换所带来的性能上的开销。

单线程也有其自身的弱点,这些弱点是学习Node的过程中必须要面对的。积极的面对这些弱点,可以享受Node带来的好处,也能避免潜在的问题,使其得以高效的利用。单线程的弱点包含以下3个方面。

  • 无法利用多核CPU。
  • 错误会整个应用退出,应用的健壮性值得考验。
  • 大量计算占用CPU导致无法继续调用异步I/O。

同步vs异步

这章我用生活中的一个栗子来演示同步和异步。
老婆今天中午准备大显身手。我们12:30开饭,菜单如下:

  • 糖醋鱼
  • 炒青菜
  • 炒白菜

同步

异步

总结

同步的例子中,老婆在等我买糖回来,做完糖醋鱼后开始做其它菜肴,本应该12:30就可以吃饭,最后13:00才吃饭。
异步的例子中,老婆并没有等我买糖回来,而是选择先做其它的菜肴。在我买糖回来后开始做糖醋鱼,最后我们12:30准时吃饭。

Node异步API(非I/O)

定时器

Node中的setTimeout()setInterval()与浏览器中的API是一致的,分别用于单次和多次定时执行任务。调用setTimeout()或者setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就生成一个事件它的回调函数将立即执行。

定时器的问题在于,它并非精确的(在容忍的范围之内)。看个例子:

尝试多次运行,你会发现每次输出的结果都不相同。

定时器的行为:

process.nextTick()

在未了解process.nextTick()之前,为了立即执行一个任务,会使用setTimeout()来达到所需要的效果,像这样:

 setTimeout(()=>{
    //TODO
 },0);
复制代码

但是由于事件循环自身的特点,定时器的精确度不够。事实上,采用定时器需要动用红黑树,创建定时器对象和迭代等操作,所以setTimeout(fn,0)的方式比较浪费性能,而process.nextTick()相对较为轻量。每次调用process.nextTick(),只会讲回调函数放入队列中,在下一轮Tick时取出执行。定时器中采用红黑树的操作时间复杂度为0(lg(n)),nextTick()的时间复杂度为0(1)。

试试用process.nextTick(fn)改造一下做饭的例子吧!

setImmediate()

setImmediate()process.nextTick()方法十分的类似,都是将回调函数延迟执行。但是两者又有细微的差别,看段代码:

从运行的结果可以看到,process.nextTick()中的回调函数执行的优先级要高于setImmediate()。主要原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一轮循环检查中,idle观察者优先于I/O观察者,I/O观察者优先于check观察者。

在实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行,而setImmediate()在每轮循环中执行链表中的一个回调函数。看段代码:

从执行结果上可以看到,在process.nextTick()回调函数执行完后,开始执行setImmediate()的回调函数。但是,当第一个setImmediate()的回调函数执行后,并没有立即执行第二个,而是进入了下一轮循环,再次按照process.nextTick()优先,setImmediate()次后的顺序执行。这样设计的目的就是为了保证每轮循环能够较快的执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。

Event Loop(事件循环)

灵魂一题

带着你的答案向下看

概念

虽然JavaScript是单线程的,但是事件循环使Node.js可以通过将操作转移到系统内核来执行非阻塞I/O操作。 我们可以从Node.js系统架构图理解一下上面这段话。

从图中可以看到,Node.js底层有一个非常重要的libuv库,它不仅实现了Node.js跨平台的特性,而且里面包含了事件队列、事件循环以及线程池。

在进程启动时Node会创建一个类似于while(true)的循环,轮询的从事件队列取出任务并分配给指定的线程去处理,一旦某个线程的任务执行完成,它会立即将执行结果返回,而每一次循环的过程我们称之为Tick。每个Tick的过程就是查看是否有待处理的事件,其流程图如下所示:

阶段详解

  1. timers(计时器)阶段,此阶段会尽早的执行到达阈值setTimeout()setInterval()的回调。但是操作系统调度或其他回调的运行可能会延迟它们。

  2. pending callbacks(待处理的回调)阶段,执行推迟到下一个循环迭代的I / O回调。

  3. idle,prepare(空闲)阶段,仅在内部使用。

  4. poll(轮询)阶段,检索新的I/O事件;执行与I/O相关的回调。该阶段包含两个功能:计算应该阻塞并轮询I/O的事件以及处理轮询队列中的事件。其规则如下:

    • 在此阶段如果有已经到期的timer(定时器),则会马上执行定时器的回调。
    • 如果没有到期的timer(定时器)并且轮询队列不为空,则会按照FIFO的顺序执行事件。如果轮询队列为空,并且脚本由setImmediate()调度,则结束轮询pool阶段进入check阶段。如果轮询队列为空,并且不包含setImmediate(),则事件循环将等待新的事件添加到队列中,然后马上执行它们。
  5. check(检查)阶段,此阶段允许在pool阶段完成后立即执行回调。setImmediate()的回调在这里执行。

  6. close(关闭)阶段,一些关闭回调。如果产生了close事件,该事件会被加入到指定的队列中。当close阶段执行完后,本轮循环结束进入下一轮循环。

setImmediate()和setTimeout()

setImmediate()实际上是一个特殊的计时器和setTimeout()类似,但行为取决于调用时间。

  • setImmediate()在当前轮询阶段完成后执行脚本。
  • setTimeout()计划在到达最小阈值后执行脚本。

计时器的执行顺序在不同的上下文中会有所不同,看以下两段代码。

  1. 主模块中运行,输出顺序受进程性能的限制:

多运行几次,就会发现其顺序输出顺序发生了变化。

  1. I/O周期中,则始终先执行setImmediate()

Node异步编程优势与难点

优势

Node最大的特性莫过于事件驱动的的I/O模型,非阻塞I/O可以使CPU与I/O并不依赖相互等待,让资源得到更好的利用。

难点

异常处理

try/catch无法捕获callback中的抛出的异常,举个栗子:

为了解决这个问题,Node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常抛出。

  async ((err,result) => {
     if (err){}
  });
复制代码

回调金字塔

有这样一个需求:要求读取指定目录下后缀名为readme.txt的文件的内容,修改其内容并验证是否修改成功。上代码:

多层的回调不仅让代码变得难以阅读,并且一旦需求发生变化维护的难度也非常高。

异步编程解决方案

Promise

概念

Promise是异步编程的的一种解决方案,比传统的解决方案事件和回调函数更合理更强大。

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。

Promise具有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 代表操作成功完成。
  • rejected: 代表操作失败。

特点

Promise对象具有以下两个特点:

  • 对象的状态不受外界的影响,只有异步操作的结果可以决定当前是哪一种状态,任何其他的操作都无法改变这个状态。
  • 一旦状态改变,就不会在变,任何时候都可以得到这个结果。

链式调用

由于Promise.prototype.thenPromise.prototype.catch都 方法返回promise对象, 所以它们可以被链式调用。

实战

1、封装异步回调

如果文件路径存在,则会打印文件内容。反之,打印异常。

2、Promise.prototype.finally

无论Promise对象最后状态如何,都会执行该操作。

3、Promise新建后就会立即执行

4、Promise.all()

将多个Promise实例包装为一个新的Promise实例。当所有Promise实例状态都变为fulfilled时,新的Promise状态才会变为fulfilled。只要有一个Promise状态为rejected,新的Promise状态就变为rejected

5、Promise.race()

Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例。只要多个Promise实例中有一个的状态率先改变则新的Promise实例的状态就会发生改变。

缺点

  • Promise一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内抛出的错误,不会反应到外部。
  • 当处于pending阶段时,无法得知当前进展到那个阶段。

async/await

概念

异步函数可以包含await指令,该指令会暂停异步函数的执行,并等待Promise执行,然后继续执行异步函数,并返回结果。

await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。

实战

1、替换Promise.then()

2、捕获异常

参考

<<深入浅出Node.js>> 朴灵 著
MDN Promise
MDN async
ECMAScript 6 入门
Event Loop

这篇关于一文搞懂Node.js异步编程|??奥力给??的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!