你笑了

你的笑,是星星跳跃浪花的笑

0%

Node.js 特性

架构

  • Node bindlings: 是C++与JavaScript沟通的桥梁, 封装了V8和Libuv的细节,向上层提供API。

特点

  • 对于I/O密集型的应用,能提供高效的性能

    文件读取、网络请求等任务

    • 网络应用

      API服务器,实时通信服务

    • 文件操作

  • 对于CPU密集型的任务,会阻塞事件循环,降低性能

    • 复杂的计算和数据处理,通常需要连续的、不间断的CPU时间来执行,将占据整个主线程,导致异步操作无法及时处理,阻塞事件循环,使程序无法及时响应,降低程序的性能

    • 可以使用单独的线程或进程去处理CPU密集型计算,避免阻塞主线程及事件循环

      线程:worker_threads(在一个 Node.js 进程中创建多个线程)

      进程:child_process(创建新的进程)

      集群:cluster(在多个 CPU 核上运行 Node.js 应用)

单线程

  • 一个 Node.js 应用只在一个进程上运行,使用一个线程来执行 JavaScript 代码

call stack

Call Stack 和 Execution Context Stack 是同一个东西,即 LIFO 队列,用于存储代码执行期间创建的上下文

  • 当调用一个函数时,该函数的返回地址、参数、局部变量都会被压栈(push)。如果当前函数又调用了其他函数,则其他函数的相关数据也会放入调用栈。当函数返回的时候,就会被出栈(pop),返回值被赋值给局部变量。然后该局部变量也会被压栈。
  • 当函数执行完后,该函数中的局部变量会被出栈(pop),然后该变量就消失了(仅限number string boolean),如果是对象、数组等的值存储在堆(heap)中。变量只是指向它们的指针,传递变量只是传递的指针,使指针的值在不同的栈帧中可变。当函数从栈中弹出时,只弹出了指向对象的指针,而实际值在堆中,由垃圾回收器回收。

异步非阻塞I/O

  • 异步指异步获取结果,但是代码还是同步执行的

  • 通过回调函数异步获取(Promise本质也是回调,只是解决了回调地狱,提高了代码的可读性)

    例如 async方法中的代码、await后面表达式中的代码、Promise传入的executor中的代码,都会同步执行,只是回调函数会进入事件循环异步执行,例如 async方法中await下面行的代码、setTimeout中传入的回调、promise.then方法中传入的回调

  • 通过事件循环实现

事件驱动

  1. 主线程执行同步代码的过程中,将异步任务及其回调函数卸载到系统内核

  2. 由libuv 通过工作队列线程池等机制执行异步任务,异步任务完成后,根据异步任务的类型将其回调函数放入事件循环的回调队列

    异步任务完成后才将回调加入队列

  3. 主线程空闲时,从事件循环中获取回调函数,压入调用栈(Call Stack)执行

libuv

  • node 事件循环底层由 libuv 实现

线程池

  • 默认大小为 4,但可以在启动时通过 UV_THREADPOOL_SIZE 环境变量来更改。增加线程池的大小能够缩短运行异步方法的多次调用的总时间

  • 支持多个异步操作并发执行

    例如支持 promise concurrency 的 Promise.all 依赖于线程池实现

工作队列

  • uv_queue_work 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    loop = uv_default_loop();
    # 获取libuv提供的 默认事件循环对象
    uv_queue_work(loop, &req[i], fib, after_fib);
    # loop 即事件循环
    # req[i] 异步工作请求
    # 执行fib函数,即异步任务
    # 执行after_fib函数,即异步任务完成后的回调函数
    uv_run(loop, UV_RUN_DEFAULT);
    # 以默认模式运行 libuv 的事件循环,开始处理异步操作和回调函数。
    # 即 uv_run 函数会一直运行,直到事件循环中没有未完成的任务或调用 uv_stop 停止事件循环
    • 将异步任务(fib)放入libuv的工作队列中执行

    • 执行完后,根据异步任务的类型,将回调函数(after_fib)放入事件循环对应的队列中

      例如 setImmediate 的回调将放入 check阶段的队列中

    • 先获取事件循环对象,主线程同步代码执行完毕后才运行

事件循环

  • nodejs 使用libuv提供的默认循环

    1
    uv_loop_t *loop = uv_default_loop()
  • 在单线程上运行

  • 由函数uv_run控制,nodejs程序启动后,通过一个 while true 循环,在主线程上运行

    1
    2
    3
    4
    5
    6
    // 整个程序运行逻辑可以按如下方式理解
    function main(){
    // run synchronous code
    uv_run(loop, UV_RUN_DEFAULT); // 同步代码执行完之后才调用 uv_run 开始事件循环
    }
    main()

    参考 Does the event loop run on same thread as the JS main thread?

    当我们在回调函数里执行 while(1) {}, 整个服务都无法响应了

阶段

  • 共6个阶段,每个阶段有自己特定的操作并维护一个FIFO回调队列,执行不同类型的回调
  • 事件循环进入到一个阶段时,会执行该阶段特定的操作及队列中的回调,直到队列为空或达到最大执行数量限制。然后进入下一阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
timers
  • 执行由 setTimeout()setInterval() 调度的回调

  • 设定的执行时间是最快执行时间,一般会比预期时间要晚

    实际执行时间由 poll 阶段控制

    进入 poll 阶段,如果队列为空,会等待,直到达到最快timer的阈值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const fs = require('fs');

    function someAsyncOperation(callback) {
    // Assume this takes 95ms to complete
    fs.readFile('/path/to/file', callback);
    }

    const timeoutScheduled = Date.now();

    setTimeout(() => {
    const delay = Date.now() - timeoutScheduled;

    console.log(`${delay}ms have passed since I was scheduled`);
    }, 100);

    // do someAsyncOperation which takes 95 ms to complete
    someAsyncOperation(() => {
    const startCallback = Date.now();

    // do something that will take 10ms...
    while (Date.now() - startCallback < 10) {
    // do nothing
    }
    });
    • 事件循环进入 timer 阶段,没有到达的timer,继续下一阶段
    • 进入 poll 阶段,由于fs.readFile 尚未完成,回调队列为空,因此node将阻塞在该阶段,直到达到最快timer的阈值(即预期等待100ms)。当等待95ms后fs.readFile完成,回调函数被加入队列中并执行,耗时10ms。回调完成时,队列为空,node将检查是否有达到的timer,发现已经有一个达到,然后回到timer阶段执行其回调,此时timer回调函数在105ms后才执行
    • libuv 会限制 poll 阶段的最大等待时间,最大值依赖于系统
pending callback
  • 执行延迟到下一个循环迭代的 I/O 回调,例如一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误 callback

    大多数情况下,所有 I/O 回调都会在轮询阶段执行。然而,在某些情况下,有些回调会被推迟到下一个循环

    如果TCP socket 在尝试连接时收到 ECONNREFUSED 错误,一些*nix系统希望等待报告错误,这些回调会在 pending callback 执行

idle, prepare
  • 仅系统内部使用
poll

poll阶段有两个功能:

  • 计算最长阻塞时间并等待传入的回调(连接或请求等)

    node会计算最快达到的timer时间,然后阻塞在该阶段

  • 处理 poll 队列的事件

    执行除 close callback、timer callback、setImmediate callback 之外的所有回调。

当进入到poll阶段,并且没有设定 timers 时,会发生以下两种情况

  1. poll队列不为空

    Event Loop 将同步的执行poll queue里的callback,直到queue为空或者执行的callback到达上限

  2. poll队列为空

    1. 如果代码设定了setImmediate(), Event Loop将结束poll阶段,进入到check阶段执行setImmediate()的回调
    2. 如果代码没有设定setImmediate(),Event Loop将阻塞在该阶段,等待回调添加到队列中,然后立即执行

当进入到poll阶段,并且设定了timers

  • 一旦 poll 队列为空,Event Loop会检查是否有已经到达的timer,如果有,Event Loop将回到timer阶段并执行那些timer的callback
check
  • 执行 setImmediate() 调度的回调
close callback
  • 执行关闭事件的回调

    例如socket.on('close', ...)

setImmediate vs setTimeout

setTimeout

  • 在指定时间后执行

    设置的时间是最快执行时间,实际执行时间要 >= 设定的时间,因为实际执行时间由 poll 阶段控制

setImmediate

  • 在当前循环 poll 阶段后执行

  • 将操作放在下一轮循环中执行

    不阻塞当前上下文向 当前微任务队列中 添加的微任务(执行)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    async function tryCallAsync(fn, ...args) {
    await new Promise(resolve => {
    setImmediate(resolve);
    });
    return fn(...args);
    }
    // use setImmediate to ensure all sync logic will run in the next loop
    new Promise(resolve => setImmediate(resolve)).then(() => { // 下面的回调将异步执行
    Auth.handleCaptchaSuccess(type, account, sendType)
    .catch(err => {
    logger.error('Fail to handle captcha success', err)
    })
    })

    参考

执行顺序

  • 在主模块(非IO循环中)注册

    执行顺序不确定,与进程性能有关

    1
    2
    3
    4
    5
    6
    7
    8
    // timeout_vs_immediate.js
    setTimeout(() => {
    console.log('timeout');
    }, 0);

    setImmediate(() => {
    console.log('immediate');
    });
  • 在回调函数中注册

    先执行 immediate 再执行 timeout

    1
    2
    3
    4
    5
    6
    7
    8
    setTimeout(() => {
    setTimeout(() => {
    console.log('timeout')
    }, 0)
    setImmediate(() => {
    console.log('immediate')
    })
    }, 0)

浏览器事件循环

不要混淆nodejs和浏览器中的event loop

区别

实现
  • nodejs的event是基于libuv实现
  • 浏览器的event loop,在html5的规范中定义,由浏览器厂商实现
任务

宏任务

浏览器 Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

微任务

浏览器 Node
process.nextTick
MutationObserver
Promise.then(catch finally)