关于promise输出顺序的疑问?
4 个回答
.then
回调可读性很差,尽量不要直接使用,用 await
, 这里只是做学术探讨。
虽问题很长,但表达清晰,不过还是会有读者看不懂问题。我的回答也只回答作者关心的,就是在 .then
回调函数里 return
一个已经 fulfilled 的 promise 和什么都不返回也就是 return undefined
有什么不同?
作者显然认为两者应该一样, 都应该让这个 .then()
返回的 promise 对象的状态从 pending 立刻变为 fulfilled,也就是会让再后面的那个 .then
回调立刻进入 microtask 队列。
但其实不是的,如果在一个 pending 状态的 promise 对象(p
)的 .then
回调里返回一个 promise 对象( p2
),或者任意带有 then
方法的对象,引擎会专门起一个额外的 microtask/job 去执行这个 p2
的 then
方法,同时把 p
的 [[resolve]]
和 [[reject]]
函数作为参数传过去,虽然 p2
已经 fulfilled 了,但它能做的也就是把 [[resolve]]
函数立刻放到 microtask 队列里,这样也就过了两个 microtask,这时 p
才会被 fulfill,p 后面的 console.log('内部第二个then')
才会被放入队列。
所以你才看到了慢两拍的表现,只有两拍,如果你 return
的是个能同步执行 [[resolve]] 函数的 普通 thenable 对象,那就仅仅会慢一拍,比如你可以返回 {then(resolve){resolve()}}
试试。
规范步骤在这里
Promise
是 JavaScript 中的对异步操作最佳的 API 之一。作为JavaScript开发人员,需要熟练掌握 Promise
相关的知识。本文就来总结一下这些知识点,先来回顾 JavaScript 过去异步操作的处理方式?然后详细介绍 Promises 对象及相关的方法。
现在先来了解一下 JavaScript 异步概念。
异步回顾
什么是异步?如果在函数返回的时候,调用者还不能获取到预期结果,而是将来通过一定的方式得到(例如回调函数),这函数就是异步。
异步回调
异步回调,就是常见的 callback
,这是过去在 JavaScript 中处理异步操作的常见方式,如 AJAX ,发起一个HTTP请求,具体服务器什么时候返回响应数据取决于客户的环境,存在很多不确定因素,这时采用回调函数可以在返回响应数据的时候触发回调函数。
这种方式当有多个请求的情况,后续请求需要等待前面请求的响应数据的时候,就会出现常见的回调地狱。
const asyncMessage = function (message, callback) {
setTimeout(function () {
console.log(message);
callback();
}, 1000);
};
asyncMessage("title", function () {
asyncMessage("cate", function () {
asyncMessage("content", function () {
console.log("detail");
});
});
});
出现回调地狱将会给项目开发带来很多问题:
- 会导致逻辑混乱,耦合性高,改动一处就会导致全部变动,嵌套多时,BUG问题难以发现。
- 不能使用
try...catch
来捕获异常。 - 不能使用
return
返回真实数据
为了避免回调地狱的出现,后来就有了 Promise
对象。
Promise 的工作方式
promise
对象是一个可以从异步函数同步返回的对象,它将处于 3 种可能的状态之一:
fulfilled
已兑现: onFulfilled()
将被调用,即操作完成(例如,resolve()
被调用) rejected
已拒绝: onRejected()
将被调用,即操作失败(例如,reject()
被调用) pending
待定:初始状态,既没有被兑现,也没有被拒绝
如果一个 promise
没有挂起(它已被解决或拒绝),它就会被解决。有时使用已解决和已解决表示同一件事:不是 pending
。
promise
一旦确定,就不能再重新确定,再次调用 resolve()
或reject()
将没有效果。一个已确定的 promise
的具有不可变性。
对于 promise
的状态监控可以使用承诺链,即在 fulfilled
已兑现的状态可以使用 then
方法可以获取已兑现的结果,在rejected
已拒绝状态使用 catch
方法获取拒绝的原因。
const myPromise = new Promise(myExecutorFunc)
.then(handleFulfilledA)
.then(handleFulfilledB)
.then(handleFulfilledC)
.catch(handleRejectedAny);
看起来比 callback
的方式优雅一点,对于需要发起多次HTTP请求才能完整呈现的需求,代码如下:
const getPost = () => fetch("https://jsonplaceholder.typicode.com/posts/1");
const getAuthor = (id) =>
fetch("https://jsonplaceholder.typicode.com/users/" + id);
const getComment = (id) =>
fetch("https://jsonplaceholder.typicode.com/users/" + id);
getPost() // #1.fetch post
.then((postResponse) => postResponse.json()) // #2. get & return post json
.then((postResponse) =>
getAuthor(postResponse.id) // #3. fetch author
.then((authorResponse) =>
authorResponse
.json() // #4 get & return author json
.then((authorResponse) =>
getComment(postResponse.id) // #5 fetch comment
.then((commentResponse) => commentResponse.json()) // #6 get & return comment json
.then((commentResponse) => {
// #7 time to combine all results
return {
postResponse,
authorResponse,
commentResponse,
}; // #8 combine & return all reponses
})
)
)
.then((results) => {
// #9 read all responses
console.log(results.postResponse);
console.log(results.authorResponse);
console.log(results.commentResponse);
})
)
.catch((error) => console.log(error)); // # 10 error handling
上面代码是否有种似曾相识的感觉,原本是为了解决回调地狱,但似乎理想跟现实还是有差距。
于是 ES2021 为 Promise 对象增加新的特征,其中包括:Promise.any()
、Promise.all()
、Promise.allSettled()
、Promise.race()
。
Promise.any()
Promise.any(promises)
能够并行运行 promise
,并解析为 promises
列表中第一个成功解析的 promise
的值。需要注意的是 Promise.any()
方法依然是实验性的,尚未被所有的浏览器完全支持。
下面来看看 Promise.any()
是如何工作的。
1.工作原理
Promise.any()
可用于以并行和竞争方式执行独立的异步操作,以获取任何第一个完成的 promise
的值。
该函数接受一个 promise
数组(通常为一个可迭代对象)作为参数,如下:
const anyPromise = Promise.any(promises);
当输入 promises
中的第一个 promise
被执行完成时,anyPromise
会立即解析为该 promise
的值。
可以使用 then
方法提取第一个 promise
的值:
anyPromise.then((firstValue) => {
firstValue; // 第一个 promise 完成后返回的值
});
也可以使用 async/await
语法:
const firstValue = await anyPromise;
console.log(firstValue); // 第一个 promise 完成后返回的值
Promise.any()
返回的 promise
与任何第一个执行的 promise
一起执行。即使某些 promise
被 rejected
,这些 rejections
也将会被忽略。
但是,如果输入数组中的所有 promises
都被拒绝,或者输入数组为空,那么Promise.any()
会 rejected
包含输入的 promises
执行的 rejection
错误原因集合。
2. 使用指南
现在来深入介绍一下 Promise.any()
, 在这之前,先来定义 2 个简单的函数。
函数resolveTimeout(value, delay)
将返回一个在经过delay
时间后有resolve
的promise
。
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
函数rejectTimeout(reason, delay)
将返回一个在经过delay
时间后有reject
的promise
。
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
接下来使用上面定义的2个辅助函数来试试 Promise.any()
。
2.1 完成所有 promises
下面尝试运行第一个解析列表:
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];
const promise = Promise.any([
resolveTimeout(fruits, 1000),
resolveTimeout(vegetables, 2000),
]);
// 等待...
const list = async () => {
const result = await promise;
console.log(result);
};
// 1 秒之后
list(); // ['potatoes', 'tomatoes']
promise .any([…])
返回一个在 1秒内
解析到数组 fruits
的 promise
,因为解析fruits的 promise
先执行完成。
第二个是2秒内
解析到数组 vegetables
的 promise
,其值将被忽略。
2.2 一个 promise
被 rejected
将上面第一个 promise
出现异常被 rejected
,如下代码:
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
const vegetables = ["oranges", "apples"];
const promise = Promise.any([
rejectTimeout(new Error("fruits is empty"), 1000),
resolveTimeout(vegetables, 2000),
]);
// 等待...
const list = async () => {
const result = await promise;
console.log(result);
};
// 2 秒之后
list(); // [ 'oranges', 'apples' ]
上面的代码,第一个 promise
在 1秒后
被rejected
,从执行的结果不难看出 Promise.any()
跳过了第一个被rejected
的promise
,等待第二个 2秒后
执行完成的promise
。
2.3 所有的 promises
被 rejected
下面来看下当所有的 promises
被 rejected
会出现什么结果,如下代码:
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
const promise = Promise.any([
rejectTimeout(new Error("fruits is empty"), 1000),
rejectTimeout(new Error("vegetables is empty"), 2000),
]);
// 等待...
const list = async () => {
try {
const result = await promise;
console.log(result);
} catch (aggregateError) {
console.log(aggregateError);
console.log(aggregateError.errors);
}
};
list(); // [AggregateError: All promises were rejected]
从上面代码的执行结果来看,当所有输入promises
被 rejected
后, Promise.any([...])
将返回一种特殊的错误 AggregateError
而被 rejected
,而详细的 rejected
原因在属性 aggregateError.errors
中 。
小结
Promise.any()
可用于以竞争方式并行执行独立的异步操作,以获取任何第一个成功执行完成的 promise
的值。如果 Promise.any()
的所有输入 promise
都被rejected
后,那么辅助函数返回的 promise
也会以错误集合的方式拒绝,该错误在一个特殊属性 AggregateError
中包含输入 promise
的拒绝原因:aggregateError.errors
。
Promise.all()
方法 Promise.all(promises)
,能够一次并行处理多个 promise
,并且只返回一个 promise
实例, 那个输入的所有 promise
的 resolve
回调的结果是一个数组。
下面来看看 Promise.all()
是如何工作的。
1.工作原理
Promise.all()
是一个内置的辅助函数,接受一组 promise
(或者一个可迭代的对象),并返回一个promise
:
const allPromise = Promise.all([promise1, promise2, ...]);
可以使用 then
方法提取第一个 promise
的值:
allPromise.then((values) => {
values; // [valueOfPromise1, valueOfPromise2, ...]
});
也可以使用 async/await
语法:
const values = await allPromise;
console.log(values); // [valueOfPromise1, valueOfPromise2, ...]
Promise.all()
返回的 promise
被解析或拒绝的方式。
如果 allPromise
都被成功解析,那么 allPromise
将使用一个包含各个 promise
已执行完成后的值的数组作为结果。数组中 promise
的顺序是很重要的——将按照这个顺序得到已实现的值。
但是如果至少有一个 promise
被 rejected
,那么 allPromise
会以同样的原因立即 rejected
(不等待其他 promise
的执行)。
如果所有的 promise
被 rejected
,等待所有的promise
执行完成,但只会返回最先被rejected
的promise
的 reject
原因。
2. 使用指南
现在来深入介绍一下 Promise.all()
, 在这之前,先来定义 2 个简单的函数。
函数resolveTimeout(value, delay)
将返回一个在经过delay
时间后有resolve
的promise
。
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
函数rejectTimeout(reason, delay)
将返回一个在经过delay
时间后有reject
的promise
。
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
接下来使用上面定义的2个辅助函数来试试 Promise.all()
。
2.1 完成所有 promises
下面定义了一个 promise
数组 allPromise
,所有的 promise
都能够成功的 resolve
值,如下:
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];
const allPromise = [
resolveTimeout(fruits, 2000),
resolveTimeout(vegetables, 1000),
];
const promise = Promise.all(allPromise);
// 等待... 2秒后
const list = async () => {
try {
const result = await promise;
console.log(result);
} catch (error) {
console.log(error.errors);
}
};
list(); // [ [ 'potatoes', 'tomatoes' ], [ 'oranges', 'apples' ] ]
从上面执行的结果来看 Promise.all()
返回的 promise
的 resolve
数组是按照执行前 allPromise
的顺序组成其结果。
promise
数组的顺序直接影响结果的顺序,和promise
执行完成的先后无关。
2.2 一个 promise
被 rejected
将上面数组 allPromise
的第一个 promise
出现异常被 rejected
,如下代码:
const promise = Promise.all([
rejectTimeout(new Error("fruits is empty"), 5000),
resolveTimeout(vegetables, 1000),
]);
// 等待...
const list = async () => {
try {
const result = await promise;
console.log(result);
} catch (error) {
console.log(error);
}
};
list(); // Error: fruits is empty
然而,在经过 5秒
之后,第一个 promise
由于异常被 rejected
,使得 allPromise
也被 rejected
,并返回跟第一个 promise
一样的错误信息:Error: fruits is empty
,即使在 1秒
后就完成的第二个 promise
的值也不被采纳。
接下来将数组 allPromise
的所有 promise
都抛出异常被 rejected
,通过定时器将 rejected
的顺序做个调整,如下:
const promise = Promise.all([
rejectTimeout(new Error("fruits is empty"), 5000),
rejectTimeout(new Error("vegetables is empty"), 1000),
]);
// 等待...
const list = async () => {
try {
const result = await promise;
console.log(result);
} catch (error) {
console.log(error);
}
};
经过 5秒
之后完成执行,而结果显示为 Error: vegetables is empty
,不难看出 allPromise
被 rejected
的原因是最先 rejected
的promise
。
Promise.all()
的这种行为被称为快速失败,如果promise
数组中至少有一个promise
被rejected
,那么返回的promise
也被拒绝。如果promise
数组中所有的都被rejected
,那么返回的promise
被拒绝的原因是先rejected
的那一个。
小结
Promise.all()
是并行执行异步操作并获取所有 resolve
值的最佳方法,非常适合需要同时获取异步操作结果来进行下一步运算的场合。
Promise.allSettled()
方法 Promise.allSettled(promises)
,返回一个在所有给定的 promise
都已经 fulfilled
或 rejected
后的 promise
,并带有一个对象数组,每个对象表示对应的promise
结果。
下面来看看 Promise.allSettled()
是如何工作的。
1.工作原理
Promise.allSettled()
可用于并行执行独立的异步操作,并收集这些异步操作的结果。
函数接受一个 promise
数组(或通常是一个可迭代的)作为参数,如下:
const statusesPromise = Promise.allSettled(promises);
当所有输入 promises
都被履行或拒绝时,statusesPromise
会解析为一个具有其状态的数组:
{ status: 'fulfilled', value:value }
: 如果相应的promise
已经履行{ status: 'rejected', reason: reason }
:如果相应的promise
被拒绝
可以使用 then
方法提取所有 promises
的状态:
statusesPromise.then((statuses) => {
statuses; // [{ status: '...', value: '...' }, ...]
});
也可以使用 async/await
语法:
const statuses = await statusesPromise;
statuses; // [{ status: '...', value: '...' }, ...]
Promise.allSettled()
返回的承诺总是以一系列状态实现,无论是否有一些(或者全部)输入承诺被拒绝。
Promise.allSettled()
和Promise.all()
的最大不同:Promise.allSettled()
永远不会被rejected
。
2. 使用指南
现在来深入介绍 Promise.allSettled()
的使用之前, 还是先来定义 2 个简单的函数。
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
接下来使用上面定义的2个辅助函数来试试 Promise.allSettled()
。
2.1 完成所有 promises
下面定义了一个 promise
数组 statusesPromise
,所有的 promise
都能够成功的 resolve
值,如下:
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];
const statusesPromise = Promise.allSettled([
resolveTimeout(fruits, 2000),
resolveTimeout(vegetables, 1000),
]);
// 等待 2 秒 ...
const list = async () => {
try {
const statuses = await statusesPromise;
console.log(statuses);
} catch (error) {
console.log(error);
}
};
list(); // [{ status: 'fulfilled', value: [ 'potatoes', 'tomatoes' ] },{ status: 'fulfilled', value: [ 'oranges', 'apples' ] }]
从上面执行的结果来看 Promise.allSettled()
返回的一个 promise
的 resolve
状态数组是按照执行前 statusesPromise
的顺序组成其结果。
2.2 一个 promise
被 rejected
将上面第一个 promise
出现异常被 rejected
,如下代码:
const fruits = ["potatoes", "tomatoes"];
const statusesPromise = Promise.allSettled([
resolveTimeout(fruits, 2000),
rejectTimeout(new Error("Vegetables is empty"), 1000),
]);
// 等待 2 秒 ...
const list = async () => {
try {
const statuses = await statusesPromise;
console.log(statuses);
} catch (error) {
console.log(error);
}
};
list(); // // [{ status: 'fulfilled', value: ['potatoes', 'tomatoes'] },{ status: 'rejected', reason: Error('Vegetables is empty') }]
即使输入数组中的第二个 promise
被 rejected
, statusesPromise
仍然可以成功解析状态数组。
2.3 所有 promises
被 rejected
将上面所有的 promises
出现异常被 rejected
,如下代码:
const statusesPromise = Promise.allSettled([
rejectTimeout(new Error("Fruits is empty"), 2000),
rejectTimeout(new Error("Vegetables is empty"), 1000),
]);
// 等待 2 秒 ...
const list = async () => {
try {
const statuses = await statusesPromise;
console.log(statuses);
} catch (error) {
console.log(error);
}
};
list(); // // [{ status: 'rejected', reason: Error('Fruits is empty') },{ status: 'rejected', reason: Error('Vegetables is empty') }]
小结
当需要执行并行和独立的异步操作并收集所有结果时,Promise.allSettled()
就是不错的选择,即使一些异步操作可能失败。
Promise.race()
方法 Promise.race(promises)
,顾名思义就是赛跑的意思,Promise.race([p1, p2, p3])
里面 promise
数组那个执行完成得快就获取那个的结果,不管结果本身是成功履行状态还是失败拒绝状态,只输出最快的 promise
。
下面来看看 Promise.race()
是如何工作的。
1.工作原理
Promise.race()
返回一个 promise
,一旦迭代器中的某个 promise
履行或拒绝,返回的 promise
就会履行或拒绝。
函数接受一个 promise
数组(或通常是一个可迭代的)作为参数,如下:
const racePromise = Promise.race(promises);
当所有输入 promises
中有一个 promise
快速被履行或拒绝时,racePromise
就会解析快速完成的 promise
结果(履行或拒绝):
可以使用 then
方法提取 racePromise
的结果:
racePromise.then((fastValue) => {
fastValue // 快速完成的 promise
});
也可以使用 async/await
语法:
const fastPromise = await racePromise;
fastPromise; // 快速完成的 promise
Promise.race()
返回的承诺和最先完成的承诺信息一致。
Promise.race()
和Promise.any()
的不同:Promise.race()
承诺列表中寻找第一个履行或拒绝的承诺;Promise.any()
是从承诺列表中查找第一个履行的承诺。
2. 使用指南
现在来深入介绍 Promise.race()
的使用之前,同样先来定义 2 个简单的函数。
function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
接下来使用上面定义的2个辅助函数来试试 Promise.race()
。
2.1 完成所有 promises
下面定义了一个 promise
数组 racePromise
,所有的 promise
都能够成功的 resolve
值,如下:
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];
const racePromise = Promise.race([
resolveTimeout(fruits, 5000),
resolveTimeout(vegetables, 1000),
]);
// 等待 1 秒 ...
const list = async () => {
try {
const fastPromise = await racePromise;
console.log(fastPromise);
} catch (error) {
console.log(error);
}
};
list(); // [ 'oranges', 'apples' ]
从上面执行的结果来看 Promise.race()
返回最快履行的 promise
的 resolve
结果。
2.2 一个 promise
被 rejected
将上面第一个 promise
出现异常被 rejected
,如下代码:
const fruits = ["potatoes", "tomatoes"];
const racePromise = Promise.race([
resolveTimeout(fruits, 2000),
rejectTimeout(new Error("Vegetables is empty"), 1000),
]);
// 等待 1 秒 ...
const list = async () => {
try {
const fastPromise = await racePromise;
console.log(fastPromise);
} catch (error) {
console.log(error);
}
};
list(); // Error: Vegetables is empty
从上面的结果看,最先完成的 promise
被 rejected
,那么 fastPromise
返回的 promise
也是被 rejected
。
下面将rejected
的承诺时间延长到 5秒,如下:
const fruits = ["potatoes", "tomatoes"];
const racePromise = Promise.race([
resolveTimeout(fruits, 2000),
rejectTimeout(new Error("Vegetables is empty"), 5000),
]);
// 等待 2 秒 ...
const list = async () => {
try {
const fastPromise = await racePromise;
console.log(fastPromise);
} catch (error) {
console.log(error);
}
};
list(); // [ 'potatoes', 'tomatoes' ]
从上面执行结果看到,最快完成的 promise
履行了 resolve
,那么 fastPromise
返回的 promise
也是履行了 resolve
。
2.3 所有 promises
被 rejected
将上面所有的 promises
出现异常被 rejected
,如下代码:
const racePromise = Promise.race([
rejectTimeout(new Error("Fruits is empty"), 2000),
rejectTimeout(new Error("Vegetables is empty"), 1000),
]);
// 等待 1 秒 ...
const list = async () => {
try {
const fastPromise = await racePromise;
console.log(fastPromise);
} catch (error) {
console.log(error);
}
};
list(); // Error: Vegetables is empty
从结果来看,虽然两个承诺都被拒绝了,fastPromise
返回的 promise
是最快被拒绝的 。
3. 使用场景
3.1. 性能测试
在有异步操作的项目中,在对于网络或数据库请求进行性能进行优化的时候,可以使用 Promises
来测试其优化效果,通过使用 Promise.race()
来测试两种不同的方法的响应速度。
3.2 最佳选择
例如获取同一类型的数据有多个请求服务器,同时向多个服务器发送请求,只要其中一个完成工作,就将其数据呈现,达到选择最佳线路的效果。这是可以使用 Promise.race()
同时执行 promise
并在第一个成功后立即完成。
小结
Promise.race()
为第一个已解决和已拒绝的 promise
执行回调函数,而 Promise.any()
为第一个已履行的 promise
执行回调函数,如果没有履行的 promise
则拒绝一个特殊属性 AggregateError
。