嵌套的process.nextTick问题
发布于 4 年前 作者 theanarkh 1908 次浏览 来自 分享

​假设我们有以下代码

function main() {
  process.nextTick(() => {
    main();
  })
}
main();
setTimeout(() => {
  console.log(2)
},0);

那么2会输出吗?答案留到最后揭晓,有兴趣的同学可以先思考一下。我们分析一下这个过程。我们首先看一下nextTick的实现。

function nextTick(callback) {
  let args;
  // 参数处理
  switch (arguments.length) {
    case 1: break;
    case 2: args = [arguments[1]]; break;
    case 3: args = [arguments[1], arguments[2]]; break;
    case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
    default:
      args = new Array(arguments.length - 1);
      for (let i = 1; i < arguments.length; i++)
        args[i - 1] = arguments[i];
  }
  // 第一个任务的话,开启tick处理逻辑,开启了说明有tick任务需要处理
  if (queue.isEmpty())
    setHasTickScheduled(true);
  // 生成一个新的节点
  const tickObject = {
    [async_id_symbol]: asyncId,
    [trigger_async_id_symbol]: triggerAsyncId,
    callback,
    args
  };
  // 插入队列
  queue.push(tickObject);
}

我们看到nextTick的实现很简单,就是生成一个节点,保存一些上下文,然后插入队列中。接着我们看一下处理tick任务的逻辑。

function processTicksAndRejections() {
  let tock;
  do {
    // 每次nextTick时就会往queue中插入一个节点
    while (tock = queue.shift()) {
      try {
        // nextTick的第一个函数,即回调
        const callback = tock.callback;
        // 执行回调
        if (tock.args === undefined) {
          callback();
        } else {
          const args = tock.args;
          switch (args.length) {
            case 1: callback(args[0]); break;
            case 2: callback(args[0], args[1]); break;
            case 3: callback(args[0], args[1], args[2]); break;
            case 4: callback(args[0], args[1], args[2], args[3]); break;
            default: callback(...args);
          }
        }
      } 
    }
  } while (!queue.isEmpty() || processPromiseRejections());
}

至此,我们或许已经知道了答案。就是while那里的queue队列一直在移除和追加,导致了死循环。nodejs早期版本限制了nextTick的嵌套深度,后来去掉了( process: remove maxTickDepth (Trevor Norris))。如果这个死循环不是符合预期的(即执行队列的回调时,回调又往队列里追加节点,导致死循环),那么我们可以怎么解决这个问题?其实nodejs中有很多这种场景,也给了解决方案。我们看一下如何处理该问题。

static void uv__run_closing_handles(uv_loop_t* loop) {
  uv_handle_t* p;
  uv_handle_t* q;
  // 先保存对队列的引用
  p = loop->closing_handles;
  // 重置头指针
  loop->closing_handles = NULL;
​
  while (p) {
    q = p->next_closing;
    uv__finish_close(p);
    p = q;
  }
}

uv__run_closing_handles是libuv close阶段的处理函数。我们看到nodejs中是如何处理这个问题的,首先保存对待处理队列的引用,然后重置头指针。接着遍历处理待处理的队列,这时候即使回调里追加节点,也只会追加到原来的队列中,而不是追加到待处理队列导致死循环。今天就分析到这,谢谢。

回到顶部