JavaScript 异步处理:从回调地狱到 Promise 和 async/await

在 JavaScript 中,异步操作是非常常见的。我们经常需要执行一些耗时的任务,例如发送网络请求、读取文件、定时器等,这些任务都是异步的,不会阻塞主线程的执行。为了有效地处理这些异步操作,并保持代码的可读性和可维护性,JavaScript 提供了多种异步处理的方法。今天我们将来聊聊js异步处理的发展。

同步和异步的概念

同步(Synchronous)指的是按照代码的顺序一步一步地执行,每一步必须等待上一步完成后才能执行下一步。在同步操作中,程序会阻塞并等待当前操作完成才能继续执行后续的操作。这意味着如果有一个操作耗时很长或者发生阻塞,整个程序都会被阻塞,直到该操作完成。同步操作适用于简单的任务或者需要按照严格顺序执行的任务。

异步(Asynchronous)指的是在遇到耗时的操作时,不会等待该操作的完成,而是继续执行后续的操作。在异步操作中,程序不会被阻塞,而是继续执行其他的任务。当异步操作完成后,会触发一个回调函数或者返回一个 Promise 对象来处理操作的结果。异步操作适用于需要进行网络请求、文件读写、定时器等耗时操作的场景,以提高程序的性能和响应速度。

通过代码我们能很轻松的理解:

javascript 复制代码
function a() {
    setTimeout(()=>{
        console.log('A');
    },1000)
}

function b() {
    console.log('B');
}

a()
b()

我们定义两个函数a和b,a中有一个setTimeout定时器,在一秒之后输出A,这是一个耗时操作,而函数b中只有一个打印B的命令,调用后立即执行,不是耗时操作。尽管我们是先调用a再调用b,但由于js代码是异步执行,则不会先执行需要耗时的a函数,而是先执行不需要耗时的b,结果如下:

如果是同步,则不管a函数耗不耗时,耗多长时间,都得按顺序先执行a再执行b。

总结起来,同步操作是按照顺序一步一步执行,必须等待上一步完成才能进行下一步;异步操作是不需要等待耗时操作的结果,可以继续执行其他任务,并通过回调函数或者 Promise 对象处理操作的结果。异步操作适用于需要响应速度和并发性的场景,而同步操作适用于简单的任务或者需要严格顺序执行的任务。

但是若是我们就是想让不耗时的b在耗时的a后面执行呢?

我们一起来看看js是如何处理的。

回调函数

在es6之前,处理异步的手段就是回调函数:

javascript 复制代码
function a() {
    setTimeout(()=>{
        console.log('A');
        b()
    },1000)
}

function b() {
    console.log('B');
}

a()

我们将b的调用放在打印A的命令后面,就能保证先输出A再输出B了:

但是回调函数这个办法有个很大的缺点,就是随着异步操作的嵌套增多,我们也容易陷入回调地狱:如果b也是耗时操作且所需时间依然比a短,要求b在a之后执行,这时再来个函数c,耗时比b短且需要在b之后执行,我们又需要把c函数的调用放在函数b中去,要是之后还有函数d,调用要放在函数c里,还有函数e、f、g...那么我们的代码的可读性和可维护性将变得非常差。

为了解决上述问题,es6引入了Promise对象

Promise

Promise异步处理如下,我们边看代码边解释:

1. promise 实例对象后面可以接 .then() ,then中回调的执行取决于promise中的resolve有没有生效

javascript 复制代码
    function a() {
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                console.log('A');
                resolve()
            },2000)
        })
    }

    function b() {
        setTimeout(()=>{
            console.log('B');
        },1000)
    }

    a().then(()=>{ 
        b()
    })

我们还是定义了两个函数a和b,b函数中的操作要比a耗时短,但我们需要先执行a再执行b。我们在函数a中通过Promise 构造函数创建一个 Promise 对象并将其返回。构造函数接受一个 executor 函数作为参数,该函数包含两个回调函数 resolve 和 reject,分别表示异步操作成功和失败时要执行的逻辑。当函数里面的异步操作'两秒后输出A'执行后,就调用resolve方法。

我们在a的调用后接上.then()方法,把b的调用放在里面,当a中Promise的resolve调用后,.then()中的回调开始生效执行,而A的输出的命令在resolve调用之前就已经完成,所以B会在A之后输出。

2. resolve(参数) 参数会传递给then中的回调函数

如果resolve()是有参数的,则参数会传递给then中的回调函数:

javascript 复制代码
function a() {
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log('A');
            resolve('hello')
        },2000)
    })

}

function b() {
    setTimeout(()=>{
        console.log('B');
    },1000)
}

a()
.then((res)=>{  
    console.log(res);
    b()
})

我们给resolve加上参数hello,then中的回调函数可以接收到,在里面加上打印这个参数的命令,结果是:

3. then 方法会默认返回promise对象,所以then2可以接在then的后面,当then当中有人为返回的新的promis对象时,then就将人为返回的promise对象作为唯一返回值,那么then2就相当于接在人为返回的promise对象后。

javascript 复制代码
    function a() {
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                console.log('A');
                resolve()
            },2000)
        })

    }

    function b() {
        return new Promise((resolve)=>{
            setTimeout(()=>{
                console.log('B');
                resolve()
            },1000)
        })

    }

    function c(){
        console.log('C');
    }

    a()
    .then(()=>{  
       return b()
    })
    .then(()=>{
        c()
    })

我们新加了个函数c,要求在执行函数b之后再执行a。then方法可以接在a()之后是因为它会返回一个Promise对象,而第二个then可以接在第一个then后面是因为then方法会默认返回一个Promise对象,此时我们需要在第一个then里将b的调用return一下,返回新的b的Promise对象,不然之后的第二个then方法会跟在第一个then默认的promise对象上,而不是b的Promise对象上,这样会导致c没有跟在b后面,会让c先执行:

所以一定要注意,连续使用then时,一定不要忘记将上一个then中的返回的Promise对象return。

实际上,当异步操作嵌套很多时,尽管Promise比回调函数美观很多,但它依然也会有一大串,于是在es7,又有了async-await。

async-await

async-await 关键字,进一步简化了异步处理的代码。async/await 是基于 Promise 的语法糖,使得异步代码看起来更像同步代码,同时保持了异步非阻塞的特性。

javascript 复制代码
    function a() {
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                console.log('A');
                resolve()
            },2000)
        })

    }

    function b() {
        return new Promise((resolve)=>{
            setTimeout(()=>{
                console.log('B');
                resolve()
            },1000)
        })

    }

    function c(){
        console.log('C');
    }
    
    async function foo(){
        await a()
        await b()
        c()
    }

    foo()

我们仍然需要和之前一样用Promise。之后使用 async-await,将异步操作包装在函数foo中,并使用 await 关键字等待 Promise 对象的解析。await 关键字会暂停异步函数的执行,直到 Promise 对象成功解析或失败为止。await a()会让之后的代码等待a执行完毕之后再去执行。

这样,就让异步处理的代码变得非常整洁美观啦!

本文的知识到这就结束啦,欢迎下次再来一起学习ヾ(◍°∇°◍)ノ゙!!

相关推荐
Neweee5 分钟前
JavaScript进阶内容详解
前端
大鸡爪6 分钟前
Vue3 组件库实战(五):Icon 图标组件的设计与实现
前端·vue.js
bluceli6 分钟前
前端测试实战指南:构建高质量代码的完整体系
前端·测试
行走的陀螺仪6 分钟前
前端公共库开发保姆级路线:从0到1复刻VueUse官方级架构(pnpm+Turbo+VitePress)
前端·架构
顽固_倔强6 分钟前
深入理解 Vue3 数据绑定实现原理
前端·面试
前端付豪7 分钟前
组件拆分重构 App.vue
前端·架构·代码规范
Wect8 分钟前
React 更新触发原理详解
前端·react.js·面试
cxxcode8 分钟前
Web 帧渲染与 DOM 准备
前端
光影少年8 分钟前
React Hooks的理解?常用的有哪些?
前端·react.js·掘金·金石计划
大鸡爪9 分钟前
Vue3 组件库实战(七):从本地到 NPM:版本管理与自动化发布指南(下)
前端·vue.js