深入解析Node.js setTimeout方法的执行过程

深入了解setTimeout源码之前,本有两个选择。一是通过chromium源码分析,二是通过Node.js源码分析。后来发现第一种方案的源码获取成本太大,于是从Node官网获取了几十兆的代码用来了解。

当前的Node版本为:v10.16.0

setTimeout方法定义于timers.js文件中,源码整体如下:

function setTimeout(callback, after, arg1, arg2, arg3) {// 基础校验if (typeof callback !== 'function') {throw new ERR_INVALID_CALLBACK();}// 参数校验var 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;}// 新建Timeout对象const timeout = new Timeout(callback, after, args, false, false);active(timeout);// 最后返回Timeout对象return timeout;
}

Timeout构造函数内部如下:

// Timer constructor function.
// The entire prototype is defined in lib/timers.js
function Timeout(callback, after, args, isRepeat, isUnrefed) {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._called = false;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.2this._onTimeout = null;this._onTimeout = callback;this._timerArgs = args;this._repeat = isRepeat ? after : null;this._destroyed = false;this[unrefedSymbol] = isUnrefed;initAsyncResource(this, 'Timeout');
}

我们这里分析只关注两个参数:1.callback, 2.after

  this._idleTimeout = after;this._onTimeout = callback;

基本初始化完成,进入active方法。active方法的item参数为新new的Timeout对象。

// Schedule or re-schedule a timer.
// The item must have been enroll()'d first.
const active = exports.active = function(item) {insert(item, false);
};

进入insert方法,item = Timeout对象, unrefed = false, start = undefined.

// The underlying logic for scheduling or re-scheduling a timer.
//
// Appends a timer onto the end of an existing timers list, or creates a new
// TimerWrap backed list if one does not already exist for the specified timeout
// duration.
function insert(item, unrefed, start) { // timeout, false // 对after做校验const msecs = item._idleTimeout;if (msecs < 0 || msecs === undefined) return;if (typeof start === 'number') {item._idleStart = start;} else {item._idleStart = TimerWrap.now();}const lists = unrefed === true ? unrefedLists : refedLists;// Use an existing list if there is one, otherwise we need to make a new one.var list = lists[msecs];if (list === undefined) {debug('no %d list was found in insert, creating a new one', msecs);lists[msecs] = list = new TimersList(msecs, unrefed);}if (!item[async_id_symbol] || item._destroyed) {item._destroyed = false;initAsyncResource(item, 'Timeout');}L.append(list, item); // list = timerlist, item = timeout, 增加一个节点在队列中,节点类型不同。assert(!L.isEmpty(list)); // list is not empty
}

TimerWrap.now()方法返回的应当是当前的时间,具体的执行代码为:

  static void Now(const FunctionCallbackInfo<Value>& args) {Environment* env = Environment::GetCurrent(args);args.GetReturnValue().Set(env->GetNow());}

这时,Timeout对象有三个关键属性:

  item._idleTimeout = after; // 延迟多少秒执行item._onTimeout = callback; // 延迟执行回调函数item._idleStart = TimerWrap.now(); // 当下时间

然后进行到lists[after] = refedLists[after] = list = new TimersList(after, false);

也就是说refedLists对象的after属性对应一个TimersList对象,而refedLists对象是全局的。

function TimersList(msecs, unrefed) {this._idleNext = this; // Create the list with the linkedlist properties tothis._idlePrev = this; // prevent any unnecessary hidden class changes.this._unrefed = unrefed;this.msecs = msecs;const timer = this._timer = new TimerWrap();timer._list = this;if (unrefed === true)timer.unref();timer.start(msecs);
}

可以将TimersList对象视作为一个双向链表节点,它内部有指向上下节点的指针,当一个节点新建时,这个节点的的上下节点会指向自己。节点的内容为:

  this.msecs = after;this._timer = new TimerWrap(); // 这里的TimerWrap为一个Native对象timer._list = this;

最后到timer.start(after),它的函数内部如下:

  static void Start(const FunctionCallbackInfo<Value>& args) {TimerWrap* wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());CHECK(HandleWrap::IsAlive(wrap));int64_t timeout = args[0]->IntegerValue(); // 这里的timeout为js代码传入的afterint err = uv_timer_start(&wrap->handle_, OnTimeout, timeout, 0);args.GetReturnValue().Set(err);}

这里关键的地方是uv_timer_start,方法的内部如下:

int uv_timer_start(uv_timer_t* handle,uv_timer_cb cb,uint64_t timeout,uint64_t repeat) {uint64_t clamped_timeout;if (cb == NULL)return UV_EINVAL;if (uv__is_active(handle))uv_timer_stop(handle);// 这里是关键。clamped_timeout的值等于当前时间加上未来要执行的时间clamped_timeout = handle->loop->time + timeout;if (clamped_timeout < timeout)clamped_timeout = (uint64_t) -1;handle->timer_cb = cb;handle->timeout = clamped_timeout; // timeout为未来要执行的时间handle->repeat = repeat;/* start_id is the second index to be compared in uv__timer_cmp() */handle->start_id = handle->loop->timer_counter++;heap_insert(timer_heap(handle->loop),(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_start(handle);return 0;
}

这里我关注的是传入参数是怎么被操作的:

  handle->timer_cb = cb;handle->timeout = clamped_timeout;

好,到这里设置完成,我们回到insert方法内部继续向下,继续执行:

  L.append(list, item); // list = timerlist, item = timeout, 增加一个节点在队列中,节点类型不同。

append方法将item追加到了list中。list对象是一个由item节点组成的双向链表。然后到这里添加结束。

你可能会疑惑,到这里就结束了?其实过程中有很多细节被我们忽略了,不过没关系。既然L.append用来追加节点,那它一定要取出节点,我们从上下文可知:

listOnTimeout方法中取出了这个节点(这个过程后面再涉及):

function listOnTimeout(handle, now) {const list = handle._list;const msecs = list.msecs;debug('timeout callback %d', msecs);debug('now: %d', now);var diff, timer;while (timer = L.peek(list)) {diff = now - timer._idleStart;// Check if this loop iteration is too early for the next timer.// This happens if there are more timers scheduled for later in the list.if (diff < msecs) {var timeRemaining = msecs - (TimerWrap.now() - timer._idleStart);if (timeRemaining <= 0) {timeRemaining = 1;}handle.start(timeRemaining);debug('%d list wait because diff is %d', msecs, diff);return true;}// The actual logic for when a timeout happens.L.remove(timer);assert(timer !== L.peek(list));if (!timer._onTimeout) {if (destroyHooksExist() && !timer._destroyed &&typeof timer[async_id_symbol] === 'number') {emitDestroy(timer[async_id_symbol]);timer._destroyed = true;}continue;}tryOnTimeout(timer);}// If `L.peek(list)` returned nothing, the list was either empty or we have// called all of the timer timeouts.// As such, we can remove the list and clean up the TimerWrap C++ handle.debug('%d list empty', msecs);assert(L.isEmpty(list));// Either refedLists[msecs] or unrefedLists[msecs] may have been removed and// recreated since the reference to `list` was created. Make sure they're// the same instance of the list before destroying.if (list._unrefed === true && list === unrefedLists[msecs]) {delete unrefedLists[msecs];} else if (list === refedLists[msecs]) {delete refedLists[msecs];}// Do not close the underlying handle if its ownership has changed// (e.g it was unrefed in its callback).if (!handle[owner_symbol])handle.close();return true;
}

listOnTimeout方法其实是整个JS层处理队列事件的核心。方法内部的handle对象实为TimerWrap。handle._list为TimersList。方法内的msecs为after。接下来while不断从TimersList中取timer。peek总是返回队首的数据。

然后到了关键处理阶段:

    diff = now - timer._idleStart; // 计算方法执行时的差额// 如果时间还不到,则进行:if (diff < msecs) {var timeRemaining = msecs - (TimerWrap.now() - timer._idleStart);if (timeRemaining <= 0) {timeRemaining = 1;}// 计算出剩余时间,再次执行Native的start方法。handle.start(timeRemaining);debug('%d list wait because diff is %d', msecs, diff);return true;}

handle.start方法的内部如下:

  static void Start(const FunctionCallbackInfo<Value>& args) {TimerWrap* wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());CHECK(HandleWrap::IsAlive(wrap));int64_t timeout = args[0]->IntegerValue();int err = uv_timer_start(&wrap->handle_, OnTimeout, timeout, 0);args.GetReturnValue().Set(err);}

这时在执行start方法时就是一个after距离真正执行的剩余时间,再次执行uv_timer_start方法,

还记得上文中提到的这段代码吗?这段代码位于uv_timer_start方法内。

  handle->timer_cb = cb;handle->timeout = clamped_timeout;

刚刚我们并没有继续深究,现在不得不深究一下。

上面说到timeout被赋值了,那一定有地方再取出它进行执行。通过上下文可知:

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);uv_timer_again(handle);handle->timer_cb(handle);}
}

uv__run_timers是一个无限循环的方法,内部永远在循环执行timer_cb方法。还记得上文中提到的clamped_timeout吗,如果clamped_timeout为任务触发的时间,这里的无限循环一直在判断时间是否到期,如果到期了则会向下执行,否则一直循环。

这个方法是如何被触发的我们暂时不在这里深究。

我们从上文的代码可以知道,上文中的timer_cb是OnTimeout方法,它的内部实现如下:

  static void OnTimeout(uv_timer_t* handle) {TimerWrap* wrap = static_cast<TimerWrap*>(handle->data);Environment* env = wrap->env();HandleScope handle_scope(env->isolate());Context::Scope context_scope(env->context());MaybeLocal<Value> ret;Local<Value> args[1];do {// 这里是关键所在args[0] = env->GetNow(); // 获取当前时间ret = wrap->MakeCallback(env->timers_callback_function(), 1, args); // 执行调用} while ((ret.IsEmpty() || ret.ToLocalChecked()->IsUndefined()) &&!env->tick_info()->has_thrown() &&env->can_call_into_js() &&wrap->object()->Get(env->context(),env->owner_symbol()).ToLocalChecked()->IsUndefined());}

这里的timers_callback_function()为js层传入的processTimers方法,设置的代码位于lib/timers.js文件中:

const [immediateInfo, toggleImmediateRef] =setupTimers(processImmediate, processTimers);

processTimers内部如下:

function processTimers(now) {if (this[owner_symbol])return unrefdHandle(this[owner_symbol], now);return listOnTimeout(this, now);
}

到这里是不是见过listOnTimeout方法?没错,我们回到了listOnTimeout方法调用处。这个从listOnTimeout方法到listOnTimeout方法会不断循环,直到if (diff < msecs) 条件不成立。也就是说当条件成立时才会继续执行。

真正的业务回调代码如下:

	// 将这次的timer任务从队列中取出L.remove(timer);assert(timer !== L.peek(list));// 这里不成立if (!timer._onTimeout) {if (destroyHooksExist() && !timer._destroyed &&typeof timer[async_id_symbol] === 'number') {emitDestroy(timer[async_id_symbol]);timer._destroyed = true;}continue;}// 关键在于这里tryOnTimeout(timer);
function tryOnTimeout(timer, start) {timer._called = true;const timerAsyncId = (typeof timer[async_id_symbol] === 'number') ?timer[async_id_symbol] : null;var threw = true;if (timerAsyncId !== null)emitBefore(timerAsyncId, timer[trigger_async_id_symbol]);try {ontimeout(timer, start);threw = false;} finally {if (timerAsyncId !== null) {if (!threw)emitAfter(timerAsyncId);if ((threw || !timer._repeat) && destroyHooksExist() &&!timer._destroyed) {emitDestroy(timerAsyncId);timer._destroyed = true;}}}
}

这里的关键在于ontimeout方法,该方法内部实现如下:

function ontimeout(timer, start) {const args = timer._timerArgs;if (typeof timer._onTimeout !== 'function')return Promise.resolve(timer._onTimeout, args[0]);if (start === undefined && timer._repeat)start = TimerWrap.now();if (!args)timer._onTimeout();elseReflect.apply(timer._onTimeout, timer, args);if (timer._repeat)rearm(timer, start);
}

上面的方法执行javascript timer._onTimeout();这里是真正的回调执行。

至此,一个普通的setTimeout方法执行完毕。

最后总结一下调用流程图:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478678.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

别只关注GPT3!细如发丝的模型更具现实杀伤力!

这个世界上有两种极具难度的工程&#xff1a;第一种是把很平常的东西做到最大&#xff0c;例如把语言模型扩大成能够写诗写文写代码的GPT-3&#xff1b;而另一种恰恰相反&#xff0c;是把很平常的东西做到最小。 ----王苏语录GPT3自从诞生以来&#xff0c;便受到…

论文浅尝 - EMNLP2020 | 基于分组式对比学习的神经对话生成

论文笔记整理&#xff1a;叶宏彬&#xff0c;浙江大学计算机博士生。论文地址&#xff1a;https://arxiv.org/abs/2009.07543摘要&#xff1a;近年来&#xff0c;神经对话问答的产生已广受欢迎。现有对话模型学习中广泛采用了最大似然估计目标&#xff08;MLE&#xff09;。但是…

LeetCode 841. 钥匙和房间(DFS/BFS)

文章目录1. 题目2. 解题2.1 DFS2.2 BFS1. 题目 有 N 个房间&#xff0c;开始时你位于 0 号房间。每个房间有不同的号码&#xff1a;0&#xff0c;1&#xff0c;2&#xff0c;…&#xff0c;N-1&#xff0c;并且房间里可能有一些钥匙能使你进入下一个房间。 在形式上&#xff…

容器中用uwsgi协议部署注意的问题以及用flask部署

1 表示当前容器名 一般通过 docker exec -it 容器名 /bin/bash , 进入容器内部进行操作&#xff0c;比如安装环境&#xff0c;传输文件 2 4888 容器内部访问端口地址&#xff0c;如果对外开放api&#xff0c;需要对其进行映射&#xff0c;比如映射成11022,11022是对外访问的端…

美团点评金融平台Web前端技术体系

背景 随着美团点评金融业务的高速发展&#xff0c;前端研发数量从 2015 年的 1 个人&#xff0c;扩张到了现在横跨北上两地 8 个事业部的将近 150 人。业务新&#xff0c;团队新&#xff0c;前端领域框架技术又层出不穷&#xff0c;各个业务的研发团队在技术选择上没有明确的指…

参会邀请 - CCKS2020 | 2020全国知识图谱与语义计算大会(CCKS2020)明日开幕

本文转载自公众号&#xff1a; 中国中文信息学会。第十四届全国知识图谱与语义计算大会将于2020年11月12日-15日在南昌召开。会议由中国中文信息学会语言与知识计算专业委员会主办&#xff0c;由江西师范大学承办&#xff0c;智源社区提供社区支持。本次会议讲习班采用线上举行…

Linux 程 序 员 失 业 警 告

文 | 小戏有多少人期待过像贾维斯一样的强人工智能&#xff1f;尽管老实说看当下的技术离这一期待还很遥远&#xff0c;但用用类似 GPT-3 这样的技术去实现些朴素的愿望似乎并没有那么困难。就在昨天&#xff0c;来自 Facebook 的 Elvis 在推特上发布了一个借助 OpenAI 的 GPT-…

LeetCode 707. 设计链表(List)

文章目录1. 设计一个单链表2. 双向链表1. 设计一个单链表 在链表类中实现这些功能&#xff1a; get(index)&#xff1a;获取链表中第 index 个节点的值。如果索引无效&#xff0c;则返回-1。 addAtHead(val)&#xff1a;在链表的第一个元素之前添加一个值为 val 的节点。插入…

用Vue.js开发微信小程序:开源框架mpvue解析

前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架。使用此框架&#xff0c;开发者将得到完整的 Vue.js 开发体验&#xff0c;同时为 H5 和小程序提供了代码复用的能力。如果想将 H5 项目改造为小程序&#xff0c;或开发小程序后希望将其转换为 H5&#xff0c;mpvue 将是…

对比学习有多火?文本聚类都被刷爆了…

文 | 花小花Posy大家好&#xff0c;我是小花。对比学习的大火???? 越来越旺了&#xff0c;已然从CV蔓延到NLP了。今天给大家介绍的正是一篇将对比学习应用到文本聚类上的工作&#xff0c;NAACL21新鲜出炉的paper——《Supporting Clustering with Contrastive Learning》。…

论文浅尝 - WWW2020 | 生成多跳推理问题以改善机器阅读理解能力

论文笔记整理&#xff1a;谭亦鸣&#xff0c;东南大学博士生。来源&#xff1a;WWW 2020链接&#xff1a;https://dl.acm.org/doi/pdf/10.1145/3366423.3380114概述这篇论文关注的任务是&#xff1a;基于给定文本的“多跳问题生成”&#xff08;多关系问题&#xff09;。作者提…

记一次Vue框架升级

框架升级背景 公司目前业务迭代很快&#xff0c;且大部分的流量都在公众号上。然而我们公众号所使用的框架却是3年前的Vue 1.0.16。面对Vue这3年来带来的无数新特性&#xff0c;我们只能望洋兴叹&#xff1a;看得见&#xff0c;摸不着&#xff0c;因为升级这事看起来太难了。 …

谈谈NLP下一个主战场:万亿参数的预训练模型!

自从BERT诞生以来&#xff0c;各大互联网巨头之间就展开了预训练语言模型军备竞赛&#xff0c;XLNet、ERNIE、RoBERTa、T5、GPT-3....但当事情进展到号称自己是zero-shot learner的GPT-3时&#xff0c;批判的声音变得明显多了。这么大&#xff0c;能用吗&#xff1f;真的能做到…

人物志 | 美团女技术总监任登君:不要给自己的人生设限

在我们美团技术团队超过6000名工程师中&#xff0c;有众多的女同学&#xff0c;她们是支撑中国领先的生活服务电子商务平台不可或缺的力量。3月8日女神节&#xff0c;我们专访了她们的代表——美团广告平台技术负责人任登君。登君也是我们团队里目前职位最高的女性技术Leader&a…

论文浅尝 - ISWC2020 | KnowlyBERT: 知识图谱结合语言模型补全图谱查询

论文笔记整理&#xff1a;胡楠&#xff0c;东南大学博士。来源&#xff1a;ISWC 2020动机像Wikidata这样的现代知识图已经捕获了数十亿个RDF三元组&#xff0c;但是它们仍然缺乏对大多数关系的良好覆盖。同时在NLP研究的最新进展表明&#xff0c;可以轻松地查询神经语言模型以获…

LeetCode 92. 反转链表 II(双指针)

1. 题目 反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。 说明: 1 ≤ m ≤ n ≤ 链表长度。 示例:输入: 1->2->3->4->5->NULL, m 2, n 4 输出: 1->4->3->2->5->NULL来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xf…

我对你的爱,是只为你而留的神经元

文 | 白鹡鸰有一个小轶专属神经元编 | 小轶有一个白鹡鸰专属神经元什么是苹果&#xff1f;红的&#xff1f;绿的&#xff1f;黄的&#xff1f;球状&#xff1f;斑点&#xff1f;香气&#xff1f;需要咬上一口才能确定&#xff1f;或者……其实我们在说某家技术公司&#xff1f;…

Android动态日志系统Holmes

背景 美团是全球领先的一站式生活服务平台&#xff0c;为6亿多消费者和超过450万优质商户提供连接线上线下的电子商务网络。美团的业务覆盖了超过200个丰富品类和2800个城区县网络&#xff0c;在餐饮、外卖、酒店旅游、丽人、家庭、休闲娱乐等领域具有领先的市场地位。平台大&a…

领域应用 | 知识图谱在小米的应用与探索

本文转载自公众号&#xff1a;DataFunTalk。分享嘉宾&#xff1a;彭力 小米编辑整理&#xff1a;马瑶出品平台&#xff1a;DataFunTalk导读&#xff1a;小米知识图谱于2017年创立&#xff0c;已支持公司了每天亿级的访问&#xff0c;已赋能小爱同学&#xff0c;小米有品、智能问…

前端应用开发架构图谱

个人整理的前端架构图谱&#xff0c;之后会根据这个图谱不断的完善内容。希望这个图谱可以对开发同学的知识脉络有个梳理的作用。 相关图谱文件已上传至Github&#xff1a;https://github.com/sahadev/front-end-architecture&#xff0c;后续将不定期更新。 2020年02月28日已…