从Node源码搞懂setTimeout、setImmediate、nextTick、Promise. resolve执行
首先说明node 版本18.15.0
nodejs依赖了很多模块,v8和libuv是node最重要的两个依赖
- uv: 提供Nodejs访问操作系统各种特性的能力,包括文件系统、Socket等,io,包括进程、线程、信号、定时器、进程间通信等。
- v8: 将Js代码解释编译执行
简单了解下架构区别:
libuv架构图介绍:
有了整体了解之后,先开始讲解第一个
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
-
将各种元数据信息放到成员变量中(如超时时间、定时器开始时间、过期回调函数等)
-
_onTimeout
变量就是setTimeout()
时候传进来的回调函数,具体在 listOnTimeout 方法中被调用 -
isRepeat
表示Timeout
是否重复执行;setTimeout()
将该值是false
,即不重复;而setInterval()
该值是true
,即重复 -
incRefCount()
:用于激活libuv
的定时器
编写如下代码:
//settimeout.js
setTimeout(() => {
console.log('立即执行setTimeout');
}, 0);
执行 node --inspect-brk settimeout.js
打开浏览器输入:chrome://inspect/#devices 进入调试页面:
调试整个insert(timeout, timeout._idleTimeout)过程:
具体源代码如下:
// 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链表中
}
主要逻辑如下
-
MathTrunc
:截断超时时间,去掉小数,只保留整数部分 -
_idleStart
:记录当前定时器的开始时间 -
从
Map
获取当前超时时间对应的链表,链表如果不存在,则计算过期时间点expiry
并实例化一个新链表,这个过期时间就是这整条链表的最近超时时间
了,然后新链表存入Map
中,同时插入优先队列;最后判断是否需要启动定时器,通过scheduleTimer()
启动定时器 -
最后把当前定时器插入到链表的最后面
例如:不同时间设置多个定时器函数就可以形成下图:
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);
构成的结构如下图,
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()如下,可知调用了 libuv
的 uv_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:
libuv
的uv_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;
}
根据官方图说明如下:
while
循环主要是处理各个阶段的事件,逻辑为
uv__update_time
:更新loop
的最后处理时间(这个时间的用处之一是setTimeout()
会以该时间为准,判断setTimeout()
是否已经过期)uv__run_timers
:执行定时器setTimeout()
事件,大概流程就是在存放定时器的小根堆里遍历出已过期的定时器,并依次执行定时器的回调uv__run_pending
:遍历并执行I/O
事件结束后(完成或失败),丢进pending
队列等待后续处理的事件对应的回调(如 TCP 进行连接时连接失败产生的回调)uv__run_idle
:遍历并执行空转(idle
)事件uv__run_prepare
:遍历并执行准备(prepare
)事件uv_backend_timeout
:获取还没过期且是最近要过期的定时器的时间间隔,这个时间是Poll I/O
阻塞等待的时间间隔uv__io_poll
:根据epoll
、kqueue
等I/O
多路复用机制,去监听等待I/O
事件触发,并以上一步uv_backend_timeout
获取的时间间隔作为最长等待时间,若超时还没有I/O
事件触发,则直接取消此次等待,因为时间到了还没有I/O
事件触发,说明还有定时器要执行,且定时器触发时间到了,那libuv
就要去处理下一轮定时器了,不能一直阻塞在等待I/O
了解了事件循环系统,根据事件循环系统会循环检查uv__update_time()中会判断有没有定时器超时,有则执行传入的 cb回调函数
,在Node.js 中此回调函数实际是 Environment
的RunTimers()
方法。代码如下
// 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
1.3.4 C++ 侧回调函数对应的 js 侧函数:processTimers
从上面我们知道在 Environment::RunTimers
方法里会取得回调函数并执行,此时回调函数是 env
的timers_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图
-
从
timerListQueue
优先队列中获取首个元素TimersList链表,首元素也就是最近要过期的那条链表 -
判断一下链表的过期时间是否大于当前时间
now
。如果大于当前时间,说明这个TimersList链表中的所有Timeout
还不能执行,于是将nextExpiry
用该链表的过期时间替换。 -
如果链表过期时间小于等于当前时间,则说明在当前状态下,该 TimersList链表中
Timeout
是存在需要被触发的。由于时间的不精确性,如果时间循环卡了一下,导致一下子过了好几毫秒,而在这之前有好几条链表都会过期,所以就需要在一次processTimers
里面持续执行Timeout
直到获取的Timeout
未过期。 -
在执行
Timeout
之前,先判断一下当前的while
里面是不是已经执行过至少一个Timeout
了。若未执行过,则直接执行;若已经执行过,则在需要执行runNextTicks()
,处理执行过程中可能会产生微任务。这个runNextTicks()
里面主要做的事情就是去处理微任务、Promise
的rejection
等, -
可以触发 js 侧的
Timeout
了,触发的逻辑是在listOnTimeout()
中 -
接下去就开始下一条循环,从链表中再获取下一条
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);
根据调试。可以清楚的看到取出队列 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
下图可以看到调试流程
根据上面的流程可以发现一些问题:因为是取过气时间最短的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() 函数的作用:就是用于处理 js
侧 immediate
链表中的Immediate
实例,并执行 setImmediate()
中的回调函数。
注册 processImmediate()
函数
Node.js 在启动时,会注册 processImmediate()
函数到 env
的 immediate_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执行');});
调试过程清楚的看到执行流程,与分析的一致:
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()
,或者Promise
有Rejection
,这个时候需要再跑一遍queue
链表,然后再执行一遍微任务
processTicksAndRejections()
被调用的地方主要有2处
-
setTimeout()
、setInterval()
、setImmediate()
等时序 API ,每次执行runNextTick
方法时 -
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"),加载编译执行用户脚本,可以通过调试查看
在执行用户脚本产生的微任务怎么处理呢??
在 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