各位观众老爷们,早上好/下午好/晚上好!今天咱们来聊聊JavaScript里一个挺有意思的东西——Promise的then方法,以及它背后的微任务队列,还有链式调用这些事儿。别担心,我会尽量说得轻松点儿,争取让大家听完之后,感觉就像刚吃完一顿火锅,浑身舒坦!
开场白:Promise这玩意儿到底是个啥?
在正式开始之前,咱们先简单回顾一下Promise。你可以把它想象成一张欠条,你让别人帮你办件事儿,办成了给你个糖,没办成给你个巴掌。这个“糖”就是resolve,表示成功;“巴掌”就是reject,表示失败。而Promise本身,就是这张欠条。
then方法:承上启下,连接美好未来
好了,有了Promise,我们怎么知道事情办成了没?这时候就轮到咱们今天的主角——then方法登场了。then方法就像一个中间人,它会告诉你,欠条上的事情办成了还是没办成,然后根据结果来决定下一步该怎么走。
then方法接收两个参数(都是可选的):
onFulfilled: 当Promise状态变为fulfilled(成功)时调用的回调函数。onRejected: 当Promise状态变为rejected(失败)时调用的回调函数。
简单来说,then就像一个条件判断,如果Promise成功了,就执行onFulfilled,否则执行onRejected。
代码示例:then的基本用法
const promise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
resolve("事情办成了!randomNumber: " + randomNumber); // 成功
} else {
reject("事情没办成!randomNumber: " + randomNumber); // 失败
}
}, 1000);
});
promise.then(
(result) => {
console.log("成功了:", result);
},
(error) => {
console.error("失败了:", error);
}
);
console.log("`Promise`创建之后,`then`之前");
运行这段代码,你会发现控制台会先输出“Promise创建之后,then之前”,然后过一秒钟左右,才会输出成功或失败的结果。这是因为setTimeout是异步操作,而then方法的回调函数会被放入微任务队列中,等待主线程空闲时执行。
微任务队列:then背后的秘密
现在,我们来聊聊微任务队列。这玩意儿是JavaScript事件循环机制中非常重要的一部分。简单来说,它是一个存放微任务的队列,微任务会在当前宏任务执行完毕后,但在浏览器渲染之前执行。
常见的微任务包括:
Promise的then、catch、finally回调MutationObserver的回调queueMicrotask()
微任务 vs 宏任务
为了更好地理解微任务,我们把它和宏任务对比一下:
| 特性 | 宏任务(Macro Task) | 微任务(Micro Task) |
|---|---|---|
| 例子 | setTimeout, setInterval, setImmediate, I/O, UI rendering |
Promise.then, MutationObserver, queueMicrotask |
| 执行时机 | 每次事件循环的末尾 | 当前宏任务执行完毕后 |
| 优先级 | 低 | 高 |
| 数量 | 每次事件循环一个 | 可以有多个 |
then与微任务队列
当Promise的状态发生改变(从pending变为fulfilled或rejected)时,then方法对应的回调函数会被放入微任务队列中。这意味着,即使Promise已经完成了,then的回调函数也不会立即执行,而是要等到当前宏任务执行完毕,且主线程空闲时才会执行。
代码示例:微任务的执行顺序
console.log("开始");
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
promise1.then((value) => {
console.log("`Promise1` resolved:", value);
});
promise2.then((value) => {
console.log("`Promise2` resolved:", value);
});
setTimeout(() => {
console.log("`setTimeout`");
}, 0);
console.log("结束");
运行这段代码,你会发现控制台输出的顺序是:
- 开始
- 结束
Promise1resolved: 1Promise2resolved: 2setTimeout
这是因为Promise.resolve().then() 和 setTimeout 都是异步任务,但是 Promise.then 是微任务,而 setTimeout 是宏任务。在当前宏任务(也就是主线程的代码)执行完毕后,会先执行微任务队列中的所有任务,然后再执行下一个宏任务。
链式调用:then的进阶用法
then方法最强大的地方在于它可以链式调用。这意味着你可以连续调用多个then方法,形成一个Promise链。每一个then方法都会返回一个新的Promise,这个新的Promise的状态取决于前一个then方法的回调函数的返回值。
代码示例:链式调用
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
promise
.then((value) => {
console.log("第一个`then`:", value);
return value + 1; // 返回一个值,这个值会作为下一个`then`的参数
})
.then((value) => {
console.log("第二个`then`:", value);
return new Promise((resolve) => {
setTimeout(() => {
resolve(value + 1); // 返回一个`Promise`
}, 500);
});
})
.then((value) => {
console.log("第三个`then`:", value);
return value + 1;
})
.then((value) => {
console.log("第四个`then`:", value);
});
运行这段代码,你会发现控制台会依次输出:
- 第一个
then: 1 - 第二个
then: 2 - 第三个
then: 3 - 第四个
then: 4
链式调用的规则
链式调用有一些需要注意的规则:
then方法总是返回一个新的Promise。- 如果
then方法的回调函数返回一个值,那么新的Promise会立即变为fulfilled状态,并将这个值作为fulfilled状态的值传递给下一个then方法。 - 如果
then方法的回调函数返回一个Promise,那么新的Promise的状态会和这个返回的Promise的状态保持一致。也就是说,新的Promise会等待返回的Promise的状态变为fulfilled或rejected,然后才会继续执行下一个then方法。 - 如果
then方法的回调函数抛出一个错误,那么新的Promise会立即变为rejected状态,并将这个错误作为rejected状态的值传递给下一个catch方法(如果没有catch方法,则会抛出未捕获的错误)。
catch方法:错误处理的利器
catch方法用于捕获Promise链中发生的错误。它可以放在Promise链的末尾,或者放在任何一个then方法之后。
代码示例:catch的用法
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
resolve(1);
} else {
reject("出错了!randomNumber: " + randomNumber);
}
}, 1000);
});
promise
.then((value) => {
console.log("第一个`then`:", value);
return value + 1;
})
.then((value) => {
console.log("第二个`then`:", value);
// 模拟一个错误
throw new Error("故意抛出的错误");
return value + 1;
})
.then((value) => {
console.log("第三个`then`:", value); // 这行代码不会执行
})
.catch((error) => {
console.error("捕获到错误:", error);
});
运行这段代码,你会发现控制台会输出:
- 第一个
then: 1 - 第二个
then: 2 - 捕获到错误: Error: 故意抛出的错误
finally方法:无论成功失败,都要执行
finally方法用于指定无论Promise成功还是失败都要执行的回调函数。finally方法不接收任何参数,并且不能访问Promise的结果或错误。
代码示例:finally的用法
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
resolve(1);
} else {
reject("出错了!randomNumber: " + randomNumber);
}
}, 1000);
});
promise
.then((value) => {
console.log("成功了:", value);
})
.catch((error) => {
console.error("失败了:", error);
})
.finally(() => {
console.log("无论成功还是失败,我都会执行");
});
运行这段代码,你会发现控制台会输出成功或失败的结果,并且最后都会输出“无论成功还是失败,我都会执行”。
Promise.all:并发执行,等待所有完成
Promise.all方法接收一个Promise数组作为参数,并返回一个新的Promise。只有当数组中所有的Promise都变为fulfilled状态时,新的Promise才会变为fulfilled状态,并将所有Promise的结果放入一个数组中作为fulfilled状态的值。如果数组中有一个Promise变为rejected状态,那么新的Promise会立即变为rejected状态,并将这个Promise的错误作为rejected状态的值。
代码示例:Promise.all的用法
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, 500);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("出错了!");
}, 1000);
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log("所有`Promise`都成功了:", values);
})
.catch((error) => {
console.error("有一个`Promise`失败了:", error);
});
运行这段代码,你会发现控制台会输出“有一个Promise失败了: 出错了!”。因为promise3最终变为rejected状态。
Promise.race:赛跑机制,谁先完成算谁的
Promise.race方法也接收一个Promise数组作为参数,并返回一个新的Promise。新的Promise的状态会和数组中第一个变为fulfilled或rejected状态的Promise保持一致。
代码示例:Promise.race的用法
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 500);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("出错了!");
}, 1000);
});
Promise.race([promise1, promise2])
.then((value) => {
console.log("第一个完成的`Promise`成功了:", value);
})
.catch((error) => {
console.error("第一个完成的`Promise`失败了:", error);
});
运行这段代码,你会发现控制台会输出“第一个完成的Promise成功了: 1”。因为promise1比promise2先完成。
总结:Promise的精髓
Promise的then方法是其核心,它通过微任务队列实现了异步操作的调度,并通过链式调用实现了复杂的异步流程控制。掌握then方法以及微任务队列的机制,对于理解和使用Promise至关重要。
一些小技巧
- 尽量避免在
then方法的回调函数中进行耗时的操作,以免阻塞微任务队列的执行。 - 合理使用
catch方法进行错误处理,避免出现未捕获的错误。 - 根据实际情况选择
Promise.all或Promise.race,以提高代码的效率。
好了,今天的分享就到这里,希望大家有所收获!如果还有什么疑问,欢迎在评论区留言,我会尽力解答。下次再见!