Nodejs 事件循环 promise 在 process.tick 之前执行

Nodejs event loop promise executes before process.tick

提问人:Ozan Başkan 提问时间:8/17/2023 更新时间:8/17/2023 访问量:35

问:


const axiosTest = async () => {

    setImmediate(() => {
        console.log('immediate axios');
    })

    const x = axios.get('https://www.google.com');

    x.then((r) => console.log('fetch'));

    const x2 = new Promise(resolve => {
        resolve('promise axios')
    });

    x2.then(console.log)

    process.nextTick(() => {
        console.log('tick axios')
    });

    console.log('stack axios');
}

const normalTest = async () => {

    setImmediate(() => {
        console.log('immediate');
    })

    const x = new Promise(resolve => {
        resolve('promise')
    });

    x.then(console.log)

    process.nextTick(() => {
        console.log('tick')
    });

    console.log('stack');
}

normalTest().then(axiosTest);

执行顺序:

stack
tick
promise
stack axios
promise axios
tick axios
immediate
immediate axios
fetch

normalTest()

执行顺序:

stack
tick
promise
immediate

axiosTest()

执行顺序:

stack axios
tick axios     
promise axios  
immediate axios
fetch

当我刚刚运行 axiosTest 函数时,tick 在 promise resolve 之前执行,但当我在 normalTest 函数 promise 首先解析之后运行它时。为什么?

我希望 promise.tick 在相同范围内解析任何 promise 之前执行,但它没有。

节点.js 堆栈 es6-promise 事件循环

评论


答:

1赞 trincot 8/17/2023 #1

回调安排在当前“阶段”结束时。文档阶段定义为在事件循环期间处理一个 FIFO 队列:process.nextTick

当 Node.js 启动时,它会初始化事件循环,处理提供的输入脚本(或放入 REPL,本文档未介绍),该脚本可能会进行异步 API 调用、调度计时器或调用 process.nextTick(),然后开始处理事件循环。

下图显示了事件循环操作顺序的简化概述。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

每个框将被称为事件循环的一个“阶段”。

我们可以用这个简化的脚本重现顺序的变化:

const test = async () => {
    console.log('------------test-------------');
    process.nextTick(() => console.log('tick'));
    Promise.resolve().then(() => console.log('promise resolved'))
}

test().then(test)

输出为:

------------test-------------
tick
promise resolved
------------test-------------
promise resolved
tick

不同之处在于,从不同的阶段执行,这意味着不同的顺序。这是一个分步分析,应该澄清这一点:test

当主脚本完成其同步部分(即 已经执行过一次),我们有这样的状态:test

  1. () => console.log('tick')在当前阶段之后等待执行
  2. () => console.log('promise resolved')正在 promise 作业队列中等待
  3. test正在 promise 作业队列中等待(第二个条目)

当同步部分结束时,引擎准备进入事件循环并处理下一阶段。但是,在过渡到下一阶段之前,将执行即时报价回调。所以这解释了“滴答声”是第一位的。

在事件循环的某个时刻,我们进入了 promise 作业阶段(上面的简化图中没有特别指出):这是第一个“promise resolved”被输出,然后被执行(第二次执行)的地方。test

再次会做类似的事情:test

  1. () => console.log('tick')在当前阶段之后等待执行
  2. () => console.log('promise resolved')正在 promise 作业队列中等待

但现在我们应该意识到,我们仍然处于承诺作业阶段,在这个阶段,发现承诺作业队列仍然不是空的(我们只是在那里添加了一个新作业),因此没有过渡到下一阶段,因此现在还不是进行“勾选”的时候。

因此,这一次,“promise resolved”首先输出,然后这个阶段结束,因为 promise 作业队列中不再有 promise 作业:现在是进行勾选的时候了,而 'tick' 是输出。

备注

这种即时报价的概念和在当前阶段之后执行的回调不是 ECMA 脚本语言规范的一部分。nextTick

就我个人而言,我会远离打电话。nextTick