为什么 Promise 在声明时就开始执行?

8

我想使用Promise.all()来执行一组承诺。我的方法是将这些承诺放入一个数组中,然后将该数组传递给Promise.all()。

然而,我发现这些承诺在声明时就开始执行,甚至不等待Promise.all()的调用。

为什么会出现这种情况,如何才能使承诺在调用Promise.all()时才开始执行呢?

let promiseArray = [];
const values = [1, 2, 3, 4, 5];

values.forEach((value)=>{
  promiseArray.push(
    new Promise((resolve, reject)=>{
      console.log('value: ' + value);
      return resolve();
    })
  )
})


console.log('start')
Promise.all(promiseArray)

/*
 output is 

  value: 1
  value: 2
  value: 3
  value: 4
  value: 5
  start

 would have expected output to have been

  start
  value: 1
  value: 2
  value: 3
  value: 4
  value: 5
*/


3
这就是它们设计的工作方式 - Promise 执行程序会立即同步执行... 为什么没有简单的答案。 - Jaromanda X
如果你想让代码按照你的期望运行,只需更改一行代码... promiseArray.push(() => 然后 Promise.all(promiseArray.map(fn => fn())) - Jaromanda X
2
你的直觉其实是合理的。急切地执行Promises构造函数的设计决策至少是值得质疑的。 - user6445533
2个回答

13
把 promise 看作“执行”的方式会让你感到困惑。Promise 只是一种通知机制。它通常与某些底层异步操作相关联,当你创建 Promise 时,异步操作通常已经开始了。
然后使用 Promise.all() 跟踪一堆你已经启动的异步操作何时完成(或以错误结束)。
因此,你不使用 Promise.all() 来启动一堆东西。你只用它来跟踪它们何时全部完成,并且它们在你的代码中的其他地方启动。
当你使用 new Promise() 手动创建一个 Promise 时,Promise 执行程序会立即执行。这就是它们的设计方式。
如果你的 Promise 执行程序中有真正的异步操作,并且在这些操作完成时使用 console.log(),那么你可能不会对 Promise 的设计方式感到困惑。我认为你的大部分困惑源于你的 Promise 执行程序实际上没有任何异步操作,因此根本没有理由使用 Promise。Promise 的设计是为了跟踪异步操作的完成情况。如果你没有真正的异步操作,就没有使用它们的理由。
另外,如果你想从 Promise 执行程序内部的某个时间开始某些异步操作,可以使用普通的 setTimeout()、process.nextTick() 或 setImmediate() 操作来安排稍后启动操作。
此外,似乎你希望输出结果按照严格的顺序进行。Promise.all() 期望有 N 个异步操作并行运行,并且这些 N 个操作的完成顺序没有保证。相反,Promise.all() 将跟踪它们所有的操作,收集所有结果,并将一个结果数组按顺序(如果它们全部成功解析)呈现给 .then() 处理程序。它不会按顺序运行操作本身。操作并行运行,并以任何自然顺序完成。

1
这真是让我大开眼界! 我一直以为 Promise 是“特殊”的函数,而不是“返回”的函数,因此适用于异步操作。 现在我只需要去弄清楚为什么我的当前代码能够正常工作 ¯_(ツ)_/¯。 - takinola
3
@takinola - 这并不是一个罕见的困惑。但是,如果你有一个返回promise的函数,请这样考虑。该函数启动异步操作,然后创建一个新的promise并将其连接到监视该异步操作。然后该函数返回该promise,就像一个跟踪器,您可以使用它来知道异步操作何时完成以及完成的结果。Promise本身不是操作,而只是一个跟踪机制。通常情况下,一个函数既会启动异步操作,又会获取一个promise来跟踪它。 - jfriend00
1
我终于可以说我对承诺不是完全一无所知了! - undefined

2
是的,Promise立即执行,它们没有其他方式可以工作。Promise和JS中的任何其他对象一样,完全不知道谁在引用它们以及它们被传递的方式,因此它们无法“等待”,直到某些特定的操作完成对它们的引用。
在您的情况下,解决方案是在将Promise传递给Promise.all()时创建它们:
let promiseFnArray = [];
const values = [1, 2, 3, 4, 5];

values.forEach((value) => {
  promiseFnArray.push((resolve, reject) => {
    console.log('value: ' + value);
    return resolve();
  }))
})

console.log('start')
Promise.all(promiseFnArray.map(promiseFn => new Promise(promiseFn));

此外,一旦您的承诺实际上使某些内容异步化,它们不一定会按原始顺序解决。要按正确顺序获取结果,可以使用Promise.all( ... ).then(),这也是Promise.all()的整个目的。


[...]它们不能以其他方式工作。[...]这不是真的,一个new Promise可以使用setImmediate延迟传递给构造函数的回调函数的执行,然后start会出现在value:输出之前。但这没有太多意义,因为Promise通常用于观察异步代码(请参见jfriend00的答案)。 - t.niese
据我所知,setImmediate不是标准的,它不会延迟Promise执行器的调用,它只是意味着你将代码包装在一个单独的延迟中。这与Promise本身或其传递方式无关。 "不能以其他方式工作"是指Promise将能够根据引用它的变量(在这种情况下被传递到Promise.all())发生的情况来调用执行器。 - Lennholm
是的,setImmediate/setTimeout ...并不是语言规范的一部分,我只是用它作示例。问题解决者想知道为什么value后面会显示start。通常会出现这种假设,因为Promises用于异步代码,在nodejs中,可能被调用异步的所有回调都应该被异步调用(所有这些 err, value回调)。因此,有些人期望传递给new Promise的回调也像那些err, value回调一样被异步调用。Promise实现当然可以这样做,但这没有意义。 - t.niese

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接