前言:当代码背叛了你的期望
想象一下:你精心设计的相亲-结婚-生娃流程代码,在运行时却变成了"闪电结婚→漫长相亲→意外生娃"的荒诞剧。这不是科幻小说情节,而是每个JavaScript开发者都曾遭遇的异步陷阱
在单线程的JavaScript世界里,时间不再是线性流动的河流,而是一片充满漩涡的海洋。那些看似顺序排列的代码,实际执行时却可能跳起令人困惑的舞蹈。当
setTimeout、fetch、fs.readFile这些异步操作出现时,你的代码逻辑可能瞬间瓦解,就像精心排练的交响乐突然变成即兴爵士。
// 你以为的顺序
去相亲();
结婚();
生娃();
// 实际执行结果
结婚(); // 立即执行
去相亲(); // 1秒后执行
生娃(); // 立即执行
这种时空错乱让无数开发者抓狂,催生了臭名昭著的"回调地狱"——代码如俄罗斯套娃般层层嵌套,阅读它就像在迷宫中寻找出口。但别担心,救世主已经降临:Promise。
本文将带你穿越JavaScript异步编程的迷雾森林,从回调深渊到Promise高地,最终抵达async/await的应许之地。准备好解开时间线的秘密了吗?让我们开始这段重构代码命运的旅程!
异步
- 进程
- 线程
-
在不同的场景中进程都可以用来描述该场景中一个效果,线程是进程里面的一个更小的单位,通常多个线程配合工作构成一个进程
-
v8运行一份js代码会创建一个进程,从上往下执行代码,遇到同步代码会直接执行,遇到异步代码就会挂起,先去执行后面大的同步代码,等到后面的同步代码执行完毕,再执行异步代码
-
js默认是单线程的语言,因为js设定是为了做浏览器的脚本语言,尽量少的开销用户设备的性能 (但是我们有手段能让js增加线程)
处理异步
- 因为js的执行规则,导致我们在开发过程中时而会出现代码的异步情况
- 回调函数:当嵌套过深时,代码的可读性差,维护困难,排查问题苦难(回调地狱)
来看下面这段代码:
let a = 1;//同步代码 不耗时v8
setTimeout(() => {
a = 2;
console.log(a, "setTimeout");
}, 1000);//异步代码
console.log(a);
执行结果是:
我们把这种不耗时的代码称为同步代码,耗时的称为异步代码
那么什么代码算是耗时的呢?
for (let i = 0; i < 1000000; i++) {
console.log(a, "for");
}
这段代码执行是耗时的,但是这是由于电脑性能的原因导致的,只要电脑性能足够强,执行就是几乎不好事的,在v8眼里会认为这段代码是不耗时的。
function xq() {
setTimeout(function () {
console.log('相亲');
}, 1000);
}
function marry() {
console.log('结婚了');
}
xq();
marry();
来看这段代码,假设我们结婚之前要先相亲,才对,但是这里顺序反了,先结了婚才去相亲,明显没有达到我们想要的目的
如何实现正确的顺序呢
第一种解决方法: 回调函数
function xq(){
setTimeout(function(){
console.log('相亲');
marry();
}, 1000);
}
function marry(){
console.log('结婚了');
}
qx();
通过回调函数的形式,在一个函数里面调用另一个函数
function a(){
console.log(1111);
b()
}
function b(){
console.log(2222);
}
function c(){
console.log(33333);
}
但是这样会有问题,如果我们的代码是这样100个函数接连回调呢,当一个代码有问题时,后面的代码就会无法执行。
这样我们排查问题的时候就会非常复杂
第二种解决方法:
return new Promise()=>{}和then()方法
返回的Promise一共有三种状态,pending(等待状态),成功状态,失败状态
then()方法也会返回一个Promsie()(默认执行状态是继承前一个Promise的状态)
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("相亲");
resolve(); //返回成功 耗时结束
// reject(); //失败 耗时结束
}, 1000);
});
// resolve(); //成功 瞬时结束不能放外面
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("结婚了");
resolve();
}, 1000);
});
}
xq().then(() => {//then在成功(resolve())之后运行marry()
marry()
});
//.catch(() => {});在reject()之后捕获错误
我们返回一个Promise函数,Promise里面会有两个方法resolve()和reject(),只有等resolve()执行完毕之后,外面的.then()方法后面的代码才执行,如果执行reject()方法的话,就会抛出一个错误,这个时候我们需要用catch来捕获错误。 return 会立即返回一个Promise函数,但是这个执行结果
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("相亲");
resolve(); //返回成功 耗时结束
// reject(); //失败 耗时结束
}, 1000);
});
// resolve(); //成功 瞬时结束不能放外面
}
function marry() {
setTimeout(() => {
console.log("结婚了");
resolve();
}, 2000);
}
function baby() {
setTimeout(() => {
console.log("生娃了");
}, 1000);
}
xq().then(() => {//then在成功(resolve())之后运行marry()
marry()
baby();
});
//.catch(() => {});在reject()之后捕获错误
这个是时候我们增加一个生娃事件,如果这样写的话执行结果是什么呢?
会是先相亲结婚再生娃吗?
我们只是让相亲有返回promise的能力,但是我们没有设置结婚和生娃之间的promise能力,我们只要在结婚添加 promise就可以了
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("相亲");
resolve(); //返回成功 耗时结束
// reject(); //失败 耗时结束
}, 1000);
});
// resolve(); //成功 瞬时结束不能放外面
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("结婚了");
resolve();
}, 1000);
});
}
function baby() {
setTimeout(() => {
console.log("生娃了");
}, 1000);
}
xq().then(() => {//then在成功(resolve())之后运行marry()
marry().then(()=>{
baby();
})
});
//.catch(() => {});在reject()之后捕获错误
但是这样写结构还是不好看,官方提供了新的方式,通过链式的方式来执行then()
xq() //里面执行到了resolve()
.then(() => {
marry();
})
.then(() => {
baby();
}); //链式
那么现在可以了对吧?
不可以
这是为什么呢?
这里的then()的源码里面也返回了一个Promise对象,所以第二个then()方法才能接在第一个then()后面。
总的过程是这样的:
1.执行xq函数,立即返回一个promise实例对象,但是此时该对象的状态是pending(等待)
2..then立即触发,但是then里面的回调函数没有触发
3.等待相亲函数里面的resolve里面的执行完毕,此时实例对象的状态变更为fulfilled(成功).then里面的回调函数才会触发
4.第二个.then()会在xq()返回Promise()时触发,这是因为,在xq()返回Promsie时,第一个then()会立即被触发,第一个then()会立即返回一个Promise(默认状态跟前一个返回Promise状态相同),于是第二个then也会被触发,生娃函数的执行时间短于结婚的执行时间,所以才会出现生娃在结婚前面。
ok,明白了为什么,那该怎么解决呢?
我们只需要保证第一个then()方法返回的Promsie的状态不要继承于第一个Promsie返回的状态
xq() //里面执行到了resolve()
.then(() => {
return marry();
})
.then(() => {
baby();
});
我们只需要返回一个状态就可以,直接返回我们结婚的Promise的状态给第二个then,而不是让它默认返回继承到第一个Promise()的执行状态。
结语:掌握时间线的艺术
当我们结束这段异步编程之旅,站在Promise构建的高地上回望,那些曾经困扰我们的回调深渊已变得清晰可见。Promise不仅是技术方案,更是思考异步的新范式——它教会我们用"状态"而非"时间"来管理操作,用"链"而非"巢"来组织逻辑。
在本文中,我们揭开了JavaScript单线程模型的面纱,理解了事件循环的舞蹈节奏;我们驯服了
setTimeout这头时间野兽,从回调地狱突围;最终,我们掌握了Promise这把打开异步编程新纪元的钥匙。但旅程并未结束:
- 当你面对复杂异步流时,记住Promise.all的并行魔法
- 当错误潜伏在角落时,善用catch的守护之力
- 当逻辑层层嵌套时,让then的链条优雅延伸
真正的异步大师,不是对抗单线程的限制,而是在约束中编织并发的艺术。 正如交响乐指挥在时间分割中创造和谐,JavaScript开发者用Promise在单线程中谱写并发乐章。
现在,带着这些新武器回到你的代码战场吧!下次当相亲函数又想抢先执行结婚仪式时,你会微笑着用Promise.resolve()告诉它:"别急,我们按剧本走"。