likes
comments
collection
share

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

作者站长头像
站长
· 阅读数 58

首先说明node 版本18.15.0

nodejs依赖了很多模块,v8和libuv是node最重要的两个依赖

  1. uv: 提供Nodejs访问操作系统各种特性的能力,包括文件系统、Socket等,io,包括进程、线程、信号、定时器、进程间通信等。
  2. v8: 将Js代码解释编译执行

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

简单了解下架构区别:

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

libuv架构图介绍:

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

有了整体了解之后,先开始讲解第一个

1. 定时器 setTimeout

Node.js 中 setTimeout 的实现,主要由两部分组成, js 这边提供的定时器的调度管理 和 libuv C++侧提供的定时器的实际执行(暂时无法理解,没关系)

查看setTimeout源码

// node-18.15.0/lib/timer.js

function setTimeout(callback, after, arg1, arg2, arg3) {

  let i, args;
  //统一回调函数中参数,参数归一化
  switch (arguments.length) {
    // fast cases
    case 1:
    case 2:
      break;
    case 3:
      args = [arg1];
      break;
    case 4:
      args = [arg1, arg2];
      break;
    default:
      args = [arg1, arg2, arg3];
      for (i = 5; i < arguments.length; i++) {
        // Extend array dynamically, makes .apply run much faster in v6.0.0
        args[i - 2] = arguments[i];
      }
      break;
  }

  // 2、实例化Timeout
  // 注意:第4个参数isRepeat表示是否重复执行,这里是跟setInterval()的区别,setInterval()对应值为true
  const timeout = new Timeout(callback, after, args, false, true);
  // 3、把Timeout实例插入Map和优先队列中
  insert(timeout, timeout._idleTimeout);

  return timeout;
}

1.2 查看 Timeout代码

class Timeout {
  constructor(callback, after, args, isRepeat, isRefed) {
    after *= 1; 
    // setTimeout超时时间小于1时,则默认为1
    if (!(after >= 1 && after <= TIMEOUT_MAX)) {
      if (after > TIMEOUT_MAX) {
        process.emitWarning(`${after} does not fit into` +
                            ' a 32-bit signed integer.' +
                            '\nTimeout duration was set to 1.',
                            'TimeoutOverflowWarning');
      }
      after = 1;
    }

    this._idleTimeout = after; //超时时间
    this._idlePrev = this;
    this._idleNext = this;
    this._idleStart = null; // 定时器开始时间
    this._onTimeout = null;
    this._onTimeout = callback; // 超时的回调函数
    this._timerArgs = args;
    this._repeat = isRepeat ? after : null;
    this._destroyed = false;
    if (isRefed)
      incRefCount();
    this[kRefed] = isRefed;
    this[kHasPrimitive] = false;

    initAsyncResource(this, 'Timeout');
  }
...

主要逻辑如下:

  1. 判断过期间隔的合法性,如果时间不合法,则强行将过期时间改为 1

  2. 将各种元数据信息放到成员变量中(如超时时间、定时器开始时间、过期回调函数等)

  3. _onTimeout 变量就是 setTimeout() 时候传进来的回调函数,具体在 listOnTimeout 方法中被调用

  4. isRepeat 表示 Timeout 是否重复执行; setTimeout() 将该值是 false,即不重复;而 setInterval() 该值是 true,即重复

  5. incRefCount() :用于激活 libuv 的定时器

编写如下代码:

//settimeout.js
setTimeout(() => {
  console.log('立即执行setTimeout');
}, 0);

执行 node --inspect-brk settimeout.js

打开浏览器输入:chrome://inspect/#devices 进入调试页面:

调试整个insert(timeout, timeout._idleTimeout)过程:

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

具体源代码如下:

// node-18.15.0/lib/internal/timers.js
function insert(item, msecs, start = getLibuvNow()) {

  msecs = MathTrunc(msecs);  // 去掉小数,保留整数部分
  item._idleStart = start;   // 当前定时器的开始时间

  let list = timerListMap[msecs];  
  if (list === undefined) {
    const expiry = start + msecs; // 计算该链表的最近超时时间
    timerListMap[msecs] = list = new TimersList(expiry, msecs); // 链表存入map,key是超时时间

    timerListQueue.insert(list); // 链表插入优先队列

    if (nextExpiry > expiry) { // nextExpiry 存的是所有Timeout实例中,最近要过期的时间
      scheduleTimer(msecs); // 启动定时器,调用了libuv的uv_timer_start方法
      nextExpiry = expiry;
    }
  }
  L.append(list, item); // 江传入的timeout 加入list链表中
}

主要逻辑如下

  1. MathTrunc:截断超时时间,去掉小数,只保留整数部分

  2. _idleStart:记录当前定时器的开始时间

  3. Map 获取当前超时时间对应的链表,链表如果不存在,则计算过期时间点 expiry 并实例化一个新链表,这个过期时间就是这整条链表的 最近超时时间 了,然后新链表存入 Map 中,同时插入优先队列;最后判断是否需要启动定时器,通过 scheduleTimer() 启动定时器

  4. 最后把当前定时器插入到链表的最后面

例如:不同时间设置多个定时器函数就可以形成下图:

setTimeout(() => {  
   console.log('立即执行setTimeout');
}, 100);

setTimeout(() => { 
 console.log('立即执行setTimeout1');
}, 3000);

setTimeout(() => { 
 console.log('立即执行setTimeout1');
}, 5000);

setTimeout(() => { 
 console.log('立即执行setTimeout1');
}, 3000);

setTimeout(() => { 
 console.log('立即执行setTimeout1');
}, 100);

setTimeout(() => {  
   console.log('立即执行setTimeout1');
}, 3000);


构成的结构如下图,

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

const timerListQueue = new PriorityQueue(compareTimersLists, setPosition);

其中timerListQueue 优先级队列是根据队列的过期时间对不同TimersList 进行排序,可以获取最小过期队列时间

优先队列中,以 最近超时时间 expiry 为权重,所以优先队列的顺序就是 100ms 的链表、3000ms 链表、5000ms 链表

到这里js侧这边就基本结束了,什么时候处理收集起来的这些定时器回调呢???

1.3 定时器的启动和执行

在前面 setTimeout()insert() 源码中,把 Timeout 实例插入链表时,有如下代码

if (nextExpiry > expiry) { // 如果有一个最近要过期的时间 比 全部的Timeout的过期时间都要小
  scheduleTimer(msecs); // 触发定时器的启动
  nextExpiry = expiry; // 替换掉所有定时器最近要过期的时间点
}

scheduleTimer() 的作用:就是开始定时器的调度:

// node-18.15.0/lib/internal/timers.js
const {
  scheduleTimer, // 内置模块timers里的ScheduleTimer方法
} = internalBinding('timers');

internalBinding 是加载 timers 内置模块,此处对应 src/timers.cc 文件,在

timers.cc 的 ScheduleTimer 如下

// node-18.15.0/src/timers.cc
void ScheduleTimer(const FunctionCallbackInfo<Value>& args) {
  auto env = Environment::GetCurrent(args);
  // 调用Environment的ScheduleTimer方法
  env->ScheduleTimer(args[0]->IntegerValue(env->context()).FromJust());
}

又调用Environment的ScheduleTimer方法

env.cc 里的ScheduleTimer()如下,可知调用了 libuvuv_timer_start()

// node-18.15.0/src/env.cc
void Environment::ScheduleTimer(int64_t duration_ms) {
  if (started_cleanup_) return;
  // libuv的uv_timer_start方法,这里注入了RunTimers方法作为 libuv定时器执行完成后会调用的回调
  uv_timer_start(timer_handle(), RunTimers, duration_ms, 0);
}

这个时候开始进入libuv:

libuvuv_timer_start如下,

如果 libuv 的定时器未激活过则会激活,然后将定时器的触发时间改成传进来的最新时间并激活

// node-18.15.0/deps/uv/src/timer.c
int uv_timer_start(uv_timer_t* handle,
                   uv_timer_cb cb, // 超时之后的回调函数
                   uint64_t timeout, // 超时时间
                   uint64_t repeat) {
  uint64_t clamped_timeout;

  if (uv__is_closing(handle) || cb == NULL)
    return UV_EINVAL;

  // 在启动定时器前,把之前的先停到
  if (uv__is_active(handle))
    uv_timer_stop(handle);

  // 计算超时时间 事件循环的当前时间+下一个超时时间
  clamped_timeout = handle->loop->time + timeout;
  if (clamped_timeout < timeout)
    clamped_timeout = (uint64_t) -1;

  // 初始化参数
  handle->timer_cb = cb; // 回调函数,在Node.js中其实就是 Environment的RunTimers方法
  handle->timeout = clamped_timeout; // 超时时间
  handle->repeat = repeat; // 是否重复运行
  /* start_id is the second index to be compared in timer_less_than() */
  handle->start_id = handle->loop->timer_counter++; // 赋予一个唯一的自增id

  // 插入小顶堆,里面会根据该节点的超时时间动态调整小顶堆
  heap_insert(timer_heap(handle->loop),
              (struct heap_node*) &handle->heap_node,
              timer_less_than);
  // 启动定时器
  uv__handle_start(handle);

  return 0;
}

1.3.2 libuv 侧:就一个定时器

在 js 侧,调用一次 setTimeout() 就生成一个 Timeout 实例,而且 Timeout 实例被 Map 和优先队列管理起来;并不实际参与调度,真正 libuv 定时器调度时,被引用一下而已。真正在 libuv 层,定时器就只有一个

Node.js 并不是每次调 setTimeout() 的时候都往libuv插入一个节点,因为这样会引起 js 侧和 C++ 、C 侧频繁通信,损耗性能。因此在 Node.js 中,只有一个关于 uv_timer_t(uv_timer_s)handle,Node.js 在 js 侧维护了 Map 和优先队列,每次计算出最快到期节点的时间,然后修改 libuv uv_timer_s handle 的超时时间

启动定时器后,然后呢???

2.0开启libuv事件循环

到这里就需要了解一点libuv事件循环系统,在执行node 脚本启动时 node::Start开始启动,启动的过程中会开启事件循环系统:

事件循环的主要代码如下

// libuv事件循环
// src/api/embed_heplers.ts
Maybe<int> SpinEventLoop(Environment* env) {
  {
    do {
        
      if (env->is_stopping()) break;
      // 启动libuv事件循环,调用libuv的事件循环,处理事件循环的7个阶段
      uv_run(env->event_loop(), UV_RUN_DEFAULT);

      if (env->is_stopping()) break;

      platform->DrainTasks(isolate);

      more = uv_loop_alive(env->event_loop());
      if (more && !env->is_stopping()) continue;

      if (EmitProcessBeforeExit(env).IsNothing())
        break;

      {
        HandleScope handle_scope(isolate);
        if (env->RunSnapshotSerializeCallback().IsEmpty()) {
          break;
        }
      }

      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env->event_loop());
    } while (more == true && !env->is_stopping());

内层循环:在 do-while 循环中,会调用 uv_run() 方法, uv_run() 就是事件循环的主要逻辑

uv_run(env->event_loop(), UV_RUN_DEFAULT) 表示执行事件循环; UV_RUN_DEFAULT :默认轮询模式,此模式会一直运行事件循环直到没有活跃句柄、引用句柄、和请求句柄

事件循环分析

// node-18.15.0/deps/uv/src/unix/core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  // 是否有活跃事件
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  // libuv的uv_stop()函数会把 loop->stop_flag 设置为 1,设置为 1 则会停止事件循环
  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop); // 更新当前时间,每轮事件循环会缓存这个时间以便后面使用,避免过多系统调用损耗性能
    uv__run_timers(loop); // 执行定时器回调
    ran_pending = uv__run_pending(loop); // 执行 pending 回调
    uv__run_idle(loop); // 处理空转事件
    uv__run_prepare(loop); // 处理准备事件

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop); // 计算 Poll I/O 阶段阻塞时间,0则不阻塞

    uv__io_poll(loop, timeout); // 处理Poll I/O,timeout是 epoll_wait 的等待时间

    uv__metrics_update_idle_time(loop);

    uv__run_check(loop); // 处理复查事件
    uv__run_closing_handles(loop); // 扫尾处理

    // 是否还有活跃事件,有则继续下一轮事件循环
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }
  // 重置状态
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

// 是否有活跃事件
static int uv__loop_alive(const uv_loop_t* loop) {
  return uv__has_active_handles(loop) ||
         uv__has_active_reqs(loop) ||
         !QUEUE_EMPTY(&loop->pending_queue) ||
         loop->closing_handles != NULL;
}

根据官方图说明如下:

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

while 循环主要是处理各个阶段的事件,逻辑为

  1. uv__update_time:更新 loop 的最后处理时间(这个时间的用处之一是 setTimeout() 会以该时间为准,判断setTimeout()是否已经过期)
  2. uv__run_timers:执行定时器 setTimeout()事件,大概流程就是在存放定时器的小根堆里遍历出已过期的定时器,并依次执行定时器的回调
  3. uv__run_pending:遍历并执行 I/O 事件结束后(完成或失败),丢进 pending 队列等待后续处理的事件对应的回调(如 TCP 进行连接时连接失败产生的回调)
  4. uv__run_idle:遍历并执行空转(idle)事件
  5. uv__run_prepare:遍历并执行准备(prepare)事件
  6. uv_backend_timeout:获取还没过期且是最近要过期的定时器的时间间隔,这个时间是 Poll I/O 阻塞等待的时间间隔
  7. uv__io_poll:根据 epollkqueueI/O 多路复用机制,去监听等待 I/O 事件触发,并以上一步 uv_backend_timeout 获取的时间间隔作为最长等待时间,若超时还没有 I/O 事件触发,则直接取消此次等待,因为时间到了还没有 I/O 事件触发,说明还有定时器要执行,且定时器触发时间到了,那 libuv 就要去处理下一轮定时器了,不能一直阻塞在等待 I/O

了解了事件循环系统,根据事件循环系统会循环检查uv__update_time()中会判断有没有定时器超时,有则执行传入的 cb回调函数,在Node.js 中此回调函数实际是 EnvironmentRunTimers() 方法。代码如下

// node-18.15.0/deps/uv/src/timer.c
// 找出小顶堆中超时的定时器节点,并执行里面的回调
void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
    // 找出小顶堆的超时节点,即堆顶节点
    heap_node = heap_min(timer_heap(loop));
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    // 当前堆顶节点时间大于当前时间,说明后面的节点也没有超时
    if (handle->timeout > loop->time)
      break;

    // 移除计时器节点
    uv_timer_stop(handle);
    // 如果设置了 repeat 则重新插入小顶堆,等待下次超时时执行
    uv_timer_again(handle);
    // 执行定时器的回调,把handle传到C++层Environment的RunTimers方法
    handle->timer_cb(handle);
  }
}

RunTimers的作用:是经过一系列逻辑后,调用 js 侧的定时回调函数,在那个函数中才会去找对应的 Timeout 并触发对应的真实回调函数,即该 RunTimers() 最终达到的是一个 Dispatcher 的效果。代码如下:

// node-18.15.0/src/env.cc
void Environment::RunTimers(uv_timer_t* handle) {
  // 因为RunTimers是静态方法,所以无法通过this来表示Environment实例,此处用了 Environment::from_timer_handle() 静态方法来通过对应 uv_timer_t 获取其所属的 Environment实例
  Environment* env = Environment::from_timer_handle(handle); 
  ...

  Local<Object> process = env->process_object(); // 获取process对象
  ...

  // 获取一个事先注册到 Environment 中的 js 侧的定时器回调函数
  // 这里是env里的timers_callback_function函数,在Node.js中是processTimers()
  Local<Function> cb = env->timers_callback_function(); 

  MaybeLocal<Value> ret;
  Local<Value> arg = env->GetNow();
  do {
    TryCatchScope try_catch(env);
    try_catch.SetVerbose(true);

    // 执行回调函数,对应js层的processTimers函数,该函数的返回值是下一次要执行的定时器的过期时间;可能是正数,也可能是负数
    ret = cb->Call(env->context(), process, 1, &arg); 
  } while (ret.IsEmpty() && ...);

  ...

  //  从js层拿到的下一次超时时间,下面会调用ScheduleTimer重启定时器
  int64_t expiry_ms =
      ret.ToLocalChecked()->IntegerValue(env->context()).FromJust();

  uv_handle_t* h = reinterpret_cast<uv_handle_t*>(handle);
  if (expiry_ms != 0) { // 不等于0说明还有定时器要执行,只是还没到过期时间
    
    // 计算下一次真正要触发的时间,这里只用expiry_ms的绝对值
    int64_t duration_ms =
        llabs(expiry_ms) - (uv_now(env->event_loop()) - env->timer_base());

    // 重启定时器
    env->ScheduleTimer(duration_ms > 0 ? duration_ms : 1);
}

env->timers_callback_function()就是processTimers

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

1.3.4 C++ 侧回调函数对应的 js 侧函数:processTimers

从上面我们知道在 Environment::RunTimers 方法里会取得回调函数并执行,此时回调函数是 envtimers_callback_function() 函数,下面来看下这个回调函数是什么?

// node-18.15.0/lib/internal/bootstrap/node.js
const { setupTimers } = internalBinding('timers');
  const {
    processImmediate,
    processTimers,
  } = internalTimers.getTimerCallbacks(runNextTicks);

  setupTimers(processImmediate, processTimers);

setupTimers 是内置模块 timers.cc 的方法,如下可以看出 setupTimers 注册了两个个函数,分别是 processImmediate()processTimers()

// node-18.15.0/src/timers.cc
void SetupTimers(const FunctionCallbackInfo<Value>& args) {
  CHECK(args[0]->IsFunction());
  CHECK(args[1]->IsFunction());
  auto env = Environment::GetCurrent(args);

  // env的immediate_callback_function()是js层的processImmediate函数
  env->set_immediate_callback_function(args[0].As<Function>());
  // env的timers_callback_function()是js层的processTimers函数
  env->set_timers_callback_function(args[1].As<Function>()); 
}

知道了回调函数是processTimers(),接着看下该函数的逻辑

Node.js 中processTimers源码如下:

// node-18.15.0/lib/internal/timers.js
// 该函数返回值是下一次过期时间
function processTimers(now) {
  nextExpiry = Infinity;

  let list;
  let ranAtLeastOneList = false;

  // 循环执行到没有定时器了,或者没有即将要执行的定时器为止
  while ((list = timerListQueue.peek()) != null) {
    if (list.expiry > now) { // 链表过期时间大于当前时间,表示没有过期的定时器要执行
      nextExpiry = list.expiry;
      return timeoutInfo[0] > 0 ? nextExpiry : -nextExpiry; 
    }

    // 接下去执行链表过期时间小于等于当前时间的逻辑

    if (ranAtLeastOneList)
      runNextTicks();
    else
      ranAtLeastOneList = true;

    listOnTimeout(list, now); // 执行定时器
  }

  return 0; // 等于0说明没有定时器要执行了
}

看到这里就回到了,👆上面画的Map图

  1. timerListQueue 优先队列中获取首个元素TimersList链表,首元素也就是最近要过期的那条链表

  2. 判断一下链表的过期时间是否大于当前时间 now。如果大于当前时间,说明这个TimersList链表中的所有 Timeout还不能执行,于是将 nextExpiry 用该链表的过期时间替换。

  3. 如果链表过期时间小于等于当前时间,则说明在当前状态下,该 TimersList链表中Timeout 是存在需要被触发的。由于时间的不精确性,如果时间循环卡了一下,导致一下子过了好几毫秒,而在这之前有好几条链表都会过期,所以就需要在一次 processTimers 里面持续执行 Timeout 直到获取的 Timeout 未过期。

  4. 在执行 Timeout 之前,先判断一下当前的 while 里面是不是已经执行过至少一个 Timeout 了。若未执行过,则直接执行;若已经执行过,则在需要执行runNextTicks() ,处理执行过程中可能会产生微任务。这个 runNextTicks() 里面主要做的事情就是去处理微任务、Promiserejection 等,

  5. 可以触发 js 侧的 Timeout 了,触发的逻辑是在 listOnTimeout()

  6. 接下去就开始下一条循环,从链表中再获取下一条 Timeout 重复上面的操作。如果链表空了,则退出,退出之后在外层循环实际上就是 Node.js 继续从优先队列中获取再继续判断了

例子:在下面会形成两个链,在TimersList 1链表中会产生微任务,在运行取队列执行过一次队列就需要去执行 runNextTicks()  所以TimersList 1队列中产生的微任务是先执行的

TimersList 2 列表后执行

setTimeout(() => {
  console.log(1);
  Promise.resolve(3).then(res=>console.log(res));
}, 1);

setTimeout(() => {
  console.log(2);
}, 2);

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

根据调试。可以清楚的看到取出队列 TimersList2 时先去执行微任务

所以执行结果就是1,3,2

js 侧定时器的执行:listOnTimeout

// node-18.15.0/lib/internal/timers.js
// list就是某个超时时间的链表; now就是当前 Tick 的时间
function listOnTimeout(list, now) { 
    const msecs = list.msecs;

    let ranAtLeastOneTimer = false;
    let timer;

    // L.peek(list)从一个 list 中获取首个元素
    while ((timer = L.peek(list)) != null) {
      const diff = now - timer._idleStart;

      // 还没过期
      if (diff < msecs) {
        list.expiry = MathMax(timer._idleStart + msecs, now + 1);
        list.id = timerListId++;
        timerListQueue.percolateDown(1); // 重排优先队列
        debug('%d list wait because diff is %d', msecs, diff);
        return;
      }

      // 下面执行过期的逻辑
      if (ranAtLeastOneTimer)
        runNextTicks();
      else
        ranAtLeastOneTimer = true;

      // The actual logic for when a timeout happens.
      L.remove(timer); // 把定时器从链表移除

      ...

      let start;
      if (timer._repeat)
        start = getLibuvNow();

      try {
        const args = timer._timerArgs;
        if (args === undefined)
          timer._onTimeout(); // 执行超时回调,回调没有参数
        else
          // 执行超时回调,_onTimeout就是js层定时器要执行的回调函数,此时回调有参数
          ReflectApply(timer._onTimeout, timer, args); 
      } finally {
        if (timer._repeat && timer._idleTimeout !== -1) {
          timer._idleTimeout = timer._repeat;
          insert(timer, timer._idleTimeout, start); // 要重复执行,重新放入链表,setInterval场景
        } else if (...) {
          ...
            if (timer[kRefed])
                timeoutInfo[0]--; // 执行完后,引用数减少1
          ... 
        }
      }
    }

listOnTimeout

主要逻辑如下

  • 首先是一个 while 通过 L.peek() 不断从链表 TimersList 中获取首个元素,首个元素就是最早过期的元素

因为链表所有元素的 timeout 超时时间相同,任何一个 Timeout 都是按时序插入到,所以在较后面时间插入的一定是前面时间插入的晚超时,这其实是一个有序列表,按超时时间点从早到晚

  • while 每次循环中,先判断一下拿到的 Timeout 实例是否应被触发,即是否过期。如果没有过期,则进入 if(diff < msecs) 分支。将当前 Timeout 实例对应的过期时间作为当前链表整体的过期时间,并重排优先队列;timerListQueue.percolateDown(1) 的意思是:对优先队列第一个元素进行下滤操作。因为这时它的 expiry 被修改了,不一定是最早过期的链表了,需要下滤以得到新的最早链表。下滤过后,退出该函数,回到之前的 processTimers(),进入下一个循环,即再拿出新的最早过期链表,并判断有没有过期,然后做后续逻辑
  • 若是当前 Timeout 过期了,即该定时可以被执行,即走一遍 runNextTicks() 的逻辑,然后从链表中将当前 Timeout 移除,runNextTicks() ,可以处理 TimersList 链表中产生的微任务

例子:

setTimeout(() => {
  console.log(1);
  Promise.resolve(3).then(res=>console.log(res));
}, 1);

setTimeout(() => {
  console.log(2);
}, 1);

这是同一个链表1,执行顺序1,3,2

下图可以看到调试流程

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

根据上面的流程可以发现一些问题:因为是取过气时间最短的TimersList链表,这条链表上可能存在很多个TimeOut,当主任务占用过多时间时,就会出现后过期的先执行

setTimeout(() => {
  console.log(1);
}, 10);

setTimeout(() => {
  console.log(2);
}, 15);

let now = Date.now();
while (Date.now() - now < 100) {
  // 100ms的循环
}

setTimeout(() => {
  console.log(3);
}, 10);

now = Date.now();
while (Date.now() - now < 100) {
  // 100ms的循环
}

本来应该执行顺序是1,2,3

由于while循环阻塞了,导致Timeout都过期了,while循环

timerListQueue.peek()取数据TimersList10这条链表先执行,TimersList15这条链表后执行,过期时间都到了,所以TimersList10 一次循环完成,执行顺序 1,3,2了!

setTimeOut看完,顺便了解下setInterval();

2 定时器 setInterval()

Node.js 中 setInterval()的源码如下

// node-18.15.0/lib/timers.js
function setInterval(callback, repeat, arg1, arg2, arg3) {
  validateFunction(callback, 'callback');

  let i, args;
  switch (arguments.length) {
    // fast cases
    case 1:
    case 2:
      break;
    case 3:
      args = [arg1];
      break;
    case 4:
      args = [arg1, arg2];
      break;
    default:
      args = [arg1, arg2, arg3];
      for (i = 5; i < arguments.length; i++) {
        // Extend array dynamically, makes .apply run much faster in v6.0.0
        args[i - 2] = arguments[i];
      }
      break;
  }

  const timeout = new Timeout(callback, repeat, args, true, true);
  insert(timeout, timeout._idleTimeout);
  return timout;

}

可以看到,实际上 setInterval() 的执行机制跟 setTimeout() 完全相同,setInterval()setTimeout()的区别只是在实例化 Timeout 时,第4个参数 isRepeat 的值不一样,setTimeout()false 表示不需要重复执行,setInterval()true 表示需要重复执行。

因为isRepeat是true 在回调执行 listOnTimeout 时

// node-18.15.0/lib/internal/timers.js
// list就是某个超时时间的链表; now就是当前 Tick 的时间
function listOnTimeout(list, now) { 
    const msecs = list.msecs;

    let ranAtLeastOneTimer = false;
    let timer;

    // L.peek(list)从一个 list 中获取首个元素
    while ((timer = L.peek(list)) != null) {
      const diff = now - timer._idleStart;

      // 还没过期
      if (diff < msecs) {
        list.expiry = MathMax(timer._idleStart + msecs, now + 1);
        list.id = timerListId++;
        timerListQueue.percolateDown(1); // 重排优先队列
        debug('%d list wait because diff is %d', msecs, diff);
        return;
      }

      // 下面执行过期的逻辑
      if (ranAtLeastOneTimer)
        runNextTicks();
      else
        ranAtLeastOneTimer = true;

      // The actual logic for when a timeout happens.
      L.remove(timer); // 把定时器从链表移除

      ...

      let start;
      if (timer._repeat)
        start = getLibuvNow();

      try {
        const args = timer._timerArgs;
        if (args === undefined)
          timer._onTimeout(); // 执行超时回调,回调没有参数
        else
          // 执行超时回调,_onTimeout就是js层定时器要执行的回调函数,此时回调有参数
          ReflectApply(timer._onTimeout, timer, args); 
      } finally {
        if (timer._repeat && timer._idleTimeout !== -1) {
          timer._idleTimeout = timer._repeat;
          insert(timer, timer._idleTimeout, start); // 要重复执行,重新放入链表,setInterval场景
        } else if (...) {
          ...
            if (timer[kRefed])
                timeoutInfo[0]--; // 执行完后,引用数减少1
          ... 
        }
      }
    }

再看下代码的最后执行finally,重新insert一天新的数据,进入链表

3. setImmediate()

Node.js 中 setImmediate() 的源码:

// node-18.15.0/lib/timers.js
function setImmediate(callback, arg1, arg2, arg3) {
  validateFunction(callback, 'callback');

  let i, args;
  switch (arguments.length) {
    case 1:
      break;
    case 2:
      args = [arg1];
      break;
    case 3:
      args = [arg1, arg2];
      break;
    default: // 跟setTimeout一样,处理多余3个的参数
      args = [arg1, arg2, arg3];
      for (i = 4; i < arguments.length; i++) {
        args[i - 1] = arguments[i];
      }
      break;
  }

  // 直接返回Immediate实例
  return new Immediate(callback, args);
}

返回Immediate,看下Immediate相关代码:

// node-18.15.0/lib/internal/timers.js
class ImmediateList {
  constructor() { 
   this.head = null;
    this.tail = null; 
 } 
 append(item) { 
   if (this.tail !== null) { 
     this.tail._idleNext = item;
     item._idlePrev = this.tail;
    } else {
      this.head = item; 
   }
 this.tail = item; 
 }
}
const immediateQueue = new ImmediateList();// 存储immediate的链表
class Immediate {
  constructor(callback, args) {
    ...
    this[kRefed] = false; // ref标记为false

    this.ref(); // 调用ref()

    // Immediate 链表的节点个数,包括 ref 和 unref 状态
    immediateInfo[kCount]++;

    // 加入链表中
    immediateQueue.append(this);
  }

  // 打上 ref 标记,往 Libuv 的 idle 链表插入一个激活状态的节点,如果还没有的话
  ref() {
    if (this[kRefed] === false) {
      this[kRefed] = true; // ref标记为true

      // immediateInfo[kRefCount] 为 0,即 ref 个数为0,则说明还没有往 libuv 的 idle 队列里插入idle 节点,则把immediate_idle_handel插入idle空转队列,只是标记作用,idle并不会执行immediate
      if (immediateInfo[kRefCount]++ === 0)
        toggleImmediateRef(true); // 对应c++侧timers.cc的toggleImmediateRef
    }
    return this;
  }
  ......
}

执行ref(),向immediateQueue队列中添加一条数据

由代码可知,ref() 调用了 C++ 侧 src/timers.cc 中的 ToggleImmediateRef()函数,代码如下

// node-18.15.0/src/timers.cc
void ToggleImmediateRef(const FunctionCallbackInfo<Value>& args) { 
  // 调用了env的ToggleImmediateRef函数
  Environment::GetCurrent(args)->ToggleImmediateRef(args[0]->IsTrue())
}

由代码可知,timers.cc 里又进一步调用了 C++ 侧 src/env.cc 中的ToggleImmediateRef()函数,代码如下

void Environment::ToggleImmediateRef(bool ref) {
  if (started_cleanup_) return;

  if (ref) { // 插入 idle 队列,设置为 active 状态,防止在 Poll IO 阶段阻塞和事件循环的退出 ,回调是一个空的匿名函数
    uv_idle_start(immediate_idle_handle(), [](uv_idle_t*){ }); // libuv的uv_idle_start函数
  } else {
    uv_idle_stop(immediate_idle_handle()); // 停止immediate_idle_handle句柄
  }
}

这个时候开始进入libuv:

libuv 的uv_idle_start()如下,

如果 libuv 的idle未激活过则会激活

// node-18.15.0/deps/uv/src/unix/loop-watcher.c //通过C语言的##连接符统一用宏定义了,并且在编译器预处理的时候产生对应的函数代码

int uv_idle_start(uv_idle_t* handle, uv_idle_cb cb) { 
    /* 如果已经执行过start函数则直接返回 */
    if (uv__is_active(handle)) return 0;

    /* 回调函数不允许为空 */ 
    if (cb == NULL) return UV_EINVAL; 

    /* 把handle插入loop中idle_handles队列,loop有prepare,idle和check三个队列 */
    QUEUE_INSERT_HEAD(&handle->loop->idle_handles, &handle->queue);

    /* 指定回调函数,在事件循环迭代的时候被执行 */
    handle->idle_cb = cb; 

    /* 启动idle handle,设置UV_HANDLE_ACTIVE标记并且将loop中的handle的active计数加一,
       init的时候只是把handle挂载到loop,start的时候handle才处于激活态 */
    uv__handle_start(handle);
    return 0;
}

传入 uv_idle_start的函数是个空函数 需要再看下事件循环机制:

uv__run_timers执行完之后,接着看下 uv__run_idle(loop); // 处理空转事件

// node-18.15.0/deps/uv/src/unix/loop-watcher.c //通过C语言的##连接符统一用宏定义了,并且在编译器预处理的时候产生对应的函数代码
void uv__run_idle(uv_loop_t* loop) { 
    uv_idle_t* h;
    QUEUE queue;
    QUEUE* q;

    /* 把loop的idle_handles队列中所有节点摘下来挂载到queue变量 */
    QUEUE_MOVE(&loop->idle_handles, &queue);

    /* while循环遍历队列,执行每个节点里面的函数 */
    while (!QUEUE_EMPTY(&queue)) {

        /* 取下当前待处理的节点 */
        q = QUEUE_HEAD(&queue);

        /* 取得该节点对应的整个结构体的基地址 */
        h = QUEUE_DATA(q, uv_idle_t, queue);

        /* 把该节点移出当前队列 */
        QUEUE_REMOVE(q); 

        /* 重新插入loop->idle_handles队列 */
        QUEUE_INSERT_TAIL(&loop->idle_handles, q); 

        /* 执行对应的回调函数 */
        h->idle_cb(h); 
    } 
}

因为传入的回调函数是个空函数,uv__run_idle 并没有真正运行setImmediate 的回调,所以这个阶段叫处理空转事件,那什么时候处理setImmediate回调呢???

定时任务是需要等待时间的,Poll I/O可以考虑定时任务等待时间,提高效率,事件循环 Poll I/O 阶段计算阻塞事件时,不会考虑 check 复查阶段的任务,但会考虑 idle 空转阶段的任务,所以当插入第一个 Immediate 任务时,Node.js 会把 immediate_idle_handle 插入 idle 空转队列中(idle阶段并不会去执行 Immediate 实例),插进去只起到标记作用,表示有任务处理,不能阻塞 Poll I/O 阶段,

Immediate是立即的意思,需要立即执行,有了idle,在 Poll I/O 阶段就不能阻塞,

timeout = uv_backend_timeout(loop); // 计算 Poll I/O 阶段阻塞时间,0则不阻塞    uv__io_poll(loop, timeout); // 处理Poll I/O,timeout是 epoll_wait 的等待时间

查看上面事件循环代码阻塞的最大等待时间是根据uv__backend_timeout()函数的返回值判断的,其规则如下

  • 如果事件循环中存在空转事件,此函数会返回 0,即不阻塞 Poll I/O等待,可以马上开始进入下一轮轮回
  • 如果没有空转事件,则返回下一个最快超时的 Timeout 定时器的过期时间,此过期时间会做为事件循环的最大阻塞时间(因为有即将超时的定时器,说明事件循环中还有定时器需要处理,不能一直阻塞)

uv_backend_timeout() 源码如下

// node-18.15.0/deps/uv/src/unix/core.c
static int uv__backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag == 0 &&
      /* uv__loop_alive(loop) && */
      (uv__has_active_handles(loop) || uv__has_active_reqs(loop)) &&
      QUEUE_EMPTY(&loop->pending_queue) &&
      QUEUE_EMPTY(&loop->idle_handles) &&
      loop->closing_handles == NULL)
    return uv__next_timeout(loop);
  return 0;
}

int uv_backend_timeout(const uv_loop_t* loop) {
  if (QUEUE_EMPTY(&loop->watcher_queue))
    return uv__backend_timeout(loop);
  /* Need to call uv_run to update the backend fd state. */
  return 0;
}

3.1 空转事件(idle节点)存在的意义:是为了标记是否有 immediate 任务需要处理

(为了让 Poll for I/O 阶段不阻塞);因为有 immediate 任务的话就事件循环就不能一直阻塞在 Poll I/O 阶段等待 I/O,并且不能退出事件循环。

接着查看  uv__run_check(loop); // 处理复查事件  这个阶段:

void uv__run_check(uv_loop_t* loop) { 
    uv_check_t* h;
    QUEUE queue;
    QUEUE* q;

    /* 把loop的check_handles队列中所有节点摘下来挂载到queue变量 */
    QUEUE_MOVE(&loop->check_handles, &queue);

    /* while循环遍历队列,执行每个节点里面的函数 */
    while (!QUEUE_EMPTY(&queue)) {

        /* 取下当前待处理的节点 */
        q = QUEUE_HEAD(&queue);

        /* 取得该节点对应的整个结构体的基地址 */
        h = QUEUE_DATA(q, uv_check_t, queue);

        /* 把该节点移出当前队列 */
        QUEUE_REMOVE(q); 

        /* 重新插入loop->check_handles队列 */
        QUEUE_INSERT_TAIL(&loop->check_handles, q); 

        /* 执行对应的回调函数 */
        h->check_cb(h); 
    } 
}

会循环调用check_handles队列回调,chek_handles中的值哪里来??

在 Node.js 初始化时会调用 InitializeLibuv() 初始化 libuv 的一些东西,里面调用了 libuv 的uv_check_start()。uv_check_start的作用就是激活 immediate check handle 句柄,并设置回调为CheckImmediate()。还调用了 uv_idle_init() 初始化一个 idle 阶段的节点

// node-18.15.0/src/env.cc
// 在 Node.js 初始化时调用
void Environment::InitializeLibuv() {
    ...
    // 初始化 immediate 相关的 handle   
    CHECK_EQ(0, uv_check_init(event_loop(), immediate_check_handle()));

    // 修改 immediate_check_handle 状态为 unref,避免没有任务时,影响事件循环的退出
    // immediate_check_handle是uv_check_t类型,它继承uv_handle_t
    uv_unref(reinterpret_cast<uv_handle_t*>(immediate_check_handle()));

    // 激活immediate check handle,设置CheckImmediate回调  
    CHECK_EQ(0, uv_check_start(immediate_check_handle(), CheckImmediate));

    // 这里初始化一个 idle 阶段的节点
    CHECK_EQ(0, uv_idle_init(event_loop(), immediate_idle_handle()));
    ...
}

其中uv_check_start 就是向chek_handles队列添加值

int uv_check_start(uv_check_t* handle, uv_check_cb cb) { 
    /* 如果已经执行过start函数则直接返回 */
    if (uv__is_active(handle)) return 0;

    /* 回调函数不允许为空 */ 
    if (cb == NULL) return UV_EINVAL; 

    /* 把handle插入loop中check_handles队列,loop有prepare,check和check三个队列 */
    QUEUE_INSERT_HEAD(&handle->loop->check_handles, &handle->queue);

    /* 指定回调函数,在事件循环迭代的时候被执行 */
    handle->check_cb = cb; 

    /* 启动check handle,设置UV_HANDLE_ACTIVE标记并且将loop中的handle的active计数加一,
       init的时候只是把handle挂载到loop,start的时候handle才处于激活态 */
    uv__handle_start(handle);
    return 0;
}

到这里就可以知道回调函数调用的是CheckImmediate

3.2 libuv 侧回调函数对应的 C++ 侧函数:CheckImmediate()

事件循环在执行 check 复查阶段的任务时,会执行注册好的CheckImmediate()回调函数,其作用类似于 setTimeout() 中的 RunTimers(),代码如下

// node-18.15.0/src/env.cc
void Environment::CheckImmediate(uv_check_t* handle) {
  ...
  // 没有 Immediate 任务需要处理    
  if (env->immediate_info()->count() == 0 || !env->can_call_into_js())
    return;

  do { // 执行 JS 层回调 immediate_callback_function 
    MakeCallback(env->isolate(),
                 env->process_object(),
                 env->immediate_callback_function(), // 执行在初始化时就注册好的processImmediate()函数
                 0,
                 nullptr,
                 {0, 0}).ToLocalChecked();
  } while (env->immediate_info()->has_outstanding() && env->can_call_into_js());

  // 所有 immediate 节点都处理完了,置 idle 阶段对应节点为非激活状态,允许 Poll IO 阶段阻塞和事件循环退出
  if (env->immediate_info()->ref_count() == 0)
    env->ToggleImmediateRef(false);
}

env->immediate_callback_function()就是js侧的processImmediate

3.3 C++ 侧回调函数对应的 js 侧函数:processImmediate()

js 侧 processImmediate() 函数的作用:就是用于处理 jsimmediate 链表中的Immediate 实例,并执行 setImmediate() 中的回调函数。

注册 processImmediate() 函数

Node.js 在启动时,会注册 processImmediate() 函数到 envimmediate_callback_function() 中(类似 setTimeout() 中的 processTimers()),源码如下

// node-18.15.0/lib/internal/bootstrap/node.js
const { setupTimers } = internalBinding('timers');
  const {
    processImmediate,
    processTimers,
  } = internalTimers.getTimerCallbacks(runNextTicks);
  setupTimers(processImmediate, processTimers); // `setupTimers`是内置模块`timers.cc`的方法
void SetupTimers(const FunctionCallbackInfo<Value>& args) {
  CHECK(args[0]->IsFunction());
  CHECK(args[1]->IsFunction());
  auto env = Environment::GetCurrent(args);

  // env的immediate_callback_function()是js层的processImmediate函数
  env->set_immediate_callback_function(args[0].As<Function>());
  // env的timers_callback_function()是js层的processTimers函数
  env->set_timers_callback_function(args[1].As<Function>()); 
}

processImmediate() 的解析

processImmediate()的源码如下

// node-18.15.0/lib/internal/timers.js
const immediateQueue = new ImmediateList();
...
const outstandingQueue = new ImmediateList();
...

function processImmediate() {
    const queue = outstandingQueue.head !== null ?
      outstandingQueue : immediateQueue;
    let immediate = queue.head; // 赋给新的链表

    // 防重入
    // Clear the linked list early in case new `setImmediate()`
    // calls occur while immediate callbacks are executed
    if (queue !== outstandingQueue) { // 说明当前queue是immediateQueue
      queue.head = queue.tail = null;
      immediateInfo[kHasOutstanding] = 1;
    }

    let prevImmediate;
    let ranAtLeastOneImmediate = false;
    while (immediate !== null) { // 遍历immediate队列
      if (ranAtLeastOneImmediate)
        runNextTicks();
      else
        ranAtLeastOneImmediate = true;
      if (immediate._destroyed) {
        outstandingQueue.head = immediate = prevImmediate._idleNext;
        continue;
      }

      immediate._destroyed = true;

      immediateInfo[kCount]--;
      if (immediate[kRefed])
        immediateInfo[kRefCount]--;
      immediate[kRefed] = null;

      prevImmediate = immediate;

      ......

      try {
        const argv = immediate._argv;
        if (!argv)
          immediate._onImmediate();
        else
          immediate._onImmediate(...argv); // 执行回调函数
      } finally { // 注意这里执行_onImmediate时没有catch
        immediate._onImmediate = null;

        ......

        outstandingQueue.head = immediate = immediate._idleNext;
      }
      ......
    }

    if (queue === outstandingQueue)
      outstandingQueue.head = null;
    immediateInfo[kHasOutstanding] = 0;
  }

主要逻辑

  • 首先,先判断 outstandingQueue 是不是为空,若为空,则将 queue 赋值为 immediateQueue,否则赋值为 outstandingQueue。然后将 queue 赋给 immediate,后面的遍历就是遍历这个 immediate (这里另一条链表就是 immediate)
  • 下面这个 if(queue !== outstandingQueue){} 就是防重入的逻辑。如果 queue 不等于 outstandingQueue,就说明它是 immediateQueue。如果当前遍历的是 immediateQueue,那么就清空这个 immediateQueue(将其首尾都赋空)。这样,在下面遍历时是遍历着已经拿过来的 immediate,在这之间新插入的 Immediate 是插入到已被赋空的 immediateQueue 链表了,两条链表毫无关系,不会再出现死循环
  • 接下去 while 循环遍历 immediate 队列
  • 调用 immediate._onImmediate 执行 Immediate 的回调函数

所以,processImmediate() 的大概流程就是:遍历这条 ImmediateList 链表,并逐个执行其回调函数 _onImmediate

outstandingQueue 队列的作用

  • 首先,执行 Immediate_onImmediate() 函数时,Node.js 用的是 try-finally,并没有 catch。这样会导致,如果 setImmediate() 中的回调函数抛错了,会触发 uncaughtException 事件。这时,如果用户监听了该错误事件并处理了,那么 Node.js 会继续执行,但是这个 Immediate 链表的遍历过程就被中断了,后面接下去再执行的话,就需要用到 outstandingQueue

  • outstandingQueue 起到了保留现场的作用。每次遍历执行一次 Immediate_onImmediate() 后的 finally 中,都记录一下 outstandingQueue 的首元素为当前执行完的 Immediate 的下一个元素。这样,j就算抛错了,也记录下来了现场。所以在 processImmediate() 函数的前面能看到,queue 的赋值时,outstandingQueue 是否为空。若不为空则说明是记录下现场后,有抛错,那么从之前的现场继续开始执行

到这里就知道了,uv__run_check 执行的ImmediateList 链表链表就是setImmediate() 回调函数,每执行一个都要去处理中间产生的微任务runNextTicks();

执行下面代码:

setImmediate(function () {  console.log('setImmediate执行');});

调试过程清楚的看到执行流程,与分析的一致:

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

4. 微任务 queueMicrotask()

process.nextTick() 源码

process.nextTick() 函数是在 Node.js 启动过程中把 nextTick() 挂载到 process 对象上的

// node-18.15.0/lib/internal/bootstrap/node.js
const { nextTick, runNextTicks } = setupTaskQueue();
process.nextTick = nextTick;

nextTick() 的源码如下

// node-18.15.0/lib/internal/process/task_queues.js
const queue = new FixedQueue();
...

function nextTick(callback) {
  validateFunction(callback, 'callback');

  if (process._exiting)
    return;

  // 处理回调的参数
  let args;
  switch (arguments.length) {
    case 1: break;
    case 2: args = [arguments[1]]; break;
    case 3: args = [arguments[1], arguments[2]]; break;
    case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
    default:
      args = new Array(arguments.length - 1);
      for (let i = 1; i < arguments.length; i++)
        args[i - 1] = arguments[i];
  }

  if (queue.isEmpty())
    setHasTickScheduled(true); // 队列为空,设置tick中有回调标识

  ...
  const tickObject = {
    ...
    callback, // 回调函数
    args // 回调函数的参数
  };
  ...
  queue.push(tickObject); // 把tick对象放入队列
}

// 存储nextTick的队列
module.exports = class FixedQueue {
  constructor() {
    this.head = this.tail = new FixedCircularBuffer();
  }

  isEmpty() {
    return this.head.isEmpty();
  }

  push(data) {
    if (this.head.isFull()) {
      // Head is full: Creates a new queue, sets the old queue's `.next` to it,
      // and sets it as the new main queue.
      this.head = this.head.next = new FixedCircularBuffer();
    }
    this.head.push(data);
  }

  shift() {
    const tail = this.tail;
    const next = tail.shift();
    if (tail.isEmpty() && tail.next !== null) {
      // If there is another queue, it forms the new tail.
      this.tail = tail.next;
      tail.next = null;
    }
    return next;
  }
};

主要逻辑

  • nextTick() 用了一个 queue 去存储 Node.js 的当前 Tick 中需要执行的 process.nextTick() 回调们

queue 是一个由一个或多个环形队列拼接而成的链表,可以把它当做是一个大链表

  • 若该 queue 为空,则通过 setHasTickScheduled(true),设置一个标识,即 tickInfo[kHasTickScheduled] 为1,这个标识表示“Tick 中有回调”

  • 设置完该标识位后,构建一个 Tick 的回调函数相关对象,将其插入 queue大链表

processTicksAndRejections()

process.nextTick() 的回调是在processTicksAndRejections()中执行的,其源码如下:

// node-18.15.0/lib/internal/process/task_queues.js
function processTicksAndRejections() {
  let tock;

  do { 
    // while 循环拿队列的tick对象,拿到回调函数callback并执行
    while ((tock = queue.shift()) !== null) {
      ...
      try {
        const callback = tock.callback; // 拿到process.nextTick的回调函数
        if (tock.args === undefined) {
          callback(); // 执行回调函数
        } else {
          const args = tock.args;
          switch (args.length) {
            case 1: callback(args[0]); break;
            case 2: callback(args[0], args[1]); break;
            case 3: callback(args[0], args[1], args[2]); break;
            case 4: callback(args[0], args[1], args[2], args[3]); break;
            default: callback(...args);
          }
        }
      } finally {
        ...
      }

      ...
    }

    // while执行完后,执行微任务,可能又产生process.nextTick,所以需要有外层循环
    runMicrotasks();

  } while (!queue.isEmpty() || processPromiseRejections()); 

  // 当外层大循环也结束后,开始扫尾,比如将 `hasTickScheduled` 等设为 `false`
  setHasTickScheduled(false);
  setHasRejectionToWarn(false);
}

主要逻辑

  • 每个 Tick 里面,都逐个从 queue 大链表中拿 Tick 回调函数相关对象,直到拿完。每拿一个,都去跑一遍它的 callback (即 process.nextTick() 中传入的函数)

  • queue 都取完且执行完后,就是执行微任务的时机了 runMicrotasks()

  • while (!queue.isEmpty() || processPromiseRejections()) 外层循环的作用:因为 runMicrotasks() 执行完后,可能 queue 中又插入了process.nextTic(),或者 PromiseRejection,这个时候需要再跑一遍 queue 链表,然后再执行一遍微任务

processTicksAndRejections() 被调用的地方主要有2处

  1. setTimeout()setInterval()setImmediate() 等时序 API ,每次执行 runNextTick 方法时

  2. Node.js 内部一些 callback 函数执行之后也会执行微任务。因为在 InternalMakeCallback()中,它会在里面创建一个 InternalCallbackScope 实例,该实例在结束的时候(InternalCallbackScope::Close()),会执行微任务

执行时机1:runNextTick()

第一个执行时机是 setTimeout()setInterval()setImmediate() 等时序API在执行时,会执行 runNextTicks() ,里面会调用 processTicksAndRejections(),它里面会执行 process.nextTick()

runNextTicks()源码:

// node-18.15.0/lib/internal/process/task_queues.js
function runNextTicks() {
  // 队列中没有nextTick回调,则执行微任务
  if (!hasTickScheduled() && !hasRejectionToWarn())
    runMicrotasks(); // 跑微任务,可能里面会产生process.nextTick()

  // 注意,这个条件跟上面的一样的
  if (!hasTickScheduled() && !hasRejectionToWarn())
    return;

  // 有nextTick回调,则执行processTicksAndRejections
  // 里面会执行process.nextTick的任务,里面也会跑微任务
  processTicksAndRejections();
}

// 表示有process.nextTick()
function hasTickScheduled() {
  return tickInfo[kHasTickScheduled] === 1;
}

function setHasTickScheduled(value) {
  tickInfo[kHasTickScheduled] = value ? 1 : 0;
}

主要逻辑

  • 如果队列中没有 nextTick回调函数,则直接跑微任务

  • 第2个 if(!hasTickScheduled() && !hasRejectionToWarn()),跟前面的判断条件是一样的,若符合则直接返回

因为可能第1个条件在跑微任务时,里面有执行 process.nextTick(),这个时候跑完微任务后,hasTickScheduled() 可不再是 false 了,所以需要第2次判断

  • 最后执行 processTicksAndRejections() ,它里面也执行 process.nextTime(), 也会跑微任务runMicrotasks()

到这里可以看出,先执行process.nextTick 后执行微任务!

执行时机2:

在进入libuv事件循环之前,LoadEnvironment中会加载执行用户的脚本

// src/node_main_instance.cc
// Run的重载函数
void NodeMainInstance::Run(int* exit_code, Environment* env) {
  if (*exit_code == 0) {
    // 加载Node环境, 在里面初始化libuv:InitializeLibuv
    LoadEnvironment(env, StartExecutionCallback{});

    // 进入libuv事件循环
    *exit_code = SpinEventLoop(env).FromMaybe(1);
  }
  ......
}

// src/api/environment.cc
MaybeLocal<Value> LoadEnvironment(
    Environment* env,
    StartExecutionCallback cb) {
  // 初始化libuv
  env->InitializeLibuv(); 
  env->InitializeDiagnostics();

  // 加载Node执行环境
  // 最终会通过ExecuteBootstrapper执行internal/main目录下各种js文件
  // 如加载文件internal/main/run_main_module.js
  return StartExecution(env, cb);
}

Startexecution中执行用户脚本

// src/node.cc
MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
  InternalCallbackScope callback_scope(
      env,
      Object::New(env->isolate()),
      { 1, 0 },
      InternalCallbackScope::kSkipAsyncHooks);

  if (cb != nullptr) {
    EscapableHandleScope scope(env->isolate());

    if (StartExecution(env, "internal/bootstrap/environment").IsEmpty())
      return {};
    ...
    return scope.EscapeMaybe(cb(info));
  }

  if (!env->snapshot_deserialize_main().IsEmpty()) {
    return env->RunSnapshotDeserializeMain();
  }

  if (env->worker_context() != nullptr) {
    return StartExecution(env, "internal/main/worker_thread");
  }

  if (first_argv == "inspect") {
    return StartExecution(env, "internal/main/inspect");
  }
 ...

  if (!first_argv.empty() && first_argv != "-") {
    return StartExecution(env, "internal/main/run_main_module");
  }
  ...
  return StartExecution(env, "internal/main/eval_stdin");
}

 StartExecution(env, "internal/main/run_main_module"),加载编译执行用户脚本,可以通过调试查看

从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行

在执行用户脚本产生的微任务怎么处理呢??

在 StartExecution函数中InternalCallbackScope 这个函数存在析构函数,在执行完用户代码,退出函数作用域时,需要执行析构函数,清理工作;

下面看一下 InternalCallbackScope 析构函数的逻辑。 

InternalCallbackScope::~InternalCallbackScope() {
  Close();
}

void InternalCallbackScope::Close() {
 tick_callback->Call(context, process, 0, nullptr);
} 

在析构函数里会执行 tick_callback 函数。我们看看这个函数是什么。 

static void SetTickCallback(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args[0]->IsFunction());
  env->set_tick_callback_function(args[0].As<Function>());
} 

tick_callback 是由 SetTickCallback 设置的。

setTickCallback(processTicksAndRejections);

我们可以看到通过 setTickCallback 设置的这个函数是 processTicksAndRejections。

function processTicksAndRejections() {
  let tock;
  do {
    while (tock = queue.shift()) {
      const callback = tock.callback;
      callback();
    }
    runMicrotasks();
  } while (!queue.isEmpty() || processPromiseRejections());
} 

微任务会在执行用户代码后执行一次,再开启事件循环

最后看一个面试题,根据所学结果能不能写出运行结果:

console.log('1');

setImmediate(function () {
    console.log('13');
});

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    });
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5');
    });
}, 0);

process.nextTick(function() {
    console.log('6');
});

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8');
});

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    });
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12');
    });
}, 0);

能得到正确结果,说明完全理解了!

转载自:https://juejin.cn/post/7376097985380909092
评论
请登录