两个promise问题?

第一个问题,这个为什么先输出3后输出2呢

      async function p2() {
        return Promise.resolve('p2');
      }

      async function p3() {
        return await Promise.resolve('p3');
      }

      p2().then((res) => console.log(res));
      p3().then((res) => console.log(res));

第二个问题,这个为什么会交错输出呢,142536

new Promise((r) => {
    r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))

new Promise((r) => {
    r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))

promise小白一枚,上述两个疑问,希望能得到详细的解答

阅读 2.9k
avatarAI BotBETA

第一个问题解答

为什么这段代码先输出 "3" 后输出 "2"?

async function p2() {
    return Promise.resolve('p2');
}

async function p3() {
    return await Promise.resolve('p3');
}

p2().then((res) => console.log(res));
p3().then((res) => console.log(res));

解释

  1. p2 函数:这个函数通过 return Promise.resolve('p2'); 直接返回了一个已解决的 Promise。注意这里并没有使用 await,所以函数执行到 return 时就会立即返回一个 Promise 对象,而不会等待 Promise 解决。
  2. p3 函数:这个函数通过 return await Promise.resolve('p3'); 返回了一个 Promise。由于使用了 await,函数会等待 Promise.resolve('p3') 解决后再继续执行并返回结果。尽管 Promise.resolve('p3') 几乎立即解决,但使用 await 会使得 p3() 函数的执行暂停,直到 Promise 解决。
  3. 执行顺序:由于 p2() 立即返回一个 Promise 并继续执行 .then() 链,而 p3() 等待其内部的 Promise 解决后再继续,因此 p2().then() 链会先被添加到微任务队列中。当当前执行栈清空后(即同步代码执行完毕后),JavaScript 引擎会处理微任务队列中的任务,因此 p2().then() 中的 console.log(res) 会先执行,输出 "2"。随后,p3() 中的 Promise 解决,p3().then() 中的 console.log(res) 才会执行,输出 "3"。

第二个问题解答

为什么这段代码交错输出 "142536"?

new Promise((r) => {
    r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))

new Promise((r) => {
    r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))

解释

  1. Promise 链的执行:两个 Promise 链几乎是同时被创建的,并且它们各自内部的 Promise 都是立即解决的(通过 r();)。但是,每个 .then() 方法调用都会将一个新的回调函数添加到各自的 Promise 解决后的微任务队列中。
  2. 微任务队列:JavaScript 有一个微任务队列,用于存放需要在当前执行栈清空后立即执行的异步操作(如 Promise 的解决回调)。由于这两个 Promise 链都是立即解决的,它们的 .then() 回调会被依次添加到微任务队列中。
  3. 执行顺序:当同步代码执行完毕后,JavaScript 引擎会检查微任务队列,并按顺序执行队列中的任务。因此,第一个 Promise 链的 .then() 回调(输出 "1"、"2"、"3")会按照它们被添加到队列中的顺序依次执行,随后是第二个 Promise 链的 .then() 回调(输出 "4"、"5"、"6")。这就导致了交错输出 "142536"。

总结:尽管这两个 Promise 链在代码中看起来是连续书写的,但它们各自的 .then() 回调会根据它们被添加到微任务队列的顺序来执行,而不是根据它们在代码中的位置。

5 个回答

第一个问题比较取巧。差异的部分就是如果 async 函数中 await 的处理过程。
这个部分稍微转换一下能够帮助你的理解:

console.log('主进程开始')

async function p2() {
  console.log('p2开始执行')
  const p2v = new Promise(resolve => {
    resolve('P2')
    console.log('p2值已返回')
  });
    console.log('p2执行结束')
  console.log('p2值 =>', p2v)
  return p2v
}

async function p3() {
  console.log('p3开始执行')
  const p3v = await new Promise(resolve => {
    resolve('p3')
    console.log('p3/await值已返回')
  });
  console.log('p3执行结束')
  console.log('p3值 =>', p3v)
  return p3v
}

p2().then((res) => {
  console.log(res + '.then 执行')
});
p3().then((res) => {
  console.log(res + '.then 执行')
});

console.log('主进程结束')

输出结果是这样的:

// logs:
// 主进程开始
// p2开始执行
// p2值已返回
// p2执行结束
// p2值 => Promise {[[PromiseState]]: 'fulfilled', [[PromiseResult]]: 'P2', Symbol(async_id_symbol): 9, Symbol(trigger_async_id_symbol): 1}
// * P.S. 此时 P2.then() 入栈
// p3开始执行
// p3/await值已返回
// * P.S. 此时 P3.then() 入栈
// 主进程结束
// p3执行结束
// p3值 => p3
// p3.then 执行
// P2.then 执行

可以看到,在执行到 证明p3/await是同步任务 时, p3 这个 async 函数的执行被暂停了(p3.then() 此时被入栈),继续回到主进程执行剩余的业务代码。

当主进程执行完毕之后,立即回到了 p3.await 之后的代码块恢复 p3 函数的执行(因为 p3.await 已经被兑现了 resolve)。

p3 函数全部执行完毕之后,此时和我们认知冲突的部分就开始出现了。执行完 return p3v 之后 并没有按照 p2.thenp3.then 的入栈顺序开始执行 p2.then(),而是直接执行了 p3.then() 中的内容

为什么会这样,AI回答了一部分也和我的猜想是相符合的,就是返回的值是有一些区别的:

p2 函数:这个函数通过 return Promise.resolve('p2'); 直接返回了一个已解决的 Promise。注意这里并没有使用 await,所以函数执行到 return 时就会立即返回一个 Promise 对象,而不会等待 Promise 解决。
p3 函数:这个函数通过 return await Promise.resolve('p3'); 返回了一个 Promise。由于使用了 await,函数会等待 Promise.resolve('p3') 解决后再继续执行并返回结果。尽管 Promise.resolve('p3') 几乎立即解决,但使用 await 会使得 p3() 函数的执行暂停,直到 Promise 解决。

也就是说, p2 函数返回了一个完整的 Promise 对象(虽然它的状态是 fulfilled 的),而 p3 函数返回了一个 Promise 被兑现之后的值,所以先输出了 p3.then 执行。随后 p2() 中的 Promise 被解决输出 P2.then 执行

并不是完全因为 awiat 的执行暂停,如果稍作修改执行的结果就会完全不一样,即使修改后的代码也会等待 p3 函数的 await 执行结束,并返回一个和 p2 函数一样的返回值。

async function p2() {
  const p2v = new Promise(resolve => {
    resolve('P2')
  });
  return p2v
}

async function p3() {
  const p3va = new Promise(resolve => { resolve('p3a')})
  const p3vb = await new Promise(resolve => {
    resolve('p3b')
  });
  return p3va
}

p2().then((res) => {
  console.log(res + '.then 执行')
});
p3().then((res) => {
  console.log(res + '.then 执行')
});
// logs:
// P2.then 执行
// p3a.then 执行
😂 但是这块我也不是很确定,只是从现象反推的结论。
如果有大佬能够找到标准文档或者更标准的回答是最好不过了。

但其实实际业务中绝大部分时间我们并不会这样去用,基本上会用以下的方式来使用:

function p2() {
  return new Promise(resolve => {
    setTimeout(() => resolve('p2'), 10)
  });
}

async function p3() {
  console.log('证明p3是同步任务1')
  const p = await new Promise(resolve => {
    console.log('证明p3是同步任务2')
    setTimeout(() => {
      console.log('证明p3是同步任务3')
      resolve('p3')
    }, 10)
  });
  console.log('证明p3是同步任务4')
  return p
}

p2().then((res) => console.log(res + '.then 执行'));
p3().then((res) => console.log(res + '.then 执行'));
// logs:
// 证明p3是同步任务1
// 证明p3是同步任务2
// p2.then 执行
// 证明p3是同步任务3
// 证明p3是同步任务4
// p3.then 执行

这样是不是执行结果就很明确了。


第二个问题就是简单的JS事件循环机制(EventLoop)了,按照执行顺序依次入栈出栈。表现出来的就是交替输出。
没有什么好说的,稍微理解一下事件循环机制就好了。

两个问题原因是一样的,.then 和 Promise.resolve 返回的都是 Promise,可以认为是在执行后才会产生一个新的微任务,排队后才能触发之后的流程。

以第一个问题举例,可以理解流程是这样的:

  1. p2 函数调用进入队列
  2. p3 函数调用进入队列
  3. p2 函数执行,返回 Promise.resolve('p2') 进入队列
  4. p3 函数执行,返回 (await Promise.resolve('p3') 也就是 'p3') 进入队列,p3().then 可以被执行了
  5. Promise.resolve('p2') 执行,返回结果 'p2', p2().then 可以被执行了
  6. 【所以按照顺序】p3 先触发、p2 再触发

第二个问题也是类似的,执行完输出 1 的 then callback 之后下一个 then 对应的触发才会进入队列,所以两个 Promise 交错触发。

第一个问题:

  1. async function执行返回promise,p2和p3执行后先后注册微任务
  2. 同步代码执行完轮到p2的回调执行,return Promise.resolve('p2')又将一个微任务入队了
  3. p3的回调执行,await强制了后续的执行
  4. 由于p3()返回的promise的状态变为了fulfilled,then()的回调就执行打印了'p3'
  5. 接下来就是剩下的微任务执行,然后then()的回调执行打印了'p2'

第二个问题:
你需要理解Promise/A+规范中对then的定义就是:
链式调用时,后一个then只会在前一个then返回的promise处理完状态变更为fulfilled才会执行(不完全是这样但是等效)

原因是p2比p3多一个ticks,规范如下:

// 直接返回promise需要 2 个 ticks后才获得结果,
const direct = (async () => Promise.resolve(1))();

// 返回await pormise需要 1 个ticks后获得结果
const awaited = (async () => await Promise.resolve(1))();

很容易验证 p2要比p3多一个ticks

p3().then((res) => console.log(res)).then(() => console.log(4)).then(() => console.log(5));
p2().then((res) => console.log(res));
// 打印顺序是:p3 4 p2 5
p2().then((res) => console.log(res));
p3().then((res) => console.log(res)).then(() => console.log('4')).then(() => console.log(5));

// 打印顺序是:p3 p2 4 5

为什么return Promise.resolve('p2')需要2个ticks。returen的值是Promise.resolve('p2'),发现该值包含可调用的「then」属性,无论该 Promise 实例是否解决,都将 enqueue 一个新任务包(1个tick)含调用该返回值的 then (1个tick)的任务(即规范中的 NewPromiseResolveThenableJob(promiseToResolve, thenable, then)),这里就多了2次ticks。
https://www.zhihu.com/question/453677175

为什么await Promise.resolve('p3') 只需要 1个tick。如果你使用node12之前的版本测试会发现先打印 p2 之后打印 p3。原因是v8之后对async await做了优化,之所有这个优化起因是node8中的一个bug。

巨大提升!更快的 async 函数和 promises

规范性:减少async/await中的tick数量

最新的提案中也有提出直接返回 promise 可以减少 ticks:
https://github.com/tc39/proposal-faster-promise-adoption

新手上路,请多包涵

转换为Promise写法后,如下所示:

console.log('主执行栈开始执行')
const p2a = new Promise((resolve) => {
  const p2 = Promise.resolve('p2') // 返回一个Promise, p2 的状态为 fulfilled, 值是 p2
  console.log(p2, 'p2 before')
  resolve(p2) // 这里resolve的是p2, 返回的是p2, 又因为 p2 是一个 Promise 对象,所以创建一个如下的(当前 microtask队列变成 [promiseResolveThenableJobTask])
  console.log(p2, 'p2 after')
})
// let promiseResolveThenableJobTask = () => {
//   p2.then((value) => {
//       ResolvePromise(a2, value) // ResolvePromise 的作用上面有介绍
//   })
// }
// 这个时候 p2a promise的状态应该还是 pending 状态, 也就是说 p2a 还没有调用 ResolvePromise
// 注意此时 p2a 还不是 fulfilled 状态,因为他需要等待 promiseResolveThenableJobTask 执行时来调用他的 resolve 才会变成 fulfilled。
console.log(p2a, 'p2a') // 这里 p2a 是 pending 状态
// 然而执行到这里的时候,又因为p2a 还没有fulfilled,所以不会将其thenable放入微任务队列中去
p2a.then((res) => {
  console.log(res, 'p2a thenable')
})

// 执行该 promise 时,执行完毕后这里 p3a 就变成了 fulfilled 状态了
const p3a = new Promise((resolve) => {
  resolve('p3') // 这里resolve的是一个值
})

console.log(p3a, 'p3a') // 这里 p3a 是 fulfilled 状态

// 执行到这里的时候p3a因为是fulfilled状态,所以会将其thenable放入微任务队列中去,这时候微任务队列就变成了 [promiseResolveThenableJobTask, p3a.thenable]
p3a.then((res) => {
  console.log(res, 'p3a thenable')
})

console.log('主执行栈执行完毕')
// 执行到这里的时候主执行栈执行完毕

// 此时微任务队列中的任务有:  [promiseResolveThenableJobTask, p3a.thenable]
// 1. 将promiseResolveThenableJobTask 从微任务队列中取出执行 p2a 就变成了 fulfilled 状态了,此时会把上面p2a的thenable放入微任务队列中去,
// 这时候微任务队列就变成了 [p3a.thenable, p2a.thenable]
// 2. 将p3a.thenable 取出来执行 打印 p3
// 3. 将p2a.thenable 取出来执行 打印 p2
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏