wyxh2004
【回归基础(一)】JS 关于Promise属性和方法

【回归基础(一)】JS 关于Promise属性和方法

最近在看一些前端的小视频,感觉自己对js的理解从精通到略懂(doge),故借本文重新回归基础

【回归基础(一)】JS 关于Promise属性和方法

我们都知道 JS 是单线程,但是一些操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种的执行模式:同步和异步模式。

JS 中用来存储待执行回调函数的队列包含 2 个不同特定的队列

  • 宏队列: 用来保存待执行的宏任务(回调), 比如:定时器回调 / DOM 事件回调 / ajax 回调
  • 微队列: 用来保存待执行的微任务(回调), 比如:promise的回调 / MutationObserver的回调
  • JS执行时会区别这两个队列
    • JS 引擎首先必须先执行所有的初始化同步任务代码
    • 每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行
1.png

每当执行宏任务之前就得看看是否有微任务,有就先执行当前队列中的所有微任务

(虽然但是)随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法(但是网络上的教程仍然使用“宏队列”一词)。宏任务队列包含一些较大的、离散的任务,例如 I/O 操作、用户交互事件(例如点击、键盘输入)、定时器事件。用于处理离散任务,个人认为可以理解为除了微队列的其它队列。

​ 在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」

了解了这些我们就可以来学习Promise了

添加任务到微队列的主要方式主要是使用 Promise

添加任务到微队列的主要方式主要是使用 Promise、MutationObserver。例如:

1
2
// 立即把一个函数添加到微队列
Promise.resolve().then(函数)

Promise 是一个处理异步操作的对象,它代表了一个异步操作的最终完成或者失败的值,Promise 提供了一种在单个异步操作成功或失败时进行回调的机制,并且可以链接多个异步操作形成复杂的流程控制。

本质上 Promise 是一个函数返回的 对象 ,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。例如,我们可以把

1
createAsync(audioSettings, successCallback, failureCallback);

优化成

1
createAsync(audioSettings).then(successCallback, failureCallback);

resolvereject

  • resolve 是 Promise 构造函数内部传入的一个函数引用,当异步操作成功完成时调用它。调用 resolve(value) 会使得 Promise 变为 fulfilled 状态,并将结果 value 传递给后续通过 .then() 注册的成功回调。
1
2
3
4
new Promise((resolve, reject) => {
// 异步操作完成后
resolve(someValue);
});
  • reject 同样是 Promise 内部的函数引用,当异步操作失败时调用它。调用 reject(reason) 会使得 Promise 变为 rejected 状态,并将错误信息 reason 传递给后续通过 .catch().then() 的第二个参数注册的错误回调。
1
2
3
4
new Promise((resolve, reject) => {
// 异步操作发生错误时
reject(new Error('Some error occurred'));
});

Promise的三种状态

  • fulfilled(已履行/已解决):Promise 在其异步操作成功完成后所处的状态,此时可以通过 .then() 方法来获取到成功的结果。
  • rejected(已拒绝):Promise 在其异步操作失败后所处的状态,此时可以通过 .catch().then() 的第二个参数来捕获和处理错误信息。
  • pending(进行中):这是Promise对象的初始状态,当一个Promise被创建但其异步操作尚未完成时,它处于pending状态。在pending状态下,Promise既没有fulfilled也没有rejected。

一、链式调用

连续执行两个或者多个异步操作是一个常见的需求,在旧的回调风格中,这种操作会导致经典的回调地狱:

1
2
3
4
5
6
7
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`得到最终结果:${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);

我们就可以通过一个 Promise 链来解决这个问题。在Promise API中,回调函数是附加到返回的 Promise 对象上的,而不是传入一个函数中!

1
2
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

在上述例子中,then() 函数会返回一个和原来不同的新的 Promise

promise2 不仅表示 doSomething() 函数的完成,也代表了你传入的 successCallback 或者 failureCallback 的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,这样的话,在 promise2 上新增的回调函数会排在这个 Promise 对象的后面。


每一个 Promise 都代表了链中另一个异步过程的完成。此外,then 的参数是可选的,catch(failureCallback) 等同于 then(null, failureCallback)——所以如果你的错误处理代码对所有步骤都是一样的,你可以把它附加到链的末尾

1
2
3
4
5
6
7
8
9
10
11
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`得到最终结果:${finalResult}`);
})
.catch(failureCallback);

不过建议在使用箭头函数的时候写上大括号和和return(其实也看情况和个人喜好),以便支持使用多个语句,

如果上一个处理程序启动了一个 Promise 但并没有返回它,那就没有办法再追踪它的状态了,这个 Promise 就是“漂浮”的

1
2
3
4
5
6
7
8
9
doSomething()
.then((url) => {
// 忘记返回了!
fetch(url);
})
.then((result) => {
// 结果是 undefined,因为上一个处理程序没有返回任何东西。
// 无法得知 fetch() 的返回值,不知道它是否成功。
});

一个经验法则是,每当你的操作遇到一个 Promise,就返回它,并把它的处理推迟到下一个 then 处理程序中

就像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const listOfIngredients = [];

doSomething()
.then((url) =>
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
}),
)
.then(() => {
console.log(listOfIngredients);
});

// 或

doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});

二、嵌套

嵌套 Promise 是一种可以限制 catch 语句的作用域的控制结构写法。明确来说,嵌套的 catch 只会捕获其作用域及以下的错误,而不会捕获链中更高层的错误。如果使用正确,可以实现高精度的错误恢复。

1
2
3
4
5
6
7
8
9
doSomethingCritical()
.then((result) => doSomethingOptional() // 可选操作,这里的可选操作是嵌套的
.then((optionalResult) => doSomethingExtraNice(optionalResult))
.catch((e) => {
console.log(e.message);
}),
) // 即便可选操作失败了,也会继续执行
.then(() => moreCriticalStuff())
.catch((e) => console.log(`严重失败:${e.message}`));

这个内部的 catch 语句仅能捕获到 doSomethingOptional()doSomethingExtraNice() 的失败,并将该错误与外界屏蔽,之后就恢复到 moreCriticalStuff() 继续执行。值得注意的是,如果 doSomethingCritical() 失败,这个错误仅会被最后的(外部)catch 语句捕获到,并不会被内部 catch 吞掉。即,使用一个 catch,这对于在链式操作中抛出一个失败之后,再次进行新的操作会很有用。如果任何一个函数调用失败,就会执行catch中的失败回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Result from Promise 1');
}, 1000);
});

promise1.then(result1 => {
console.log(result1); // 输出: "Result from Promise 1"

var promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Result from Promise 2');
}, 500);
});
// 返回第二个Promise实例给下一个.then()
return promise2;
}).then(result2 => {
console.log(result2); // 输出: "Result from Promise 2"
});

在这个例子中,promise1内部的.then()方法返回了promise2,导致了Promise的嵌套。当promise1成功解决后,紧接着执行promise2,并在其解决后打印结果。

为了避免过多的嵌套,可以利用Promise链式调用(Chaining)来简化代码,使其更易读和维护。通过将每个异步操作的结果传递给下一个.then(),可以消除嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Result from Promise 1');
}, 1000);
});

promise1.then(result1 => {
console.log(result1);
// 直接返回新的Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Result from Promise 2');
}, 500);
});
}).then(result2 => {
console.log(result2);
});

// 或者使用async/await进一步简化:
async function process() {
const result1 = await promise1;
console.log(result1);

const promise2 = new Promise(/* ... */);
const result2 = await promise2;
console.log(result2);
}

process();

一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,就立即返回它,最终的链应是扁平化的:

1
2
3
4
5
6
7
8
9
10
11
12
doSomething()
.then(function (result) {
// 如果使用完整的函数表达式:返回 Promise
return doSomethingElse(result);
})
// 如果使用箭头函数:省略大括号并隐式返回结果
.then((newResult) => doThirdThing(newResult))
// 即便上一个 Promise 返回了一个结果,后一个 Promise 也不一定非要使用它。
// 你可以传入一个不使用前一个结果的处理程序。
.then((/* 忽略上一个结果 */) => doFourthThing())
// 总是使用 catch 终止 Promise 链,以保证任何未处理的拒绝事件都能被捕获!
.catch((error) => console.error(error));

上述代码的写法就是具有适当错误处理的简单明确的链式写法。使用async/await 时可以解决以上大多数错误

三、组合

有四个组合工具可用来并发异步操作Promise.all()Promise.allSettled()Promise.any()Promise.race()

他们是Promise对象提供的方法,它们用于处理多个Promise实例的集合。以下是每个方法的用法和作用

1. Promise.all(iterable)

  • 用法:接收一个可迭代对象(通常是一个Promise实例数组)作为参数。
  • 作用:只有当所有传入的Promise都变为fulfilled状态时,Promise.all()返回的Promise才会变为fulfilled,并且其结果是一个包含所有Promise resolve值的数组,顺序与原数组保持一致。只要其中有一个Promise变为rejected状态,那么Promise.all()返回的Promise就会立即变为rejected,并返回第一个被reject的Promise的结果。

Javascript

1
2
3
4
5
6
7
let promise1 = Promise.resolve(3);
let promise2 = new Promise((resolve, reject) => setTimeout(resolve, 1000, 'Hello'));
let promise3 = fetch('https://api.example.com/data');

Promise.all([promise1, promise2, promise3])
.then(results => console.log(results))
.catch(error => console.error(error));

2. Promise.allSettled(iterable)

  • 用法:同样接受一个Promise实例组成的可迭代对象作为参数。
  • 作用:不论传入的Promise是fulfilled还是rejected,Promise.allSettled()都会等待所有Promise完成(settled),然后返回一个新的Promise,该Promise在所有子Promise都settled后resolve,结果是一个数组,数组中的元素包含了每个Promise的状态(fulfilled或rejected)以及对应的值或拒绝原因。

Javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise1 = Promise.resolve(1);
let promise2 = Promise.reject(new Error('Failed'));
let promise3 = fetch('https://api.example.com/data');

Promise.allSettled([promise1, promise2, promise3])
.then(results => {
results.forEach(promiseResult => {
if (promiseResult.status === 'fulfilled') {
console.log('Resolved with value:', promiseResult.value);
} else if (promiseResult.status === 'rejected') {
console.error('Rejected with reason:', promiseResult.reason);
}
});
});

3. Promise.any(iterable)

  • 用法:从ES2021开始引入,它接受一组Promise实例作为参数。
  • 作用:只要输入的Promise中有任意一个变为fulfilled状态,Promise.any()返回的Promise就会变为fulfilled,并返回那个率先fulfilled的Promise的resolve值。如果所有Promise都变为rejected状态,那么返回的Promise也会变为rejected,其reason是一个AggregateError对象,包含了所有Promise的拒绝原因。

Javascript

1
2
3
4
5
6
let promise1 = new Promise((resolve, reject) => setTimeout(reject, 500, 'Promise 1 failed'));
let promise2 = new Promise((resolve, reject) => setTimeout(resolve, 1000, 'Success!'));

Promise.any([promise1, promise2])
.then(result => console.log('First resolved value:', result))
.catch(reason => console.error('All promises rejected:', reason.errors));

4. Promise.race(iterable)

  • 用法:接收一组Promise实例作为参数。
  • 作用:一旦输入的Promise中有一个变为fulfilled或者rejected状态,Promise.race()返回的Promise就立即以相同的fulfilled或rejected状态结束,并返回第一个改变状态的Promise的结果或错误。

Javascript

1
2
3
4
5
6
let promise1 = Promise.resolve(1);
let promise2 = new Promise((resolve, reject) => setTimeout(reject, 500, 'Promise 2 failed'));

Promise.race([promise1, promise2])
.then(value => console.log('First settled value:', value))
.catch(reason => console.error('First rejected reason:', reason));

四、时序问题

在基于回调的 API 中,回调函数何时以及如何被调用取决于 API 的实现者。例如,回调可能是同步调用的,也可能是异步调用的:

1
2
3
4
5
6
7
function doSomething(callback) {
if (Math.random() > 0.5) {
callback();
} else {
setTimeout(() => callback(), 1000);
}
}

不建议使用上述这种设计,因为它会导致所谓的“Zalgo 状态”。另一方面,Promise 是一种控制反转的形式——API 的实现者不控制回调何时被调用。相反,维护回调队列并决定何时调用回调的工作被委托给了 Promise 的实现者(一般情况浏览器引擎),这样一来,API 的使用者和开发者都会自动获得强大的语义保证,包括:

  • 被添加到 then() 的回调永远不会在 JavaScript 事件循环的当前运行完成之前被调用。
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。

友情提示:传入 then() 的函数永远不会被同步调用,即使 Promise 已经被解决了(resolved),详情请自行了解浏览器事件循环的原理,这里放一张示意图

根据 W3C 的最新解释:

image-20220809223027806

Promise 回调被处理为微任务,加入微队列,而 setTimeout() 回调被处理为任务队列,加入其他线程。

五、使用async/await异步函数

异步函数(async function)是 ECMAScript 2017 (ECMA-262) 标准的规范,几乎被所有浏览器所支持。

来看下面这个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function print(delay, message) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}
// 在不同时间段输出三行文字
print(1000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});

我们可以将这段代码变得更好看:

1
2
3
4
5
6
async function asyncFunc() {
await print(1000, "First");
await print(4000, "Second");
await print(3000, "Third");
}
asyncFunc();

异步函数 async function 中可以使用 await 指令,await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于阅读。

处理异常的机制将用 try-catch 块实现:

1
2
3
4
5
6
7
8
9
10
11
async function asyncFunc() {
try {
await new Promise(function (resolve, reject) {
throw "Some error"; // 或者 reject("Some error")
});
} catch (err) {
console.log(err);
// 会输出 Some error
}
}
asyncFunc();

如果 Promise 有一个正常的返回值,await 语句也会返回它:

1
2
3
4
5
6
7
8
9
10
async function asyncFunc() {
let value = await new Promise(
function (resolve, reject) {
resolve("Return value");
}
);
console.log(value);
}
asyncFunc();
// 会输出 Return value

六、面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})

Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
// 输出为 0、1、2、3、4、5、6
// 详解参见https://www.douyin.com/search/promise?aid=9dfeb0dc-a165-4ed3-8c68-d1c63c192b91&enter_from=personal_homepage&modal_id=7267905977767202100&source=normal_search
参考文献:

稀土掘金 【宏队列与微队列的Promise】作者:奶香南瓜饼

[菜鸟教程 【JavaScript Promise】](JavaScript Promise | 菜鸟教程 (runoob.com))

W3C 官方

Promises/A+ 规范

渡一教育

Author:wyxh2004
Link:https://wyxh2004.github.io/blog/2024/01/15/【回归基础(一】JS 关于Promise属性和方法/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可