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执行完毕之后再去执行。

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

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

相关推荐
J不A秃V头A36 分钟前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客1 小时前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林3 小时前
npm发布插件超级简单版
前端·npm·node.js
我码玄黄4 小时前
THREE.js:网页上的3D世界构建者
开发语言·javascript·3d
罔闻_spider4 小时前
爬虫----webpack
前端·爬虫·webpack