nodejs 14.0.0源码分析之setTimeout

2024-03-27 21:32

本文主要是介绍nodejs 14.0.0源码分析之setTimeout,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这一篇我们来看看nodejs是如何实现定时器的。14.0.0的nodejs对定时器模块进行了重构,之前版本的实现是用一个map,以超时时间为键,每个键对应一个队列。即有同样超时时间的节点在同一个队列。每个队列对应一个底层的一个节点(二叉堆里的节点),nodejs在时间循环的timer阶段会从二叉堆里找出超时的节点,然后执行回答,回调里会遍历队列,哪个节点超时了。14.0.0重构后,只使用了一个二叉堆的节点。我们看一下他的实现。
我们先看下定时器模块的组织结构。
在这里插入图片描述
下面我们继续看一下定时器模块的几个重要的数据结果。

1 TimersList

超时时间一样的会被放到同一个队列,这个队列就是由TimersList来管理。对应图中的list那个方框。

// expiry是超时时间的绝对值。用来记录队列中最快到期的节点的时间,msecs是超时时间的相对值(相对插入时的当前时间) 
function TimersList(expiry, msecs) {// 用于链表this._idleNext = this; this._idlePrev = this; this.expiry = expiry;this.id = timerListId++;this.msecs = msecs;// 在优先队列里的位置this.priorityQueuePosition = null;
}

2 优先队列

const timerListQueue = new PriorityQueue(compareTimersLists, setPosition)

nodejs用优先队列对所有1中的链表进行管理,优先队列本质是一个二叉堆(小根堆),每个链表在二叉堆里对应一个节点。根据1中,我们知道每个链表都保存链表中最快到期的节点的过期时间。二叉堆以该事件为依据,即最快到期的list对应二叉堆中的根节点。我们判断根节点是否超时,如果没有超时,说明整个二叉堆的节点都没有超时。如果超时了,就需要不断遍历堆中的节点。

3 超时时间和链表的映射

1中已经提到,超时时间一样的节点,会排在同一个链表中个,nodejs中用一个map保存了超时时间到链表的映射关系。

了解完定时器整体的组织和基础数据结构,我们可以开始进入真正的源码分析了。

我们直接从setTimeout函数开始。

function setTimeout(callback, after, arg1, arg2, arg3) {if (typeof callback !== 'function') {throw new ERR_INVALID_CALLBACK(callback);}let i, args;switch (arguments.length) {// fast casescase 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.0args[i - 2] = arguments[i];}break;}const timeout = new Timeout(callback, after, args, false, true);insert(timeout, timeout._idleTimeout);return timeout;
}

两个主要操作,new Timeout和insert。我们一个个来。

1 Timeout

function Timeout(callback, after, args, isRepeat, isRefed) {after *= 1; // Coalesce to number or NaNif (!(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; // Schedule on next tick, follows browser behavior}// 超时时间相对值this._idleTimeout = after;// 前后指针,用于链表this._idlePrev = this;this._idleNext = this;// 定时器的开始时间this._idleStart = null;// This must be set to null first to avoid function tracking// on the hidden class, revisit in V8 versions after 6.2// 超时回调this._onTimeout = null;this._onTimeout = callback;// 执行回调时传入的参数this._timerArgs = args;// 是否定期执行回调,用于setIntervalthis._repeat = isRepeat ? after : null;this._destroyed = false;// 激活底层的定时器节点(二叉堆的节点),说明有定时节点需要处理if (isRefed)incRefCount();this[kRefed] = isRefed;initAsyncResource(this, 'Timeout');
}

Timeout主要是新建一个对象记录一些定时器的上下文信息。

2 insert(对照上面的图理解)

function insert(item, msecs, start = getLibuvNow()) {msecs = MathTrunc(msecs);// 记录定时器的开始时间,见Timeout函数的定义item._idleStart = start;// 该超时时间是否已经存在对应的链表let list = timerListMap[msecs];// 还没有if (list === undefined) {// 算出绝对超时时间const expiry = start + msecs;// 新建一个链表timerListMap[msecs] = list = new TimersList(expiry, msecs);// 插入优先队列timerListQueue.insert(list);// 算出下一次超时的时间,即最快到期的时间if (nextExpiry > expiry) {// 设置底层的最后超时时间,这样保证可以尽量按时执行scheduleTimer(msecs);nextExpiry = expiry;}}// 把当前节点加到队列里L.append(list, item);
}

scheduleTimer函数是对c++函数的封装。

void ScheduleTimer(const FunctionCallbackInfo<Value>& args) {auto env = Environment::GetCurrent(args);env->ScheduleTimer(args[0]->IntegerValue(env->context()).FromJust());
}void Environment::ScheduleTimer(int64_t duration_ms) {if (started_cleanup_) return;uv_timer_start(timer_handle(), RunTimers, duration_ms, 0);
}

uv_timer_start就是开启底层计时,即往libuv的二叉堆插入一个节点。超时时间是duration_ms,就是最快到期的时间,在timer阶段会判断是否过期。是的话执行RunTimers函数。我们先看一下该函数的主要代码。

Local<Function> cb = env->timers_callback_function();
ret = cb->Call(env->context(), process, 1, &arg);

RunTimers会执行timers_callback_function。timers_callback_function是在nodejs初始化的时候设置的。我们先暂定一下,看一下定时器模块的初始化流程。再回来这里分析。

nodejs在初始化的时候通过一下代码对定时器进行了初始化工作。

setupTimers(processImmediate, processTimers);

setupTimers对应的c++函数是

void SetupTimers(const FunctionCallbackInfo<Value>& args) {auto env = Environment::GetCurrent(args);env->set_immediate_callback_function(args[0].As<Function>());env->set_timers_callback_function(args[1].As<Function>());
}

nodejs把processTimers设置为超时的回调函数。现在我们知道了nodejs是如何设置超时的处理函数,也知道了什么时候会执行该回调。那我们就来看一下回调时具体处理逻辑。

void Environment::RunTimers(uv_timer_t* handle) {Local<Function> cb = env->timers_callback_function();MaybeLocal<Value> ret;Local<Value> arg = env->GetNow();do {// 执行js回调,即下面的processTimers函数ret = cb->Call(env->context(), process, 1, &arg);} while (ret.IsEmpty() && env->can_call_into_js());// 是否执行了所有的节点if (ret.IsEmpty())return;int64_t expiry_ms = ret.ToLocalChecked()->IntegerValue(env->context()).FromJust();uv_handle_t* h = reinterpret_cast<uv_handle_t*>(handle);// 还有超时节点,开块超时时间是expiry_ms ,需要重新插入底层的二叉堆。if (expiry_ms != 0) {// 算出下次超时的相对值int64_t duration_ms =llabs(expiry_ms) - (uv_now(env->event_loop()) - env->timer_base());// 重新把handle插入libuv的二叉堆env->ScheduleTimer(duration_ms > 0 ? duration_ms : 1);}
}

该函数主要是执行回调,然后如果还有没超时的节点,重新设置libuv定时器的时间。看看js层面。

  function processTimers(now) {nextExpiry = Infinity;let list;let ranAtLeastOneList = false;// 取出优先队列的根节点,即最快到期的节点while (list = timerListQueue.peek()) {// 还没过期,if (list.expiry > now) {nextExpiry = list.expiry;// 返回下一次过期的时间return refCount > 0 ? nextExpiry : -nextExpiry;}listOnTimeout(list, now);}return 0;}function listOnTimeout(list, now) {const msecs = list.msecs;debug('timeout callback %d', msecs);let ranAtLeastOneTimer = false;let timer;// 遍历具有统一相对过期时间的队列while (timer = L.peek(list)) {// 算出已经过去的时间const diff = now - timer._idleStart;// 过期的时间比超时时间小,还没过期if (diff < msecs) {// 整个链表节点的最快过期时间等于当前还没过期节点的值,链表是有序的list.expiry = MathMax(timer._idleStart + msecs, now + 1);// 更新id,用于决定在优先队列里的位置list.id = timerListId++;// 调整过期时间后,当前链表对应的节点不一定是优先队列里的根节点了,可能有他更快到期,即当前链表需要往下沉timerListQueue.percolateDown(1);return;}// 准备执行用户设置的回调,删除这个节点L.remove(timer);let start;if (timer._repeat)start = getLibuvNow();try {const args = timer._timerArgs;// 执行用户设置的回调if (args === undefined)timer._onTimeout();elsetimer._onTimeout(...args);} finally {// 设置了重复执行回调,即来自setInterval。则需要重新加入链表。if (timer._repeat && timer._idleTimeout !== -1) {// 更新超时时间,一样的时间间隔timer._idleTimeout = timer._repeat;// 重新插入链表insert(timer, timer._idleTimeout, start);} else if (!timer._idleNext && !timer._idlePrev && !timer._destroyed) {timer._destroyed = true;if (timer[kRefed])refCount--;}// 为空则删除if (list === timerListMap[msecs]) {delete timerListMap[msecs];timerListQueue.shift();}}

上面的代码主要是遍历优先队列,

  • 如果当前节点超时,即遍历他对应的链表。否则重新计算出最快超时时间,修改底层libuv的节点。即更新超时时间。
  • 遍历链表的时候如果遇到超时的则执行,如果没有超时的说明后面的节点也不会超时了。因为链表是有序的。修改链表的最快超时时间的值,调整他在优先队列的位置。因为超时时间变了。可能需要调整。

定时器模块的setTimeout分析完了,后面有机会的话再补充一下,另外setInterval是类似的。

这篇关于nodejs 14.0.0源码分析之setTimeout的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/853452

相关文章

MyBatis Plus 中 update_time 字段自动填充失效的原因分析及解决方案(最新整理)

《MyBatisPlus中update_time字段自动填充失效的原因分析及解决方案(最新整理)》在使用MyBatisPlus时,通常我们会在数据库表中设置create_time和update... 目录前言一、问题现象二、原因分析三、总结:常见原因与解决方法对照表四、推荐写法前言在使用 MyBATis

Python主动抛出异常的各种用法和场景分析

《Python主动抛出异常的各种用法和场景分析》在Python中,我们不仅可以捕获和处理异常,还可以主动抛出异常,也就是以类的方式自定义错误的类型和提示信息,这在编程中非常有用,下面我将详细解释主动抛... 目录一、为什么要主动抛出异常?二、基本语法:raise关键字基本示例三、raise的多种用法1. 抛

github打不开的问题分析及解决

《github打不开的问题分析及解决》:本文主要介绍github打不开的问题分析及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、找到github.com域名解析的ip地址二、找到github.global.ssl.fastly.net网址解析的ip地址三

Mysql的主从同步/复制的原理分析

《Mysql的主从同步/复制的原理分析》:本文主要介绍Mysql的主从同步/复制的原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录为什么要主从同步?mysql主从同步架构有哪些?Mysql主从复制的原理/整体流程级联复制架构为什么好?Mysql主从复制注意

java -jar命令运行 jar包时运行外部依赖jar包的场景分析

《java-jar命令运行jar包时运行外部依赖jar包的场景分析》:本文主要介绍java-jar命令运行jar包时运行外部依赖jar包的场景分析,本文给大家介绍的非常详细,对大家的学习或工作... 目录Java -jar命令运行 jar包时如何运行外部依赖jar包场景:解决:方法一、启动参数添加: -Xb

Apache 高级配置实战之从连接保持到日志分析的完整指南

《Apache高级配置实战之从连接保持到日志分析的完整指南》本文带你从连接保持优化开始,一路走到访问控制和日志管理,最后用AWStats来分析网站数据,对Apache配置日志分析相关知识感兴趣的朋友... 目录Apache 高级配置实战:从连接保持到日志分析的完整指南前言 一、Apache 连接保持 - 性

Linux中的more 和 less区别对比分析

《Linux中的more和less区别对比分析》在Linux/Unix系统中,more和less都是用于分页查看文本文件的命令,但less是more的增强版,功能更强大,:本文主要介绍Linu... 目录1. 基础功能对比2. 常用操作对比less 的操作3. 实际使用示例4. 为什么推荐 less?5.

spring-gateway filters添加自定义过滤器实现流程分析(可插拔)

《spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔)》:本文主要介绍spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔),本文通过实例图... 目录需求背景需求拆解设计流程及作用域逻辑处理代码逻辑需求背景公司要求,通过公司网络代理访问的请求需要做请

Java集成Onlyoffice的示例代码及场景分析

《Java集成Onlyoffice的示例代码及场景分析》:本文主要介绍Java集成Onlyoffice的示例代码及场景分析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 需求场景:实现文档的在线编辑,团队协作总结:两个接口 + 前端页面 + 配置项接口1:一个接口,将o

IDEA下"File is read-only"可能原因分析及"找不到或无法加载主类"的问题

《IDEA下Fileisread-only可能原因分析及找不到或无法加载主类的问题》:本文主要介绍IDEA下Fileisread-only可能原因分析及找不到或无法加载主类的问题,具有很好的参... 目录1.File is read-only”可能原因2.“找不到或无法加载主类”问题的解决总结1.File