从回调地狱到 Promise:实现优雅的异步处理

133 阅读7分钟

前言

我们时常在JS中遇到要接受数据或者遇到要时间来执行的代码后,再进行一系列操作的情况,接受数据和要时间执行的代码通常都需要一些时间等待,而有些代码后续的执行起来又不需要,今天我们就来细细聊聊遇到这种情况我们该怎么办。

认识异步

let a = 1
console.log(a, 2);

setTimeout(() => {
  a = 2
  console.log(a, 6);
}, 1000);

console.log(a, 9);


//输出 :1  2
    //  1  9
    //  2  6

我们可以看到先输出了第二行,再输出第九行,再输出第六行。欸?代码的执行不是按照从上往下依次执行的吗,为什么我先执行了第九行的输出呢?

恭喜你!你发现了异步:

JS遇到需要耗时执行的代码就将其先挂起,等到后续不耗时的代码执行完毕后再执行耗时的代码

有的小伙伴肯定就要说了,为什么JS要这么设计,为啥就不能按照顺序先把要时间的代码先执行完,再执行后续的代码应该更符合大家的习惯?

我们要知道的是

js是单线程语言,一次只能干一件事情,如果只能硬着头皮按照顺序去执行代码,你想象这种画面:前面的代码巨巨巨巨耗时就比如说for循环个几万次,但是后面的代码和前面完全无关,就因为你一个for循环让大家都跟着你排队是不是不太好啊,官方一定会被戳脊梁骨的。因此它被设计成先执行不耗时的代码,再执行耗时的代码的机制,这样的效率就是最高的。

解决异步

官方既然这么设计,一身反骨的我就是要让耗时的代码先执行,有没有什么办法嘞?

答案当然是有的,回调函数promise就是解决异步问题的专家,现在我们来认识一下。

回调函数

function a(cb) {//callback回调函数
  setTimeout(() => {
    console.log('a执行完毕');
    cb()
  }, 1000)
}
function b() {
  console.log('b执行完毕');
}
a(b)

//输出:  a执行完毕
      //   b执行完毕

我们将要耗时的代码和不耗时的代码都各自放进两个函数里面,将不耗时代码的函数b作为实参,传给要耗时代码的函数a,在你想什么时候要让b执行的时候加上cb(),调用b自然就会在a执行完之后执行b,解决了异步问题。

但是在实际开发中如果要耗时函数有点多出现函数的嵌套,我们又要让它不出现异步问题,我们使用回调函数试试看。

function a(cb, cb2, cb3) {//callback回调函数   复杂,不好排查错误
  setTimeout(() => {
    console.log('a执行完毕');
    cb(cb2, cb3)
  }, 1000)
}
function b(cb, cb3) {
  setTimeout(() => {
    console.log('b执行完毕');
    cb(cb3)
  }, 1000)
}
function c(cb) {
  setTimeout(() => {
    console.log('c执行完毕');
    cb()
  }, 1000)
}
function d() {
  console.log('d执行完毕');
}
a(b, c, d)

//输出:a执行完毕
//b执行完毕
//c执行完毕
//d执行完毕

这一大串串的,写这里写一半我都觉得复杂了,我要在a里面传三个参数,在它肚子里面的callback回调函数我又要传两个参数,头大,写一半的时候还突然出错了,我还不知道是哪里出错,这就是使用回调函数的缺点,也就是业内所说的回调地狱:

如果函数嵌套过深,维护成本大,一旦出现错误难以排查

那我们还能用什么来解决异步问题吗? 这时候超级英雄promise来救场啦!

promise

我们来模拟一个场景,章总是一个视瓦为性命的瓦友(p),他一起床,那得干嘛,先吃个饭呗,不然就没劲打瓦了

function getup() {
    setTimeout(() => {
      console.log('章总起床了')
    }, 2000)
}
function eat() {
  setTimeout(() => {
    console.log('章总吃饭了')
  }, 1000)
}
getup()
eat()
//输出章总吃饭了
//章总起床了

章总简直就是超人,他还没有起床就能吃饭,哈哈哈哈,不扯淡了。我们来看这段代码,因为两个函数都是耗时的,我们虽然先调用getup再调用eat由于eat所需时间更短eat先执行,这个以后会在事件循环里面提到代码执行的顺序,接下来我们用promise来解决这个问题

function getup() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('章总起床了')
      resolve('起床成功')//没有这个resolve(),后面代码不执行
    }, 2000)
  })
}
function eat() {
  setTimeout(() => {
    console.log('章总吃饭了')
  }, 2000)
}
getup()
  .then((res) => {
    console.log(res)
    eat()
  })
  
//章总起床了
//起床成功
//章总吃饭了

来看看我们加了promise和之前的代码有什么不一样,我们在要先执行的代码里面加了一个return new Promise巴拉巴拉的一段固定句式,又多加了一个resolve(),然后在执行阶段,哦呦好像不太一样哦,我们用到了.then()方法,我先来大致介绍一下promise的用法:

promise用法

在耗时代码里面加上return new Promise( (resolve,reject) =>{函数体内容},我们可以看到这里Promise是大写的,很明显它是构造函数,我们这函数的返回值是构造函数Promise new 出来的实例对象,函数里面有两个形参resolvereject

二者对promise可谓是功不可没,如果没有调用这二者,则返回出来的实例对象状态就是Pending(待定),它无法做操作。

如果在函数体调用了resolve(value)对象的状态就会变成Fulfilled(已完成),在已完成状态下Promise对象可以使用.then方法,并传递结果 value

如果在函数体调用了reject(reason)对象的状态就会变成Rejected(已拒绝),在已拒绝状态下Promise对象可以使用.catch方法,并传递错误原因 reason

注意:Promise 的状态一旦从 Pending 变为 Fulfilled 或 Rejected,就不可再改变。因此,Promise 状态是单向的:从 Pending 转变为 Fulfilled 或 Rejected。这意味着你无法从一个已完成的 Promise 重新回到 Pending 状态。

我们来看看reject的情况

function a() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log('a');
      // resolve('a执行完毕');
      reject('a执行失败了')
    }, 1000)
  });
}
a()
  .then(res => {
    console.log(res);
  })
  .catch(err => {
    console.log(err);
  })
//输出a
//a执行失败了

接下来由我们的promise来挑战回调地狱面临的问题,章总打瓦流程

function getup() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('章总起床了')
      resolve('起床成功')//没有这个resolve(),后面代码不执行
    }, 2000)
  })
}

function eat() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('章总吃饭了')
      resolve('吃饭成功')
    }, 1000)
  })
}

function happy() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('章总打瓦了')
      resolve('打完了')
    }, 3000)
  })
}
function record() {
  console.log('章总0/21/0孤独carry')
}

getup()
  .then((res) => {
    console.log(res)
    return eat()
  })
  .then((res) => {
    console.log(res)
    return happy()
  })
  .then((res) => {
    console.log(res)
    record()
  })
  
//输出:章总起床了
//起床成功
//章总吃饭了
//吃饭成功
//章总打瓦了
//打完了
//章总0/21/0孤独carry

我们前面说了Promise实例对象在调用了resolve(value)对象的状态就会变成Fulfilled(已完成) ,在已完成状态下Promise对象可以使用.then方法,因此我们可以让第一个.then方法返回eat(),也就是返回了一个状态为Fulfilled(已完成)promise,因此我们可以继续接一个.then方法,依靠此方法我们能让这些函数像一条链子一样依次执行,不需要给过多的参数,代码写起来也十分优雅,甚至我们可以添加 catch 方法来捕获可能发生的错误,来更好地排查和处理异常情况。

欢迎大家的交流与指正看到这里了,就不妨动动手点个赞吧,谢谢大家