博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
通过源码解析 Node.js 中高效的 timer
阅读量:5798 次
发布时间:2019-06-18

本文共 4158 字,大约阅读时间需要 13 分钟。

在 Node.js 中,许许多多的异步操作,都需要来一个兜底的超时,这时,就轮到 timer 登场了。由于需要使用它的地方是那么的多,而且都是基础的功能模块,所以,对于它性能的要求,自然是十分高的。总结来说,要求有:

  • 更快的添加操作。

  • 更快的移除操作。

  • 更快的超时触发。

接下来就让我们跟着 Node.js 项目中的 lib/timer.jslib/internal/linklist.js 来探究它具体的实现。

更快的添加 / 移除操作

说到添加和移除都十分高效的数据结构,第一个映入脑帘的,自然就是啦。是的,Node.js 就是使用了双向链表,来将 timer 的插入和移除操作的时间复杂度都降至 O(1) 。双向链表的具体实现便在 lib/internal/linklist.js 中:

// lib/internal/linklist.js'use strict';function init(list) {  list._idleNext = list;  list._idlePrev = list;}exports.init = init;function peek(list) {  if (list._idlePrev == list) return null;  return list._idlePrev;}exports.peek = peek;function shift(list) {  var first = list._idlePrev;  remove(first);  return first;}exports.shift = shift;function remove(item) {  if (item._idleNext) {    item._idleNext._idlePrev = item._idlePrev;  }  if (item._idlePrev) {    item._idlePrev._idleNext = item._idleNext;  }  item._idleNext = null;  item._idlePrev = null;}exports.remove = remove;function append(list, item) {  remove(item);  item._idleNext = list._idleNext;  list._idleNext._idlePrev = item;  item._idlePrev = list;  list._idleNext = item;}exports.append = append;function isEmpty(list) {  return list._idleNext === list;}exports.isEmpty = isEmpty;

可以看到,都是些修改链表中指针的操作,都十分高效。

更快的超时触发

链表的缺点,自然是它的查找时间,对于一个无序的链表来说,查找时间需要 O(n) ,但是,只要基于一个大前提,那么我们的实现就并不需要使用到链表的查询,这也是更高效的超时触发的基础所在,那就是,对于同一延迟的 timers ,后添加的一定比先添加的晚触发。所以,源码的具体做法就是,对于同一延迟的所有 timers ,全部都维护在同一个双向链表中,后来的,就不断往链表末尾追加,并且这条链表实际上共享同一个定时器 。这个定时器会在当次超时触发时,动态计算下一次的触发时间点。所有的链表,都保存在一个对象 map 中。如此一来,既做到了定时器的复用优化,又对链表结构进行了扬长避短。

让我们先以 setTimeout 为例看看具体代码,首先是插入:

// lib/timer.js// ...const refedLists = {};const unrefedLists = {};exports.setTimeout = function(callback, after) {  // ...   var timer = new Timeout(after);  var length = arguments.length;  var ontimeout = callback;  // ...  timer._onTimeout = ontimeout;    active(timer);  return timer;};const active = exports.active = function(item) {  insert(item, false);};function insert(item, unrefed) {  const msecs = item._idleTimeout;  if (msecs < 0 || msecs === undefined) return;  item._idleStart = TimerWrap.now();  var list = lists[msecs];  if (!list) {    // ...    list = new TimersList(msecs, unrefed);    L.init(list);    list._timer._list = list;    if (unrefed === true) list._timer.unref();    list._timer.start(msecs, 0);    lists[msecs] = list;    list._timer[kOnTimeout] = listOnTimeout;  }  L.append(list, item);  assert(!L.isEmpty(list));}

即检查当前在对象 map 中,是否存在该超时时间(msecs)的双向链表,若无,则新建一条。你应该已经看出,超时触发时具体的处理逻辑,就在 listOnTimeout 函数中:

// lib/timer.js// ...function listOnTimeout() {  var list = this._list;  var msecs = list.msecs;  var now = TimerWrap.now();  var diff, timer;  while (timer = L.peek(list)) {    diff = now - timer._idleStart;    if (diff < msecs) {      this.start(msecs - diff, 0);      return;    }    L.remove(timer);    // ...    tryOnTimeout(timer, list);    // ...  }  this.close();  // ...}

即不断从链表头取出封装好的包含了注册时间点处理函数的对象,然后挨个执行,直到计算出的超时时间点已经超过当前时间点。

举个图例,在时间点 10,100,400 时分别注册了三个超时时间为 1000 的 timer,在时间点 300 注册了一个超时时间为 3000 的 timer,即在时间点 500 时,对象 map 的结构即为:

3.pic.jpg

随后在时间点 1200 触发了超时事件,并在时间点 1300 执行完毕,彼时对象 map 的结构即为:

4.pic.jpg

setInterval 和 setImmediate

setInterval 的实现总体和 setTimeout 很相似,区别在于对注册的回调函数进行了封装,在链表的尾部重新插入:

// lib/timer.js// ...function wrapper() {  timer._repeat(); // 执行传入的回调函数  if (!timer._repeat)    return;  // ...  timer._idleTimeout = repeat;  active(timer);}

setImmediatesetTimeout 实现上的主要区别则在于,它会一次性将链表中注册的,都执行完:

// lib/timer.js// ...function processImmediate() {  var queue = immediateQueue;  var domain, immediate;  immediateQueue = {};  L.init(immediateQueue);  while (L.isEmpty(queue) === false) {    immediate = L.shift(queue);    // ...    tryOnImmediate(immediate, queue);    // ...  }  if (L.isEmpty(immediateQueue)) {    process._needImmediateCallback = false;  }}

所以作为功能类似的 process.nextTicksetImmediate ,在功能层面上看,每次事件循环,它们都会将存储的回调都执行完,但 process.nextTick 中的存储的回调,会先于 setImmediate 中的执行:

'use strict'const print = (i) => () => console.log(i)process.nextTick(print(1))process.nextTick(print(2))setImmediate(() => {  print(3)()  setImmediate(print(6))  process.nextTick(print(5))})setImmediate(print(4))console.log('发车')// 发车// 1// 2// 3// 4// 5// 6

最后

参考:

转载地址:http://lxsfx.baihongyu.com/

你可能感兴趣的文章
VC中实现文字竖排的简单方法
查看>>
程序员常用的六大技术博客类
查看>>
深入理解浏览器的缓存机制
查看>>
又拍云沈志华:如何打造一款安全的App
查看>>
dubbo源码分析-架构
查看>>
6套毕业设计PPT模板拯救你的毕业答辩
查看>>
Windows phone 8 学习笔记
查看>>
我的友情链接
查看>>
sshd_config设置参数笔记
查看>>
LeetCode--112--路径总和
查看>>
感悟贴2016-05-13
查看>>
百度编辑器ueditor 光标位置的坐标
查看>>
DEV-C++ 调试方法简明图文教程(转)
查看>>
Sublime Text 2 技巧
查看>>
参加婚礼
查看>>
刚毕业从事java开发需要掌握的技术
查看>>
vim
查看>>
Java重写equals方法和hashCode方法
查看>>
Spark API编程动手实战-07-join操作深入实战
查看>>
H3C-路由策略
查看>>