深入解析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 cases
    case 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.0
        args[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 NaN
  if (!(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.2
  this._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 to
  this._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代碼傳入的after
    int 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();
  else
    Reflect.apply(timer._onTimeout, timer, args);
  if (timer._repeat)
    rearm(timer, start);
}

上面的方法執行javascript timer._onTimeout();這裏是真正的回調執行。

至此,一個普通的setTimeout方法執行完畢。

最後總結一下調用流程圖:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章