nodejs定时器setInterval,setTimeout,clearTimeout, clearInterval源码学习

nodejs Timer


本文章最好配合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);

即调用的是HandleWrapUnref方法:

// 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是否在activeactive_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;
}
  1. active(timer)
    • 激活定时器就是将其挂在TimerList上, 调用list的insert(timer, false)函数.
    • 其实现可以看到, 这里的定时器分了两种, 分别为链表集:unrefedListsrefedLists, setTimeoutunrefed=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);
}
  1. new TimeWrap()创建C定时器
    createTimersList()时, 整个TimerList公用一个定时器, 返回给了_timer, 且指定了当前的list的超时时间:
function TimersList(msecs, unrefed) {
  ...
  this._timer = new TimerWrap();
  ...
  this.msecs = msecs;
}
  1. L.append()将当前定时器挂在链表上
    参考linkedlist.js的实现, L.append()是在链表末尾挂载定时器(相同timeout, 后挂载的超时事件越晚);

  2. 分析TimersList的创建
    下面初始化链表创建了一个TimerWrap对象, 后面的源码分析说明, 这里的_timer是一个C定时器对象

// createTimersList()
const list = new TimersList(msecs, unrefed);
// TimersList()
 this._timer = new TimerWrap();
  1. unref
    这里对引用参数进行了判断, 其底层调用了uv_unref, 如果为true, 则将该定时器标记为非REF
// createTimersList()
if (unrefed === true) list._timer.unref();
  1. 定时器启动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).

  1. 定时器链表超时检查与调用回调
    下面的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. 定时器链表的清除工作
    上面的代码给出了定时器链表为空时做的清除工作:
    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, 删引用REF
  return 0;
}

2) 释放链表的内存delete ...

TimerWrap

TimerWrap()

看看TimerWrap干了些什么, 下面的构造函数里调用了父类HandlerWrap的构造函数, 然后执行了初始化uv_timer_init:

// timer_wrap.cc
 TimerWrap(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_;

其又包含(继承)通用结构, 该结构中有dataloop对象:

#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定时器

  1. 其实现与setTimeout一样, 只是创建Timeout对象后, 设置了timer._repeat = repeat;
  2. 在执行回调函数后, 判断了重复
// 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件事:
    1. 实例化Immediate对象,并初始化;
    2. 将对象挂载到全局的立即执行回调队列immediateQueue;
    3. 同时, 对全局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 callback
    var 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;
    else
      immediate = 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_stopuv_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总结

  1. let timer = setTimeout(cb, t);函数返回的不是一个真的定时器对象, 而是一个js对象Timeout, 该对象是TimersList链表的节点类型, 这里我称它为虚定时器;
  2. TimersList身上的_timer才是真正的C定时器对象, 每个list上的所有虚定时器共享这个_timer, 同一个链表上的所有虚定时器有相同的超时时间;
  3. 链表本身是由list节点链接首尾节点组成的闭合链表, 检查超时每次只要取头部虚定时器, 添加时只要在尾部添加, 时间复杂度都为O(1);
  4. unix定时器对象在底层的存储方式为最小堆结构, 增删查定时器复杂度O(logn);
阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
关于NodeJS点餐系统源码,需要先明确一些概念。NodeJS是运行在服务器端的JavaScript语言,它具有事件驱动、非阻塞I/O等特点,因此可以轻松地开发高性能的网络应用程序。 点餐系统,是指通过网络进行点餐的系统。这种系统需要与用户互动,提供菜单、下单、结算等功能,并与后台进行交互,包括更新菜单、订单管理、支付等。 在开发NodeJS点餐系统源码时,主要需要考虑以下几个方面: 1. 前端界面设计:包括菜单展示、购物车、订单确认、支付等界面设计,需要考虑用户体验,易用性和美观度。 2. 后台接口设计:需要定义前端和后台交互的接口,包括获取菜单、添加菜品、下单、结算等功能。这些接口需要满足RESTful风格,保证接口清晰明确,易于维护。 3. 数据库设计:点餐系统需要存储菜单、订单、用户信息等数据,需要设计合理的数据库结构,保证数据安全可靠。 4. 系统架构:NodeJS点餐系统需要考虑分布式架构,提高系统的可扩展性、高可用性等。 在实现上述方面时,我们可以采用一些现有的技术,比如Express框架、Sequelize ORM库、MySQL数据库等。这些技术都可以轻松地与NodeJS集成,提高开发效率和代码质量。 总之,NodeJS点餐系统源码的开发需要综合考虑前端界面、后台接口、数据库设计和系统架构等方面,并采用现有技术和工具提高开发效率和代码质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呵离

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值