JS异步大法总结篇(二):从回调到Promise,摆脱磨人的回调小妖精

239 阅读7分钟

前言: 在上一篇文章JS异步大法总结篇(一):消息队列和事件循环机制,站在山顶看事件机制我们聊到了任务执行机制里面的一些基本知识,我们对消息队列、事件循环机制、宏任务与微任务有了基本的理解。今天我们主要来聊聊从回调函数到Promise的发展。 Promise可以说是前段里面非常重要的内容之一了,有这么一个比喻说:Promsie是前端开发的“水”’和“电”。近些年涌现的一些新技术的api以及前端框架很多内容都是基于Promise的,所以说深入理解Promise能够帮助我们更好地开发。

本文先是介绍回调函数以及它带来的问题,通过问题来引入Promise。接着详细介绍了Promise的内容。本文阅读时长大约为20mins。

1.回调函数:层层嵌套让人头皮发麻

上一节对回调函数的概念以及同步回调和异步回调有了基本介绍。 回调函数应该是大家经常使用到的,以下代码就是一个回调函数的例子:

ajax(url, () => {
    // 处理逻辑
})

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback-hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

以上代码看起来不利于阅读和维护,当然,你可能会想说解决这个问题还不简单,把函数分开来写不就得了

function firstAjax() {
  ajax(url1, () => {
    // 处理逻辑
    secondAjax()
  })
}
function secondAjax() {
  ajax(url2, () => {
    // 处理逻辑
  })
}
ajax(url, () => {
  // 处理逻辑
  firstAjax()
})

以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题:

  • 第一个就是嵌套调用:嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  • 第二个就是错误处理:嵌套函数一多,就很难处理错误

解决:

  • 消灭循环嵌套
  • 合并多个错误处理

2.Promise:登上历史舞台

Promise是如何解决消灭循环嵌套和合并多个错误处理这两个问题的呢?回答之前我们先来看看Promise是什么。

2.1 Promise的基本结构

Promise的三种状态

  • pending(请求状态)
  • resolve(成功状态)
  • reject(失败状态)

Promise.prototype.then()

:我们用Promsie写一个setDelay() 延迟执行函数,以下的内容都会基于这个例子来说明: then()函数里面是resolve成功之后返回的内容

const setDelay = (time) => {
    return new Promise((resolve, reject) => {
        if (typeof time != 'number' || time > 5) reject(new Error('输入的time有误,请重新输入!'))
        setTimeout(() => {
            resolve(`执行setDelay() 共执行了${time}秒哦`)
        }, time * 1000)
    })
}
setDelay(3)
.then(result=>{
    console.log(result) //执行setDelay() 共执行了3秒哦
})

注:如果上一个Promise对象没有返回,下一个则为undefined

Promise.prototype.catch()

:catch()函数里面是reject失败之后返回的内容

setDelay(3)
.then(result=>{
    console.log(result)
})
.catch(err=>{
    console.log(err) //输入的time有误,请重新输入!
})

Promise.prototype.finally()

:finally()中的代码无论如何都是会执行的

setDelay(3)
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

Promise的链式写法

接下来我们在写一个延迟函数setDelaySeconds():

  const setDelaySeconds = (seconds) => {
    return new Promise((resolve, reject) => {
        if (typeof seconds != 'number' || seconds > 5000) reject(new Error('输入的time有误,请重新输入!'))
        setTimeout(() => {
            resolve(`执行了setDelaySeconds() 共执行了${seconds}毫秒哦 `)
        }, seconds)
    })
}

假设我们要在下一个需要依赖的resolve去返回另一个Promise,会发生什么呢?我们执行一下:

const setDelay = (time) => {
    return new Promise((resolve, reject) => {
        if (typeof time != 'number' || time > 5) reject(new Error('输入的time有误,请重新输入!'))
        setTimeout(() => {
            resolve(setDelaySeconds(time*3000)  //在setDelay的内部返回一个Promise
        }, time * 1000)
    })
}
setDelay(3)
.then(result => {
    console.log(result)  //执行了setDelaySeconds() 共执行了3000毫秒哦
})
.catch(err => {
    console.log(err)
})

但是加入有多个依赖,耦合度就会很高。有人说,我不想耦合性这么高,想先执行setDelay函数再执行setDelaySeconds,但不想用上面那种写法,可以吗,答案是当然可以。链式写法就派上用场了。

setDelay(3)
    .then((result) => {
        console.log(result)
        //假如这里没有返回 如:setDelaySeconds(3000) 下面then中就会输出undefined
        return setDelaySeconds(3000)  
    })
    .then(result => {
        console.log(result)
    })
    .catch(err => {
        console.log(err)
    })

Promise采用链式写法之后代码变得更加清晰易懂,同时也对错误进行了有效处理。

Promise三个注意点:

  • 返回值穿透
setDelay(3)
    .then((result) => {
        console.log(result)
        //以下返回一个Promise对象返回值会发生穿透 将Promise对象穿透到最外层 在下一层的then()中去调用
        return setDelaySeconds(3000)  
    })
  • 延时绑定回调机制
const setDelay = (time) => {
    return new Promise((resolve, reject) => {
        if (typeof time != 'number' || time > 5) reject(new Error('输入的time有误,请重新输入!'))
        //下面resolve成功返回的内容会延迟绑定到then()中执行
        //console.log('setDelay()开始执行')会先于resolve的内容
        resolve(`执行了setDelay() 共执行了${time}秒哦`)
        console.log('setDelay()开始执行')
    })
}
  • 错误冒泡机制
setDelay(3)
    .then((result) => {
        console.log(result)
        //假如这里没有返回 如:setDelaySeconds(3000) 下面then中就会输出undefined
        return setDelaySeconds(3000)  
    })
    .then(result => {
        console.log(result)
    })
    //所有的错误会向上冒泡 一直找到catch语句为止 若最后没有被捕获,则会报错。
    .catch(err => {
        console.log(err)
    })

也可以在每个then方法后面触发catch,但是不推荐 如下:

setDelay(6)
.then((result)=>{
  console.log('第一步完成了');
  console.log(result)
  return setDelaySecond(3)
})
.catch((err)=>{   // 这里移到第一个链式去,发现上面的不执行了,下面的继续执行
  console.log(err);  //输入的time有误,请重新输入!
})
.then((result)=>{  //下面的代码依旧会执行
  console.log('第二步完成了');
  console.log(result);
})
  • 链式中的catch并不是终点!!catch完如果还有then还会继续往下走!
  • 所以,catch放的位置也很有讲究,一般放在一些重要的、必须catch的程序的最后。这些重要的程序中间一旦出现错误,会马上跳过其他后续程序的操作直接执行到最近的catch代码块,但不影响catch后续的操作!

2.2 如何跳出Promise循环?

Promise也有一些缺点。无法取消Promise,一旦新建它就会立即执行,无法中途取消。 其实我们可以在return一个新的Promise对象,让代码一直处于一个pending状态,此时后面的代码不会执行。当然这本质上并不是取消Promise,只是将请求的状态挂起。

.then(result => {
        console.log(result)
        console.log('我从这里跳出循环')
        return new Promise(() => { console.log('后面的不会在执行了') })
    })

缺点:

  • 一直处于pending状态下的Promise会一直处于被挂起的状态,可能会导致潜在的内存泄漏;
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成) 所以如何跳出Promise循环,是一大难点,问题本身比较复杂。有兴趣的同学可以去了解一波。

2.3 其他Promise方法介绍

Promise.all() :通过数组将多个Promise对象包装成一个Promise对象。 注:[]数组里的多个Promise对象为并行执行,所有的对象都成功返回则输出一个数组,否则输出错误

Promise.all([setDelay(3),setDelaySeconds(3000)])
  .then(result=>{
      console.log(result)  //["先是执行setDelay() 共执行了3秒哦", "先是执行了setDelaySeconds() 共执行了3000毫秒哦"]
  })
  .catch(err=>{
      console.log(err)  //假如有一个对象返回失败则输出 Error: 输入的time有误,请重新输入!
  })

一些其他Promise方法可以移步阮大神的ES6教程传送门

  • Promise.race()
  • Promise.allSettled()
  • Promise.any()...

总结:我们从callback回调函数出发,了解了Promise对象的前世今生。以问题为导向,引出了Prmise对象。通过Promise异步回调解决了callback的回调地狱和合并多个错误处理。接下来通过延迟函数的例子进一步了解了Promise的结构、方法等。Promsie的世界还有许多好玩的事情,这篇文章只是让你迈进这个世界的大门。在日常的工作学习当中对于异步的解决办法仍然需要进一步的学习。下一小节中我们来看看Promsie的进化版,async/await这对好基友。

问题:你能自己总结callback回调函数、Promise解决异步问题的优缺点吗?

参考资料: 阮一峰老师的ES6教程-Promise对象 Promise:使用Promise,告别回调函数

本人前端菜鸟一枚,可能有一些对方总结不到位或者存在问题,欢迎大家批评斧正,也希望与大家一起交流学习,共同进步。