各位观众老爷们,早上好/下午好/晚上好!今天咱们来聊聊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("结束");
运行这段代码,你会发现控制台输出的顺序是:
- 开始
- 结束
Promise1
resolved: 1Promise2
resolved: 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
,以提高代码的效率。
好了,今天的分享就到这里,希望大家有所收获!如果还有什么疑问,欢迎在评论区留言,我会尽力解答。下次再见!