nodejs定时器setInterval,setTimeout,clearTimeout, clearInterval源码学习
nodejs Timer
- nodejs Timer
- timer.unref()的失效情况
- 先看timer.unref的底层调用
- 对失效的解释
- 定时器的创建
- TimerWrap
- TimerWrap()
- 再重点看下TimerWrap里的uv_timer_init
- 总结
- TimerWrap
- setInterval定时器
- clearTimeout, clearInterval
- setImmediate
- node timer触发
- uv_run_timers
- 事件循环结束
- Timer总结
- timer.unref()的失效情况
本文章最好配合nodejs c语言源码一起,下面几个链接是相关的写的非常好的文章。
github优化资料
alinode源码
alinode TIme
nodejs timer解析
timer.unref()的失效情况
- 有效用法
下面的timer1
在创建后被unref
, 所以不会执行callback函数;
let timer1 = setInterval(()=>{console.log('timer1...');
}, 1000);
timer1.unref();
- 失效用法
当同时存在2个timer
时,unref
函数失效, 两个定时器都会执行;
let timer1 = setInterval(()=>{console.log('timer1...');
}, 1000);
let timer2 = setInterval(()=>{console.log('timer2...');
}, 2000);
timer1.unref();
先看timer.unref
的底层调用
unref
对应的C++代码调用:
// timer_wrap.cc
constructor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Timer"));
...
env->SetProtoMethod(constructor, "unref", HandleWrap::Unref);
即调用的是HandleWrap
的Unref
方法:
// handle_wrap.cc
void HandleWrap::Unref(const FunctionCallbackInfo<Value>& args) {HandleWrap* wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());if (IsAlive(wrap))uv_unref(wrap->GetHandle());
}// uv-common.cc
void uv_unref(uv_handle_t* handle) {uv__handle_unref(handle);
}
uv-common.h
里面uv__handle_unref
干了两件事:
1) 取消REF位标记: (h)->flags &= ~UV__HANDLE_REF;
2) 如果timer
处于active状态, 则将活跃句柄-1: loop->active_handle--
;
对失效的解释
创建了2个timer后, node循环中, 有两个active_handles
:
let timer1 = ...
// setInterval=>uv_timer_start(timer1) => active_handles = 1
let time2 = ...
// setInterval=>uv_timer_start(timer2) => active_handles = 2
active_handles>0
激活loop
, 这里L1
为循环为标记点:
// L1: ative_handles > 0 => loop()
由于unref
只是标记了REF
, 这里还是执行了timeout
(TODO需要说明为什么还会执行??), 当timer1
到时后, 判断了active_handles=1
, loop
仍然处于acitve
, 所以会执行再次启动定时器uv_timer_start(timer1)
:
// timer1 timeout => uv_timer_stop(timer1) active_handles = 1 => callback() => uv_timer_start(timer1) active_handles = 2
// timer2 timeout => uv_timer_stop(timer2) active_handles = 1 => callback() => uv_timer_start(timer2) active_handles = 2
// goto L1
也就是说, 就是看loop
是否active
状态, 处于active
, 则会执行循环中的handle
, loop
是否在active
看active_handles
是否大于0;
#define uv__has_active_handles(loop) \((loop)->active_handles > 0)
??? 具体为什么在unref后还会执行, 需要查验, REF标记对handle执行的影响
定时器的创建
setTimeout
为入口,
1. new Timeout();
这里的Timeout
对象timer时挂在TimerList
链表上的node, 所以其结构如下, 其中的_idlePrev
, _idleNext
为链表指针:
function Timeout(after, callback, args) {this._called = false;this._idleTimeout = after;this._idlePrev = this;this._idleNext = this;this._idleStart = null;this._onTimeout = callback;this._timerArgs = args;this._repeat = null;
}
active(timer)
- 激活定时器就是将其挂在
TimerList
上, 调用list的insert(timer, false)
函数. - 其实现可以看到, 这里的定时器分了两种, 分别为链表集:
unrefedLists
与refedLists
,setTimeout
的unrefed=false
, 属于unrefedLists
, 这个ref
具体什么意识,目前还不是很清楚;
- 激活定时器就是将其挂在
function insert(item, unrefed) {const msecs = item._idleTimeout;...item._idleStart = TimerWrap.now();const lists = unrefed === true ? unrefedLists : refedLists;var list = lists[msecs];if (!list) {lists[msecs] = list = createTimersList(msecs, unrefed); // new TimeWrap()}L.append(list, item);
}
new TimeWrap()
创建C定时器
createTimersList()
时, 整个TimerList
公用一个定时器, 返回给了_timer
, 且指定了当前的list的超时时间:
function TimersList(msecs, unrefed) {...this._timer = new TimerWrap();...this.msecs = msecs;
}
L.append()
将当前定时器挂在链表上
参考linkedlist.js
的实现,L.append()
是在链表末尾挂载定时器(相同timeout, 后挂载的超时事件越晚);分析
TimersList
的创建
下面初始化链表创建了一个TimerWrap
对象, 后面的源码分析说明, 这里的_timer
是一个C定时器对象
// createTimersList()
const list = new TimersList(msecs, unrefed);
// TimersList()this._timer = new TimerWrap();
unref
这里对引用参数进行了判断, 其底层调用了uv_unref
, 如果为true
, 则将该定时器标记为非REF
// createTimersList()
if (unrefed === true) list._timer.unref();
- 定时器启动
list._timer.start(msecs)
调用TimerWrap::Start()
函数,
// TimerWrap::Start()
int err = uv_timer_start(&wrap->handle_, OnTimeout, timeout, 0);
uv_timer_start
是与平台相关, 看unix实现, 下面的代码可见, unix下的定时器是以二叉树最小堆的结构来保存的, 相关原因看本页前面的几个链接.
int uv_timer_start(uv_timer_t* handle,uv_timer_cb cb,uint64_t timeout,uint64_t repeat) {...handle->start_id = handle->loop->timer_counter++;heap_insert((struct heap*) &handle->loop->timer_heap,(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_start(handle);...
}
// uv__handle_start()
#define uv__handle_start(h) \
...
(h)->flags |= UV__HANDLE_ACTIVE; \ // activing
if (((h)->flags & UV__HANDLE_REF) != 0) uv__active_handle_add(h); \ // refed => add()
...
heap_insrt
函数定位于heap_inl.h
头文件中, 实现的是一个二叉树结构的最小堆, 查找与插入复杂度均为O(logn)
.
- 定时器链表超时检查与调用回调
下面的listOnTimeout
为封装的对该TimersList的定时器的超时调用,
// createTimersList()list._timer[kOnTimeout] = listOnTimeout;// listOnTimeout()
....
var now = TimerWrap.now();
...while (timer = L.peek(list)) {diff = now - timer._idleStart;if (diff < msecs) {...// 剩余时间发生变化, 跟新当前定时器在定时器最小堆中的位置, 只要判断链表头的Timeout对象, 所以直接退出this.start(timeRemaining);return;}// 定时器超时// 链表中清除L.remove(timer);if (!timer._onTimeout) continue;...// 执行回调tryOnTimeout(timer, list);...}// 链表为空, 做清除工作assert(L.isEmpty(list));this.close();if (list._unrefed === true && list === unrefedLists[msecs]) {delete unrefedLists[msecs];} else if (list === refedLists[msecs]) {delete refedLists[msecs];}
- 定时器链表的清除工作
上面的代码给出了定时器链表为空时做的清除工作:
1) 关闭定时器对象this.close()
该函数调用的是HandleWrap::Close()
, 其中主要调用了uv_close(wrap->handle_, OnClose);
, 而这个函数也是与平台相关, unix底层实现就是从最小堆中删除该定时器:
switch (handle->type) {....case UV_TIMER:uv__timer_close((uv_timer_t*)handle);break;....
}// timer.c
void uv__timer_close(uv_timer_t* handle) {uv_timer_stop(handle);
}
int uv_timer_stop(uv_timer_t* handle) {....// 从定时器堆中删除heap_remove((struct heap*) &handle->loop->timer_heap,(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_stop(handle); // 标记为!_ACTIVE, 删引用REFreturn 0;
}
2) 释放链表的内存delete ...
TimerWrap
TimerWrap()
看看TimerWrap
干了些什么, 下面的构造函数里调用了父类HandlerWrap
的构造函数, 然后执行了初始化uv_timer_init
:
// timer_wrap.ccTimerWrap(Environment* env, Local<Object> object): HandleWrap(env,object,reinterpret_cast<uv_handle_t*>(&handle_),AsyncWrap::PROVIDER_TIMERWRAP) {int r = uv_timer_init(env->event_loop(), &handle_);CHECK_EQ(r, 0);}
上面的HandleWrap
做了一些v8环境与句柄的初始化(还没看懂), 这里的handle_
是一个uv通用结构, 封装了unix和windows, 用户完成具体的事件循环:
uv_timer_t handle_;
其又包含(继承)通用结构, 该结构中有data
与loop
对象:
#define UV_HANDLE_FIELDS \/* public */ \void* data; \/* read-only */ \uv_loop_t* loop; \uv_handle_type type; \ ...
loop
对象的结构如下, 具体的结构与平台的回调库相关, 这里我们注意两个公共的数据: data
(用户定义的数据, 即后面的handle_->data = this;
)与active_handlers
(消息循环引用计数器),
struct uv_loop_s {/* User data - use this for whatever. */void* data;/* Loop reference counting. */unsigned int active_handles;...
};
具体的东西很底层, 没看;
再重点看下TimerWrap
里的uv_timer_init
这里的handler_
可以理解为定时器句柄.
uv_timer_init
具体实现与平台相关,
// timer_warp.cc TimerWrap构造函数
int r = uv_timer_init(env->event_loop(), &handle_);
CHECK_EQ(r, 0);
看下unix的实现
// uv/src/unix/timer.c
int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) {uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER);handle->timer_cb = NULL;handle->repeat = 0;return 0;
}
调用了通过的handle_init
, 主要就是执行了写初始化, 并标记为REF
状态.
#define uv__handle_init(loop_, h, type_) \do { \(h)->loop = (loop_); \(h)->type = (type_); \(h)->flags = UV__HANDLE_REF; /* Ref the loop when active. */ \ ....} \while (0)
显然, setTimeout
函数的unrefed=false
, 所以属于引用类型, node内部使用的_unrefActive()
属于unrefed=true
;
总结
TimerWrap
就是构造了一个平台相关的C定时器.
setInterval定时器
- 其实现与
setTimeout
一样, 只是创建Timeout
对象后, 设置了timer._repeat = repeat;
- 在执行回调函数后, 判断了重复
// ontimeout
if (timer._repeat)rearm(timer);
在rearm
实现里, 就是重写开始计时, 这里的_handle
可能不是Timeout
实例(???_unrefAcitve()??
)
// rearm
function rearm(timer) {// // Do not re-arm unenroll'd or closed timers.if (timer._idleTimeout === -1) return;// If timer is unref'd (or was - it's permanently removed from the list.)if (timer._handle && timer instanceof Timeout) {timer._handle.start(timer._repeat);} else {timer._idleTimeout = timer._repeat;active(timer);}
}
clearTimeout, clearInterval
clearTimeout
实现如下, clearInterval
时对clearTimeout
的封装, 仅仅多了置_repeat=null
, 下面的unenroll
好像时内部对就重复利用的定时器, 现在还没看到:
exports.clearTimeout = function(timer) {if (timer && (timer[kOnTimeout] || timer._onTimeout)) {timer[kOnTimeout] = timer._onTimeout = null;if (timer instanceof Timeout) {timer.close(); // for after === 0} else {unenroll(timer); // ????????}}
};
setImmediate
- 全局只有条立即执行队列
该队列为一单链表, 链接的节点是new Immediate()
对象
// Create a single linked list instance only once at startup
var immediateQueue = new ImmediateList();function ImmediateList() {this.head = null;this.tail = null;
}
setImmediate(cb)
函数做了3件事:- 实例化
Immediate
对象,并初始化; - 将对象挂载到全局的立即执行回调队列
immediateQueue
; - 同时, 对全局
process
对象挂载立即执行回调函数_immediateCallback
- 实例化
function createImmediate(args, callback) {var immediate = new Immediate();immediate._callback = callback;immediate._argv = args;immediate._onImmediate = callback;if (!process._needImmediateCallback) {process._needImmediateCallback = true;process._immediateCallback = processImmediate;}immediateQueue.append(immediate);return immediate;
}
- 全局
process._immediateCallback
调用
function processImmediate() {var immediate = immediateQueue.head;var tail = immediateQueue.tail;var domain;// 立即执行链表只会调用一次, 所以,这里提前删除, 以防在callback执行期间又创建了一个immediateQueue.head = immediateQueue.tail = null;// 遍历执行链表while (immediate) {...immediate._callback = immediate._onImmediate;// Save next in case `clearImmediate(immediate)` is called from callbackvar next = immediate._idleNext;tryOnImmediate(immediate, tail);....// If `clearImmediate(immediate)` wasn't called from the callback, use the `immediate`'s next item 这里的判断好像用处不大if (immediate._idleNext)immediate = immediate._idleNext;elseimmediate = next;}... //后面这几句没看懂
}
Immediate
回调函数执行
回调函数实在try{}catch{}
下执行, 如果抛出异常, 仍然会在下一帧继续执行后面的回调函数:
function tryOnImmediate(immediate, oldTail) {var threw = true;try {runCallback(immediate);threw = false;} finally {if (threw && immediate._idleNext) {// Handle any remaining on next tick, assuming we're still alive to do so....const next = immediate._idleNext;if (curHead) {...} else {immediateQueue.head = next;immediateQueue.tail = oldTail;}process.nextTick(processImmediate);}}
}
node timer触发
在回调主循环uv_run
里面, 每帧一开始就会处理定时器:
// unix.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {while (r != 0 && loop->stop_flag == 0) {uv__update_time(loop);uv__run_timers(loop);....r = uv__loop_alive(loop);}
}
uv_run_timers
uv_run_timers
的实现如下, 就是不断从最小堆中去除根节点, 判断是否超时, 如果没超时则退出:
void uv__run_timers(uv_loop_t* loop) {struct heap_node* heap_node;uv_timer_t* handle;for (;;) {heap_node = heap_min((struct heap*) &loop->timer_heap);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_timer_stop
来停止定时器与uv_timer_again
来从启定时器.
下面时这两个函数的实现:
* uv_timer_stop
1) 取得定时器, 从最小堆中移除;
2) 标记~ACTIVE
, 并减计数器;
// uv/src/unix/timer.c
int uv_timer_stop(uv_timer_t* handle) {if (!uv__is_active(handle))return 0;heap_remove((struct heap*) &handle->loop->timer_heap,(struct heap_node*) &handle->heap_node,timer_less_than);uv__handle_stop(handle);return 0;
}
uv_timer_again
如果定时器要求重复触发(repeat
), 则先停止, 再从新启动(跟新超时时间, 插入heap)
int uv_timer_again(uv_timer_t* handle) {if (handle->timer_cb == NULL)return -EINVAL;if (handle->repeat) {// 先移除uv_timer_stop(handle);// 再从新启动uv_timer_start(handle, handle->timer_cb, handle->repeat, handle->repeat);}return 0;
}
事件循环结束
- 上面
uv_run
里面, 循环继续的条件是uv__loop_alive(loop) != 0 && loop->stop_flag == 0
, 即要么loop
被标记为STOP
状态, 要么uv__loop_alive
返回为0; - 而
uv__loop_alive
的实现又说明, 在loop
里的活跃句柄active_handles
对象数只要大于0就不会退出循环, 这就是本文前面的例子里解释;
前面的例子定义了2个
setInterval
对象,unref()
了一个, 此时loop->active_handles=1
, 所以循环会继续, 在执行uv__run_timers
时依次执行uv_timer_stop
和uv_timer_again
, 由于两个定时器都有repeat==true
, 所以两个定时器会再次执行uv_timer_start
, 此时没有被unref
的定时器再一次会执行uv__active_handle_add()
, 所以循环会一直持续下去;
// uv/src/unix/timer.c
static int uv__loop_alive(const uv_loop_t* loop) {return uv__has_active_handles(loop) ||uv__has_active_reqs(loop) ||loop->closing_handles != NULL;
}
// uv-common.h
#define uv__has_active_handles(loop) \((loop)->active_handles > 0)
总结
unref
仅仅时将定时器对象句柄解引用, 不是将定时器从定时器链表中移除, 所以如果循环没有结束, 则定时器还是会被调用;
Timer总结
let timer = setTimeout(cb, t);
函数返回的不是一个真的定时器对象, 而是一个js对象Timeout
, 该对象是TimersList
链表的节点类型, 这里我称它为虚定时器;TimersList
身上的_timer
才是真正的C定时器对象, 每个list上的所有虚定时器共享这个_timer
, 同一个链表上的所有虚定时器有相同的超时时间;- 链表本身是由
list
节点链接首尾节点组成的闭合链表, 检查超时每次只要取头部虚定时器, 添加时只要在尾部添加, 时间复杂度都为O(1); - unix定时器对象在底层的存储方式为最小堆结构, 增删查定时器复杂度O(logn);
nodejs定时器setInterval,setTimeout,clearTimeout, clearInterval源码学习相关推荐
- 【iScroll源码学习01】准备阶段 - 叶小钗
[iScroll源码学习01]准备阶段 - 叶小钗 时间 2013-12-29 18:41:00 博客园-原创精华区 原文 http://www.cnblogs.com/yexiaochai/p/3 ...
- Electron源码学习: Electron组成与初始化流程
Electron源码学习: Electron组成与结构 前言 最近研究学习Electron的源码结构已经有一些小的进展, 越接触Electron就越发现组成这个软件的大集合不得了.现在学习到的仍然 ...
- Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)
在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十) 我们学到了通过conventional-changelog来生成项目的Changelog更新日志,通过更新日志 ...
- 博通Broadcom SDK源码学习与开发5——ECOS系统层剖析
声明:原创作品,严禁用于商业目的. 本系列文章将全面剖析以Bcm33xxx芯片开发Cablemodem产品的SDK源码为例,从编译系统到各个功能模块进行分析与探讨. 文章目录 0.写在前篇 1. Op ...
- 源码学习-net/http
package net/http是Go语言的主要应用场景之一web应用的基础,从中可以学习到大量前文提到的io,以及没有提到的sync包等一系列基础包的知识,代码量也相对较多,是一个源码学习的宝库.本 ...
- Vuex 4源码学习笔记 - Vuex是怎么与Vue结合?(三)
在上一篇笔记中:Vuex源码学习笔记 - Vuex开发运行流程(二) 我们通过运行npm run dev命令来启动webpack,来开发Vuex,并在Vuex的createStore函数中添加了第一个 ...
- Electron源码学习:Electron加密与安全
Electron加密与安全 引言 目前网络上主要流传的加密就只是网页文件打包成asar和JS混淆加密,以及用addon的方式,这几种方式的话,基本就没有什么破解难度.针对的官方asar的打包,这种 ...
- 【Android 源码学习】SystemServer启动原理
Android 源码学习 SystemServer启动原理 望舒课堂 SystemServer进程启动原理学习记录整理. 参考文章: Android系统启动流程(三)解析SyetemServer进程启 ...
- Shiro源码学习之二
接上一篇 Shiro源码学习之一 3.subject.login 进入login public void login(AuthenticationToken token) throws Authent ...
最新文章
- JDBC+Servlet+JSP整合开发之30-JDBC、Servlet、JSP的MVC
- [Apache] Apache 從 2.2 換至 2.4 httpd.conf 的調整筆記 (windows 環境)
- SQL 2005各版本的区别
- Java queue总结
- 数据可视化|实验五 分析1996-2015年人口数据各个特征的分布与分散状况
- NSArray创建和使用
- linux内核编译及添加系统调用(hdu)_浅谈关于Linux内核write系统调用操作的原子性
- Kotlin学习笔记 第二章 类与对象 第十四 十五节 委托 委托属性
- ssd颗粒查看工具_贴吧机佬强烈推荐的游戏SSD?西数蓝盘3D M.2 500G实测
- Sicily/1927. Conflict
- 自己碰到的一个“无法读取源文件或磁盘”问题处理
- 移动开发用户行为分析神器之--AppSee!
- 企业管理系统可视化权限功能设计
- CST启用GPU加速的调试笔记
- Function类型(函数)
- BTC源码分析 交易(一)
- 喵星球上的点名(后缀自动机+dfs序+莫队)
- ie浏览器下载文件中文名称乱码
- java-net-php-python-jspm零担快跑物流管理系统计算机毕业设计程序
- 9年级计算机主要学的什么好,九年级信息技术下册教学计划