相亲→结婚→生娃:用人生大事 3 分钟吃透 Promise 链式调用

107 阅读6分钟

前言

首先我们来看一段代码

let a = 1
setTimeout(() => {
  a = 2
}, 1000)
// console.log('hello');  //导致这一行执行效率太低
console.log(a);    //  先跨过延时代码

代码开头声明了一个变量 a = 1,这段代码里面有一个计时器,倒计时 1 秒,里面也有一个 a ,但是值为 2,我们打印一下 a :

c6c330c9-64ad-42dd-a2a7-f583ba70da0f.png 可以看到打印出一个 1,而不是 2。我们正常理解的执行顺序应该是从上往下依次执行输出,但是这个仿佛就是直接跳过计时器输出结果,这就是我们今天要讲的 异步

一、什么是异步?

一句话先给答案: 异步就是“先挂号,不等叫号,继续干别的,轮到我了再回来”—— JavaScript 让浏览器(或 Node)在等待 一件耗时的外部事情 时,不阻塞主线程,而是把回调登记起来,事情搞定后再把回调插进事件队列,等主线程空了才执行。
首先我们知道 js 默认是单线程运行的(v8 默认只会开一个主线程来跑 js 代码),因为 js 设计之初是浏览器的脚本语言,设计成单线程可以节约用户的设备性能,同时 js 代码中存在耗时执行的(异步代码),会被v8 挂起,先执行不耗时的(同步代码)。这样说是不是就能一下理解上面那段代码为啥输出的是 1,而不是 2。

二、怎么处理异步?

上面我们讲解了异步的原理,也理解了为什么要有异步,

let a = 1
console.log(a);
function foo() {
  setTimeout(() => {
    a = 2
  }, 1000)
}
foo()
console.log('foo', a);
function bar() {
  setTimeout(() => {
    a = 3
  }, 2000)
}
bar()
console.log('bar', a);

7db872c8-21c6-4afc-b4fe-6bde6bd55b2c.png 但是现实场景我们就是要让他按顺序执行,就是要等上一段代码执行完再执行下一段代码,我就是要先输出 1再输出 2,最后再 3,我任性你管得着啊,碰到这种我们应该怎么处理呢?--- 两个办法:

1. 回调函数

let a = 1
console.log(a)
function foo() {
  setTimeout(() => {
    a = 2
    console.log('foo', a);
    bar()
  }, 1000)
}
function bar() {
  setTimeout(() => {
    a = 3
    console.log('bar', a);
  }, 2000)
}
foo()

我们直接把 console.log 放进定时器里面,并且在定时器里面调用后面要执行的函数,诶你不执行里面的代码,我不调用下一个函数不给你输出。这确实是一个处理异步的办法,但是呢,它同时会给你带来麻烦,打个比方就是你在写一个项目的时候有 n 个回调,万一其中有一个环节出问题了,你辛辛苦苦写了两天的代码最后运行报错,等你要去查找这个问题出在哪的时候,密密麻麻的全是回调,这你受得了?所以官方考虑到这个问题,直接给我们构造了一个 promise 用法

2.Promise

为了方便理解 promise ,我们重新构造一个相亲模型

function xq() {
    setTimeout(() => {
      console.log('广源相亲成功');
    }, 3000)
}
xq()
function marry() {
    setTimeout(() => {
      console.log('广源结婚了');
    }, 2000)
}
marry()
function baby() {
  setTimeout(() => {
    console.log('小源源出生');
  }, 1000)
}
baby()

有请我们男主角 -- 广源,广源到了该结婚的年龄,这时候他还没有对象,于是家里给他安排了相亲,如果相亲成功就能进行下一步 -- 结婚,结完婚就可以生小孩了,这个顺序不能乱,但是代码按照快的先执行原则,这肯定会导致异步,则会依次打印为

0a8aa7d5-4025-4f41-bbd2-a914518dd91f.png
那将会直接乱套,这时候我们就能用 promise 来解决它

function xq() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('广源相亲成功');
      resolve('相中')  // 返回一个成功状态
    }, 3000)
  })
}
function marry() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('广源结婚了');
      resolve('愉快')
    }, 2000)
  })
}
function baby() {
  setTimeout(() => {
    console.log('小源源出生');
  }, 1000)
}
 xq().then(() => {
 marry().then(() => {
  baby()
 })
 })  
 // 1. xq() 立即返回了一个 promise对象,状态为等待
// 2. 3 秒后xq()得到的promise对象 状态变为 成功
// 3. then 里面的回调函数才执行

f2ba21aa-d42c-46e6-b029-1c874383b96c.png 这样就让它成功依次执行了,之里面主要分成了三个步骤:首先 xq() 立即返回了一个 promise 对象,状态为等待,然后 3 秒后 xq() 得到的 promise 对象 状态变为 成功,最后 then 里面的回调函数才执行,但是你这样细细看过来会发现,这个 then 里面还是大肠包小肠,那我们能怎么修改让它更美观,话不多说直接揭晓答案

function xq() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('广源相亲成功');
      resolve('相中')  // 返回一个成功状态
    }, 3000)
  })
}
function marry() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('广源结婚了');
      resolve('愉快')
    }, 2000)
  })
}
function baby() {
  setTimeout(() => {
    console.log('小源源出生');
  }, 1000)
}
xq().then(() => {        // then源码默认也返回了一个promise对象,状态继承前面的xq
  return marry()    // return 让 then 返回的 promise 状态根据marry返回的 promise 状态而改变
})
.then(() => {
  baby()
})

我直接将 then 排成一排,这样就能一目了然,不过在这里我们需要注意一点, then 里面的 returnthen 返回的 promise 状态根据 marry 返回的 promise 状态而改变,如果你没有带上 return ,那么then 源码默认也返回了一个 promise 对象,状态继承前面的 xq ,如果都是一路顺风那还没啥问题,那万一广源在结婚过程中出了些问题,婚没结成,但是相亲是成功了的,这会导致继承了相亲时候的状态,从而变成了相亲成功,结婚失败,还有了小孩。这不就纯扯淡吗。所以切记不要忘了 renturn
说到这里,你可能就要问了,说这么多,那还不是和回调函数一样麻烦,出问题了照样找不到问题出在哪,别急,这不正要说的嘛。话不多说,直接上代码

    function xq() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('广源相亲成功');
      resolve('相中')  // 返回一个成功状态
    }, 3000)
  })
}
function marry() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('广源结婚了');
      reject('不愉快')
    }, 2000)
  })
}
function baby() {
  setTimeout(() => {
    console.log('小源源出生');
  }, 1000)
}
xq()
.then(() => {  
  return marry() 
})
.then(() => {
  baby()
})
.catch((err) => {
  console.log('catch', err);
})

3ea56264-921a-48eb-a031-f986780447c4.png
我们可以通过 catch 来捕捉到底是哪个环节出了问题,就和第 29 行代码一样,形参为 err ,最后返回的就是第 13 行代码 reject 里面的实参 “不愉快”,这样就能快速锁定错误点,来方便修改。

结语

今天用广源的故事,我们把“回调地狱”捋成了“ Promise 链”:

  1. 把每个异步步骤包成 Promise,成功 resolve、失败 reject
  2. return 把后一步的 Promise 交给下一个 .then ,顺序就能锁死。
  3. 最后只用一个 .catch ,链上任何环节出错都能精准捉虫。

记住口诀: 包 Promisereturn 接力、catch 兜底。 下次再遇到“必须先 A 再 B 再 C”的异步需求,直接把广源三部曲套上去,代码一口气排成队,维护不再掉头发!

在此特别感谢好基友广源的特别出场😁😁🙇🏻