深入理解 JavaScript 异步编程:从回调地狱到 Promise 的艺术

103 阅读8分钟

前言:当代码背叛了你的期望

想象一下:你精心设计的相亲-结婚-生娃流程代码,在运行时却变成了"闪电结婚→漫长相亲→意外生娃"的荒诞剧。这不是科幻小说情节,而是每个JavaScript开发者都曾遭遇的异步陷阱

在单线程的JavaScript世界里,时间不再是线性流动的河流,而是一片充满漩涡的海洋。那些看似顺序排列的代码,实际执行时却可能跳起令人困惑的舞蹈。当setTimeoutfetchfs.readFile这些异步操作出现时,你的代码逻辑可能瞬间瓦解,就像精心排练的交响乐突然变成即兴爵士。

// 你以为的顺序
去相亲();
结婚();
生娃();

// 实际执行结果
结婚(); // 立即执行
去相亲(); // 1秒后执行
生娃(); // 立即执行

这种时空错乱让无数开发者抓狂,催生了臭名昭著的"回调地狱"——代码如俄罗斯套娃般层层嵌套,阅读它就像在迷宫中寻找出口。但别担心,救世主已经降临:Promise

本文将带你穿越JavaScript异步编程的迷雾森林,从回调深渊到Promise高地,最终抵达async/await的应许之地。准备好解开时间线的秘密了吗?让我们开始这段重构代码命运的旅程!

异步

  • 进程
  • 线程
  1. 在不同的场景中进程都可以用来描述该场景中一个效果,线程是进程里面的一个更小的单位,通常多个线程配合工作构成一个进程

  2. v8运行一份js代码会创建一个进程,从上往下执行代码,遇到同步代码会直接执行,遇到异步代码就会挂起,先去执行后面大的同步代码,等到后面的同步代码执行完毕,再执行异步代码

  3. js默认是单线程的语言,因为js设定是为了做浏览器的脚本语言,尽量少的开销用户设备的性能 (但是我们有手段能让js增加线程)

处理异步

  • 因为js的执行规则,导致我们在开发过程中时而会出现代码的异步情况
  1. 回调函数:当嵌套过深时,代码的可读性差,维护困难,排查问题苦难(回调地狱)

来看下面这段代码:

let a = 1;//同步代码 不耗时v8

setTimeout(() => {
  a = 2;
  console.log(a, "setTimeout");
}, 1000);//异步代码



console.log(a);

执行结果是:

image.png

我们把这种不耗时的代码称为同步代码,耗时的称为异步代码

那么什么代码算是耗时的呢?

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();

来看这段代码,假设我们结婚之前要先相亲,才对,但是这里顺序反了,先结了婚才去相亲,明显没有达到我们想要的目的

%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B6%202025-06-30%20192730_converted.gif

如何实现正确的顺序呢

第一种解决方法: 回调函数

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()之后捕获错误

这个是时候我们增加一个生娃事件,如果这样写的话执行结果是什么呢?

会是先相亲结婚再生娃吗?

%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B6%202025-06-30%20195426_converted.gif

我们只是让相亲有返回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();
  }); //链式

那么现在可以了对吧?

%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B6%202025-06-30%20195426_converted.gif

不可以

这是为什么呢?

这里的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()的执行状态。

第三种方法 async await es新加特性

结语:掌握时间线的艺术

当我们结束这段异步编程之旅,站在Promise构建的高地上回望,那些曾经困扰我们的回调深渊已变得清晰可见。Promise不仅是技术方案,更是思考异步的新范式——它教会我们用"状态"而非"时间"来管理操作,用"链"而非"巢"来组织逻辑。

在本文中,我们揭开了JavaScript单线程模型的面纱,理解了事件循环的舞蹈节奏;我们驯服了setTimeout这头时间野兽,从回调地狱突围;最终,我们掌握了Promise这把打开异步编程新纪元的钥匙。

但旅程并未结束:

  • 当你面对复杂异步流时,记住Promise.all的并行魔法
  • 当错误潜伏在角落时,善用catch的守护之力
  • 当逻辑层层嵌套时,让then的链条优雅延伸

真正的异步大师,不是对抗单线程的限制,而是在约束中编织并发的艺术。  正如交响乐指挥在时间分割中创造和谐,JavaScript开发者用Promise在单线程中谱写并发乐章。

现在,带着这些新武器回到你的代码战场吧!下次当相亲函数又想抢先执行结婚仪式时,你会微笑着用Promise.resolve()告诉它:"别急,我们按剧本走"。