1.NodeJs 特点

  • 单线程:单线程的好处,减少了内存开销,操作系统的内存换页。
  • 非阻塞I/O, 进程不会等CPU I/O计算结束,而会执行后面的程序。
  • 事件驱动:事件环,不管是新用户的请求,还是老用户的I/O完成,都将以事件方式加入事件环,等待调度。
  • 线程池: liuv 为异步请求开辟了相关的独立线程处理完毕,通过回调返回结果

    2. 相关概念

    2.1同步编程 vs 异步编程

  • 同步编程:前代码任务耗时执行会阻塞后续代码的执行;

  • 异步编程:一个线程中执行一堆任务,这个线程可以自由的保存,恢复任务的状态。

    2.2并发 vs 并行

  • 并行,指同一时刻内多任务同时进行;

  • 并发,指在同一时间段内,多任务同时进行着,但是某一时刻,只有某一任务执行

    2.3 IO 模型

    NodeJs 使用同步非阻塞IO. 这使得Nodejs程序非常善于处理任务调度,io密集型应用。
    image.png

  • 阻塞IO: 进程放弃CPU(睡眠),等待CPU返回数据。对于所有的“慢速设备”(socket、pipe、fifo、terminal)的IO默认的方式都是阻塞的方式。

  • 非阻塞IO:设置IO相关的系统调用为non-blocaking,随后进行的IO操作无论有没有可用数据都会立即返回,并设置errno为EWOULDBLOCK或者EAGAIN。我们可以通过主动check的方式(polling,轮询)确保IO有效时,随之进行相关的IO操作。当然这种方式看起来就似乎不太靠谱,浪费了太多的CPU时间,用宝贵的CPU时间做轮询太不靠谱儿了
  • 多路复用是让阻塞发生在我们的多路复用IO操作的系统调用上面,而不是我们真正去执行IO的系统调用。使用这个方式的好处就是可以同时监控多个用于IO的文件描述符。
  • 其它的暂时略过,不太清除原理。

NodeJs 为什么不选择异步IO
从发展上看,最初是纯异步,除了性能似乎没啥优势,callbackhell被无数人恶意攻击,随着技术发展,它借鉴C#等语言里优秀的流程控制,在保证异步性能的前提下完成同步写法,这是历史的演进

2.4 堆栈与队列

  • 堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);
  • 栈(stack):后进先出的顺序存储数据结构,通常存储函数参数和基本类型值变量(按值访问);
  • 队列(queue):先进先出顺序存储数据结构

    2.5 JS执行环境

  • 消息队列(message queue),也叫任务队列(task queue):存储待处理消息及对应的回调函数或事件处理程序;

  • 执行栈(execution context stack),也可以叫执行上下文栈:JavaScript执行栈,顾名思义,是由执行上下文组成,当函数调用时,创建并插入一个执行上下文,通常称为执行栈帧(frame),存储着函数参数和局部变量,当该函数执行结束时,弹出该执行栈帧;

Javascript 程序同步程序 通过执行栈(execution context stack)形式在V8中顺序执行。对于普通异步回调会通过列队的形式先进先出。而对于 定时器的回调会通过堆的形式存储(后续事件环中会提到)
image.png

3.Javascript 异步编程

3.1 Nodejs 线程池

在 Node 中,有两种类型的线程:一个事件循环线程(也称为主循环、主线程、事件线程等),它负责任务的编排;另一个是工作线程池中的 K 个工作线程(也被称为线程池),它专门处理繁重的任务。
libuv 通过init_threads 函数初始化线程池,初始化时会根据一个名为 UV_THREADPOOL_SIZE 的环境变量来初始化内部线程池的大小,线程最大数量为 128 ,默认为 4 。(require fs 才启用相关线程?)

libuv 的线程池(工作线程池)作用于以下 4 种枚举的异步请求:

  • UV_FS: fs 模块的异步函数(除了 uv_fs_req_cleanup ),fs.access、fs.stat 等。
  • UV_GETADDRINFO:dns 模块的异步函数,dns.lookup 等。
  • UV_GETNAMEINFO:dns 模块的异步函数,dns.lookupService 等。
  • UV_WORK:zlib 模块的 zlib.unzip、zlib.gzip 等;在 Node.js 的 Addon(C/C++) 中通过 uv_queue_work 创建的多线程请求。

其它的 UV_CONNECT、UV_WRITE、UDP_SEND 等则并不会通过线程池去执行。

  • UV__WORK_CPU:CPU 密集型,UV_WORK 类型的请求被定义为这种类型。因此根据这个分类,不推荐在 uv_queue_work 中做 I/O 密集的操作。
  • UV__WORK_FAST_IO:快 IO 型,UV_FS 类型的请求被定义为这种类型。
  • UV__WORK_SLOW_IO:慢 IO 型,UV_GETADDRINFO 和 UV_GETNAMEINFO 类型的请求被定义为这种类型。

UVWORK_SLOW_IO 执行不同于 UVWORK_CPU 与 UV__WORK_FAST_IO ,libuv 执行它的时候流程会有些差异,这个后面会提到。
promise. timer. nextTick 异步由 v8 执行。

3.2 浏览器执行环境

在浏览器中,JavaScript 执行为单线程(不考虑 web worker),所有代码均在主线程调用栈完成执行。当主线程任务清空后才会去轮循任务队列中的任务。
异步任务分为 task(宏任务,也可以被称为 macrotask)和 microtask(微任务)两类。
当满足执行条件时,task 和 microtask 会被放入各自的队列中,等待进入主线程执行,这两个队列被称为 task queue(或 macrotask queue)和 microtask queue。
task:包括 script 中的代码、setTimeoutsetIntervalI/OUI render
microtask:包括 promise、Object.observe、MutationObserver
不过,正如规范强调的,这里的 task queue 并非是队列,而是集合(sets),因为事件循环的执行规则是执行第一个可执行的任务,而不是将第一个任务出队并执行。
浏览器事件循环流程

  1. 执行完主线程中的任务
  2. 清空 microtask queue 中的任务并执行完毕
  3. 取出 macrotask queue 中的一个任务执行
  4. 清空 microtask queue 中的任务并执行完毕
  5. 重复 3、4

    8.事件循环(Event Loop)流程

  • 本轮循环: Promise ,process.nextTick
  • 次轮循环: setTimeout, setImmediate
  1. 宿主环境为JavaScript创建线程时,会创建堆(heap)和栈(stack),堆内存储JavaScript对象,栈内存储执行上下文;
  2. 栈内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行时,该异步任务进入等待状态(不入栈),同时通知线程:当触发该事件时(或该异步操作响应返回时),需向消息队列插入一个事件消息;
  3. 当事件触发或响应返回时,线程向消息队列插入该事件消息(包含事件及回调);
  4. 当栈内同步任务执行完毕后,线程从消息队列取出一个事件消息,其对应异步任务(函数)入栈,执行回调函数,如果未绑定回调,这个消息会被丢弃,执行完任务后退栈;
  5. 当线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick,事件循环流转一次称为一次tick)。
  6. 事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。

    4.Libuv事件循环7个阶段

    在 libuv 的七个阶段之前, 会先执行process.nextTick,然后执行Promise.resolve 等微任务

  7. Timers: 处理setTimeout和setInterval的回调函数

  8. Pending callbacks:这个阶段会执行除了 close 事件回调、被 timers 设定的回调、setImmediate 设定的回调之外的回调函数(上一轮未执行的io回调);
  9. idle, prepare: libuv 内部调用,这里可以忽略。
  10. Poll for I/O: 轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。等待 I/O 请求返回结果。这个阶段的时间会比较长。
  11. check: 执行上轮setImmediate回调函数
  12. close callbacks: 执行关闭请求的回调函数,比如socket.on(‘close’, …)。

image.png

5.一些总结

  1. 大量的同步请求(await,promise等待)并不会阻塞nodejs 进程。 因为 NodeJs 是非阻塞IO模型;
  2. 尽量不要在for 循环里面写 await 代码。除非有执行先后顺序依赖。
  3. nodejs与浏览器相识。都是先处理一个宏任务,然后清理调微任务。然后继续下一个宏任务。在Node11之前,Nodejs 是先清理宏任务列队里面的任务,然后在event loop每个阶段开始清理微任务。

参考链接:
JavaScript异步编程
Node 定时器详解
从源码解读 Node 事件循环