异步编程(Promise详解)

目录

异步编程

回调函数

回调地狱

Promise

基本概念

Promise的特点

1.Promise是一种构造函数

2.Promise接收函数创建实例

3.Promise对象有三种状态

4.Promise状态转变不可逆

5.Promise 实例创建即执行

6.Promise可注册处理函数

7.Promise支持链式调用

Promise的静态方法

1.resolve()方法

2.reject()方法

 3.all()方法

4.race()方法

5.allSettled() 方法

Promise的实例方法

1.then() 方法

2.catch() 方法

 3.finally() 方法

async/await 

基本概念

基本用法

async

await

使用示例

async/await 的优势


异步编程

JavaScript 在设计之初就是单线程的,这意味着它在同一时间只能执行一个任务,然而,浏览器和 Node.js 环境都提供了多种机制来允许 JavaScript 代码在不阻塞主线程的情况下执行异步操作。

此处仅以浏览器环境下举例( 参考浏览器渲染基本原理 )

浏览器有三大进程 —— 网络进程,浏览器进程和渲染进程。每个窗口开启一个渲染进程。每个渲染进程仅开启一个渲染主线程,主线程负责执行代码(HTML,CSS,JS),渲染页面。

所以在浏览器中,JS是运行在承担着诸多工作的渲染主线程上的。如果使用同步的方式(等待任务结束再执行下一个任务),就有可能会造成阻塞,浪费主线程的宝贵时间;还可能导致页面无法及时更新,给用户造成“卡死”现象。

所以采用异步执行来避免:当发生耗时任务时,渲染主线程会将任务交给其它渲染子线程取处理,自身则立即结束任务的执行,转而执行后续代码。当其他线程完成处理后,会将事先传递的回调函数包装成任务,加到消息队列末尾排队,等待渲染主线程通过事件循环机制调度执行。

在这种异步模式下,浏览器永不阻塞,从而最限度的保证了单线程的流畅运行。

 举例:

setTimeout(function () {
    console.log('异步任务')
}, 0)
console.log('同步任务')
// 输出顺序  同步任务 ,异步任务

 使用异步方式执行任务,在不阻塞主线程的同时还能极大的缩短程序执行所需的时间

该图摘自菜鸟教程中的异步编程小节

回调函数

在JavaScript中,回调函数是一段可执行的代码段(function),它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段代码(简单来说就是函数作为参数传递到另外一个函数中,这个作为参数的函数就是回调函数)。

回调函数是 JavaScript 异步编程中最早也是最基本的模式。当一个异步操作完成时,它会调用事先定义好的回调函数来处理异步操作完成后的结果或响应。

回调函数在异步编程中的作用:将异步操作的处理逻辑封装在回调函数中,然后在异步操作完成时执行这些逻辑。这有助于将异步代码与同步代码分离,使得程序的结构更加清晰。

const success = (data) => {
    console.log('请求成功,开始处理' + data)
    //....其他操作
}
const fail = (err) => {
    console.log('请求失败,开始处理错误' + err)
    //....其他操作
}
//回调函数callback1,请求失败时的操作
//回调函数callback2,请求成功时的操作
function foo(signal, callback1, callback2) {
    //模拟异步请求
    setTimeout(() => {
        if (signal === 'ok') {
            callback1('data')
        } else callback2('err')
    }, 1000)
}
foo('ok', success, fail)

在上面的例子中,等待1s后(模拟耗时任务),根据任务结果(此处我用foo()的第一个参数‘ok’代替),传递任务结果并调用相应的回调函数。

回调地狱

最简单的网络请求大概是这样

请求1(function callback(请求1的结果) {
    处理请求1的结果
})

很简单

但是随着需求的变化,当我们需要根据第一次请求的结果进行第二次请求时

请求1(function callback(请求结果1) {
    //根据请求1的结果,进行请求2
    请求2(function callback(请求结果2) {
        处理请求结果2
    })
})

也不复杂

直至,需求不断变化,最终出现了类似下面的情况

请求1(function callback(请求结果1) {
    //根据请求1的结果,进行请求2
    请求2(function callback(请求结果2) {
        //根据请求2的结果,进行请求3
        请求3(function callback(请求结果3) {
            //根据请求3的结果,进行请求4
            请求4(function callback(请求结果4) {
                //根据请求4的结果,进行请求5
                请求5(function callback(请求结果5) {
                    // ...
                })
            })
        })
    })
})

这下懵圈了,在异步请求和回调函数的不断嵌套下,臭名昭著的 回调地狱 诞生了。

试想一下,在回调函数内部可能会存在大量的处理逻辑,并且每一次的请求结果会成为下一请求的依赖,请求又会存在有失败和成功两种情况.....如此一来,代码将显得十分臃肿复杂,后期为维护将变得十分困难。

回调地狱带来的负面作用

  • 代码臃肿
  • 可读性差
  • 耦合度过高,可维护性差
  • 代码复用性差
  • 容易滋生 bug
  • 只能在回调里处理异常

Promise

基本概念

为了优雅的解决回调地狱的问题,在ES6中,一种新的异步编程的一种解决方案Promise诞生了。

在JS中,Promise是一个代表了异步操作最终完成或失败,及其结果值的对象。它允许开发人员为异步操作的成功和失败情况注册处理程序,极大地简化了异步编程的复杂性。

Promise的特点

1.Promise是一种构造函数

通过new关键字创建实例对象,构造函数自身有all,race,reject,resolve等静态方法,其原型对象上有then,catch,finally等方法。

let promise = new Promise(() => {})
console.log(typeof Promise)//function
console.log(typeof promise)//object

//查看Promise函数对象
console.dir(Promise)

2.Promise接收函数创建实例

创建一个新的Promise实例时,必须传递一个函数作为参数给Promise的构造函数,它定义了异步操作的逻辑。这个函数被称为执行器(executor)函数,它本身接受两个函数作为参数:resolvereject。这两个参数也是函数,由JavaScript引擎提供,用于改变Promise的状态。

let promise = new Promise((resolve, reject) => {})
3.Promise对象有三种状态

分别为pending(进行中),fulfilled(已成功)和rejected(已失败),其通过执行器(executor)函数接收的resolve和reject来改变。

        1). new Promise()

let promise = new Promise((resolve, reject) => {})
console.dir(promise)

 新创建的promise实例,其状态为pending(进行中),结果为undefined

        2). resolve(value)

let promise = new Promise((resolve, reject) => {
    resolve('完成结果值')
})
console.dir(promise)

resolve()接收异步任务执行结果的值作为参数,并将promise实例的状态转变为fulfilled(已成功),结果为 resolve()接收的值

         3).rejected(reason)

let promise = new Promise((resolve, reject) => {
    reject('完成失败原因')
})
console.dir(promise)

rejected()接收异步任务执行失败的原因作为参数,并将promise实例的状态转变为rejected(已失败),结果为 rejected()接收的失败原因

4.Promise状态转变不可逆

 Promise 状态的转变是不可逆且只能发生一次,且只能由异步操作结果决定。任何其他操作都不能改变这个状态

5.Promise 实例创建即执行
let promise = new Promise((resolve, reject) => {
    reject('获取数据失败')
})

创造 promise 实例后,它会立即执行的特点,所以该段代码运行后会立即抛出一个错误

但如果将其放入函数的私有作用域内,由于函数的特性,promise仅在函数被调用时,才会执行

6.Promise可注册处理函数

可通过.then()和.catch()方法为promise注册处理函数,在promise状态改变时调用(then和catch的具体用法见下方Promise的实例方法

7.Promise支持链式调用

建议先阅读下方Promise的实例方法Promise链式调用的核心时then方法,由于then方法返回一个新的 Promise实例对象(该对象也就可以调用promise的实例方法),这使得我们可以继续链式调用其他then()方法。如此一来便可以很容易地处理异步操作的序列。

注意:then()的链式调用中,下一个依赖于上一个返回结果

举例:

function fetchData(url) {
    return new Promise((resolve, reject) => {
        // 假设这是一个异步请求数据的函数
        setTimeout(() => {
            if (url) {
                resolve(`数据来自 ${url}`)
            } else {
                reject('URL 不能为空')
            }
        }, 1000)
    })
}
// 链式调用
fetchData('https://example.com')
    .then((data) => {
        console.log(data) // 输出:数据来自 https://example.com
        return fetchData('https://another.com') // 返回另一个 Promise
    })
    .then((anotherData) => {
        console.log(anotherData) // 输出:数据来自 https://another.com
    })
    .catch((error) => {
        console.error('捕获到错误:', error) // 如果有 Promise 被拒绝,则执行这里
    })
    .finally(() => {
        console.log('所有操作完成,无论成功还是失败') // 无论成功还是失败都会执行
    })

Promise的静态方法

Promise 提供了几个静态方法,这些方法不是用于创建新的 Promise 实例的,而是用于处理 Promise 对象的集合或进行某些特定的操作(只能由构造函数调用)。

1.resolve()方法

静态方法 Promise.resolve(value) 将异步执行的结果value作为参数,返回一个以给定值解析后的已完成状态的 Promise 对象。

 Promise.resolve('成功了').then((value) => {
     console.log(value) // 输出: 成功了
 })
 // 如果传入的是 Promise 对象,则直接返回该 Promise 对象
 let promise = new Promise((resolve) =>
     resolve('直接返回的 Promise')
 )
 Promise.resolve(promise).then((value) => {
     console.log(value) // 输出: 直接返回的 Promise
 })
2.reject()方法

静态方法 Promise.reject(reason) 返回一个以特定原因(reason)拒绝状态的 Promise 对象。

Promise.reject('出错了').catch((error) => {
    console.log(error) // 输出: 出错了
})
 3.all()方法

静态方法 Promise.all( iterator ) 用于处理一个 Promise 对象的数组(或可迭代对象),并返回一个新的 Promise 实例。这个新的 Promise 实例会在所有给定的 Promise 都成功完成时才会成功完成,其结果是一个数组,包含了所有给定 Promise 的结果(按相同的顺序)。

function delay(time, value) {
    return new Promise((resolve) =>
        setTimeout(() => resolve(value), time)
    )
}
Promise.race([
    delay(1000, '1s'),
    delay(2000, '2s'),
    delay(3000, '3s')
]).then((results) => {
    console.log(results) // 输出: ['1s', '2s', '3s']
})
4.race()方法

静态方法 Promise.race( iterator ) 同样接收一个 Promise 对象的数组(或可迭代对象),但它返回的新 Promise 实例会在给定的 Promise 数组中任何一个 Promise 改变状态(无论是成功还是失败)时,立即以那个 Promise 的结果作为自己的结果来改变状态。

function delay(time, value) {
    return new Promise((resolve) =>
        setTimeout(() => resolve(value), time)
    )
}
Promise.race([
    delay(1000, '1s'),
    delay(2000, '2s'),
    delay(3000, '3s')
]).then((results) => {
    console.log(results) //  输出: 1s,因为第一个 delay(1000) 最快完成  
})
5.allSettled() 方法

静态方法 Promise.allSettled(iterable)(ES2020 新增) 返回一个在所有给定的 Promise 都已经 settled(完成,无论结果如何)后完成的 Promise 实例,结果是一个数组,数组中的每个元素都是一个对象,表示对应的 Promise 的结果。

Promise.allSettled([
    Promise.resolve(3),
    Promise.reject(-1),
    Promise.resolve(5),
    Promise.reject(4)
]).then((results) => {
    console.log(results)
})
/* 输出:  
[  
  { status: 'fulfilled', value: 3 },  
  { status: 'rejected', reason: -1 },  
  { status: 'fulfilled', value: 5 },  
  { status: 'rejected', reason: 4 },  
]  
*/

Promise的实例方法

Promise 的实例方法主要包括 .then(), .catch(), 和 .finally()。这些方法允许你以链式调用的方式处理 Promise 的结果或错误。

1.then() 方法

   1).then(onFulfilled , onRejected)

  • onFulfilled :指定resolve时调用的函数,它接收 Promise 的值作为参数(一般由resolve传递)
  • onRejected( 可选 ):指定reject时调用的函数,它接收 Promise 的拒绝原因作为参数(一般由reject传递)

    2).then() 方法主要用于处理 Promise 成功的情况,但也允许提供可选第二个参数                        onRejected 函数来处理其失败的情况

   3).then() 返回一个新的 Promise 实例对象

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject('出错了'), 1000)
})
promise.then(
    (value) => console.log(value), // 不会被调用,因为 Promise 被拒绝了
    (reason) => console.log(reason) // 会被调用,并输出: 出错了
)
2.catch() 方法

   1).catch(onRejected)方法是 .then(null, onRejected) 的语法糖

  • onRejected:指定reject时调用的函数,它接收 Promise 的拒绝原因作为参数(一般reject传递)

    2)catch() 主要用于处理 Promise 拒绝(rejected)的情况

    3).catch() 也返回一个新的 Promise 实例对象

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject('出错了'), 1000)
})
promise.catch((error) => {
    console.log(error) // 输出: 出错了
    // 可以选择在这里解决这个 Promise,返回一个值或者另一个 Promise
    // return '错误已处理';
})

      4).不过它还有另外一个作用:在执行 resolve 的回调(也就是上面 then 中的第一个参

       数)时,如果抛出异常了(代码出错了),那么并不会报错卡死 js,而是会进到这个 

  catch 方法中。

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject('失败'), 1000)
})
    .then((value) => {
        console.log(value)
        return '继续处理'
    })
    .catch((error) => {
        console.log(error) //输出失败
    })

 控制台结果

 

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject('失败'), 1000)
}).then((value) => {
        console.log(value)
        return '继续处理'
    })

 控制台结果

 catch 既能处理 reject 回调,也能捕捉错误

 3.finally() 方法
  • .finally(onFinally)

  • onFinally :当 Promise 结束时调用的函数,它不接收任何参数,也不关心 Promise 的结果。在 Promise 结束时,无论它是成功还是失败,都会回调执行。这为在 Promise 链的末尾执行清理操作提供了一种方式,无论 Promise 的结果如何
  • finally() 也返回一个新的 Promise 实例对象
let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('成功'), 1000)
    // 或者 setTimeout(() => reject('失败'), 1000);
})
promise
    .then((value) => {
        console.log(value) // 如果 Promise 成功,则输出 '成功'
        return '继续处理' // 可以返回一个新的值或另一个 Promise
    })
    .catch((error) => {
        console.error(error) // 如果 Promise 失败,则输出错误信息
    })
    .finally(() => {
        console.log('Promise 结束了,无论成功还是失败')
    })

async/await 

基本概念

Promise 虽然摆脱了回调地狱,但 then 链式调⽤的阅读负担还是存在的,为了优代码的阅读体验,增强了代码的可读性和可维护性,一种新的异步编程解决方案async/await诞生了。

async/await 是 JavaScript中用于处理异步操作的关键字对。它们使得异步代码看起来和写起来更像是同步代码,从而大大增强了代码的可读性和可维护性。这对关键字特别适用于处理如文件读写、网络请求等耗时的异步操作。

基本用法

async
  • async关键字用于声明异步函数。这意味着该函数内部可以进行异步操作,比如发送网络请求、读取文件等。
  • 异步函数内部可以使用 await 关键字来等待异步操作的完成。
  • 异步函数会隐式地返回一个 Promise 对象。如果函数执行成功并且显式地返回了一个值,那么这个值会被封装成一个解析状态(fulfilled)的 Promise 对象。如果没有显式地返回任何值,或者返回了一个非 Promise 类型的值,JS会自动将这个值封成 Promise.resolve(value)(其中 value 是函数的返回值或 undefined),并返回这个 Promise 对象。
  • async function fun() {
        return 'async函数显示返回的值'
    }
    console.log(fun())

  • 如果异步函数在执行过程中抛出了异常,那么这个异常会被捕获,并导致返回的 Promise 对象进入拒绝(rejected)状态,异常信息作为拒绝的原因。
await
  • await 关键字只能在 async 函数内部使用。它用于等待一个 Promise 完成。
  • 使用 await 时,async 函数的执行会暂停,直到等待的 Promise 被解析(fulfilled)或拒绝(rejected)。
  • 如果 Promise 被解析,await 表达式的结果就是 Promise 解析的值。此时,async 函数的执行会继续进行。
  • 如果 Promise 被拒绝,await 表达式会抛出一个异常。这个异常可以被 async 函数内部的 try...catch 语句捕获,也可以继续向上传播,直到被更高层级的错误处理机制捕获。
使用示例

假设我们有一个异步函数 fetchData,它返回一个 Promise,该 Promise 在一段时间后解析为一些数据:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('数据加载完成')
        }, 1000)
    })
}

使用 async/await 来调用这个函数:

async function loadData() {
    try {
        const data = await fetchData()
        console.log(data) // 输出: 数据加载完成
    } catch (error) {
        console.error('数据加载失败:', error)
        //throw error; // 可选:重新抛出错误,以便上层调用者可以处理  
    }
}
loadData()

在这个例子中,loadData 是一个 async 函数,它内部使用了 await 来等待 fetchData 函数返回的 Promise 完成。当 Promise 被解析时,await 表达式的结果(即 '数据加载完成')被赋值给 data 变量。如果 fetchData 函数抛出了一个错误(虽然在这个例子中没有),那么错误会被 catch 块捕获。

注意:

  • await 只能在 async 函数内部使用。
  • await 会暂停 async 函数内部后续代码的执行,直到 await 后的 Promise 完成。
  • await 可以用在任何返回 Promise 的表达式上,不仅仅是函数调用。
  • 使用 async/await 可以使异步代码看起来更加直观和易于理解,但也要注意不要滥用,特别是在处理大量并发异步操作时。
async/await 的优势
  1. 代码清晰async/await 使得异步代码看起来和写起来更像是同步代码,提高了代码的可读性和可维护性。

  2. 错误处理:使用 try...catch 可以很容易地捕获和处理 await 表达式抛出的异常,这与同步代码中的错误处理非常相似。

  3. 条件语句和循环:在 async 函数中,可以像处理同步代码一样使用 if 语句、for 循环等控制流语句来等待 Promise 完成。

  4. 中间件和组合:虽然 async/await 本身并不提供新的组合或中间件模式,但它与 Promises 很好地结合,使得可以轻松地组合多个异步操作。


---------------------------------------------------------------------------------------------------------------------------------

若有错误或描述不当的地方,烦请评论或私信指正,万分感谢 😃

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值