一文搞懂Node.js异步编程|??奥力给??
2020/1/30 17:11:30
本文主要是介绍一文搞懂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的过程就是查看是否有待处理的事件,其流程图如下所示:
阶段详解
-
timers(计时器)阶段,此阶段会尽早的执行到达
阈值
的setTimeout()
或setInterval()
的回调。但是操作系统调度或其他回调的运行可能会延迟它们。 -
pending callbacks(待处理的回调)阶段,执行推迟到下一个循环迭代的I / O回调。
-
idle,prepare(空闲)阶段,仅在内部使用。
-
poll(轮询)阶段,检索新的
I/O
事件;执行与I/O
相关的回调。该阶段包含两个功能:计算应该阻塞并轮询I/O
的事件以及处理轮询队列中的事件。其规则如下:- 在此阶段如果有已经到期的
timer(定时器)
,则会马上执行定时器的回调。 - 如果没有到期的
timer(定时器)
并且轮询队列
不为空,则会按照FIFO
的顺序执行事件。如果轮询队列
为空,并且脚本由setImmediate()
调度,则结束轮询pool
阶段进入check
阶段。如果轮询队列
为空,并且不包含setImmediate()
,则事件循环将等待新的事件添加到队列中,然后马上执行它们。
- 在此阶段如果有已经到期的
-
check(检查)阶段,此阶段允许在
pool
阶段完成后立即执行回调。setImmediate()
的回调在这里执行。 -
close(关闭)阶段,一些关闭回调。如果产生了
close
事件,该事件会被加入到指定的队列中。当close
阶段执行完后,本轮循环结束进入下一轮循环。
setImmediate()和setTimeout()
setImmediate()
实际上是一个特殊的计时器和setTimeout()
类似,但行为取决于调用时间。
setImmediate()
在当前轮询阶段完成后执行脚本。setTimeout()
计划在到达最小阈值后执行脚本。
计时器的执行顺序在不同的上下文中会有所不同,看以下两段代码。
- 主模块中运行,输出顺序受进程性能的限制:
多运行几次,就会发现其顺序输出顺序发生了变化。
- 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.then
和Promise.prototype.catch
都 方法返回promise
对象, 所以它们可以被链式调用。
实战
1、封装异步回调
2、Promise.prototype.finally如果文件路径存在,则会打印文件内容。反之,打印异常。
无论
Promise
对象最后状态如何,都会执行该操作。
3、Promise新建后就会立即执行
4、Promise.all()
5、Promise.race()将多个
Promise
实例包装为一个新的Promise实例。当所有Promise
实例状态都变为fulfilled
时,新的Promise状态才会变为fulfilled
。只要有一个Promise
状态为rejected
,新的Promise状态就变为rejected
。
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异步编程|??奥力给??的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南