您现在的位置是:首页 >技术杂谈 >彻底理解Promise和async/await网站首页技术杂谈

彻底理解Promise和async/await

weixin_49035434 2023-07-08 04:00:03
简介彻底理解Promise和async/await


Promise

1.异步行为是为了优化因计算量大而时间长的操作.

2.pedding 待定: 表示尚未开始或正在进行中

   fulfilled 解决: 表示已经成功完成

   rejected 拒绝: 表示没有完成

3.从pedding状态切换到fulfilled状态或rejected状态后,状态就不会再改变.而且也不能保证promise比如会脱离待定状态.

因此,无论promise是resolve还是reject,甚至永远处于待定状态,都应该具有恰当的行为

4.执行器函数的两项职责: 初始化promise的异步行为和控制状态的最终转换.

控制promise状态的转换是通过resolve()和reject()来实现的

resolve()会把状态切换为兑现

reject()会把状态切换为拒绝,reject()会抛出错误.

5. 执行器函数是同步执行的, 因为执行器函数是promise的初始化程序

console.log(1);

new Promise((resolve,reject) => {
    console.log(3);
})

console.log(2);

//打印1, 3, 2

6.无论resolve()和reject()中的哪个被调用,状态转换后都不可撤销. 继续修改状态会默认失败

const p = new Promise((resolve,reject) => {
    resolve('666')
    reject('000') // 没有效果
})

console.log(p); // 666

7.为了避免promise卡在待定状态,可以添加一个定时退出功能,通过setTimeout设置一个10秒后无论如何都会拒绝promise的回调

如果执行器中的代码在超时之前已经解决或拒绝,那么再次调用reject也会默认失败

8.Promise.resolve()

        通过Promise.resolve()静态方法,可以实例化一个解决的Promise

        使用这个静态方法实际上可以把任何值都转换为一个Promise

const p1 = new Promise((resolve,reject) => resolve())
const p2 = Promise.resolve() // undefined
const p3 = Promise.resolve(3) // 3
const p4 = Promise.resolve(4,5) // 4 多余参数会忽略, 可以使用对象

9.Promise.reject()

实例化一个拒绝的promise并抛出一个异步错误(这个错误不能通过try/catch捕获,只能通过拒绝处理程序捕获)

const p = Promise.reject(4) // promise <rejected> 4

10. Promise的实例方法

10.1.实现Thenable接口

10.2.Promise.prototype.then()

        Promise.prototype.then()是为Promise实例添加处理程序的主要方法.

        then()最多接收两个参数 onResolved处理程序和onRejected处理程序,这两个参数都是可选的分别进入兑现和拒绝状态

let p1 = new Promise((resolve,reject) => setTimeout(resolve,2000))
let p2 = new Promise((resolve,reject) => setTimeout(reject,2000))

p1.then(() => onResolved('p1'),
        () => onRejected('p1'))

p2.then(() => onResolved('p2'),
        () => onRejected('p2'))
        
function onRejected(id){
    console.log(id,'rejected');
}

function onResolved(id){
    console.log(id,'onResolved');
}

因为promise的只能转换最终状态一次,所以这两个操作一定的互斥的.

传给then()的任何非函数类型的参数都会被静默忽略.如果只提供onRejected参数,要在onResolved位置上传入undefined

p1.then('glsdfosdfisdjif') // 会被忽略,不推荐

p1.then(null,() => onRejected('p2')) // 不传onResolved的规范写法

Promise.prototype.then()返回一个新的Promise实例, 会通过Promise.resolve()隐式包装来生成新的Promise

const p3 = Promise.resolve('wei') // 默认返回undefined

10.3.Promise.prototype.catch()

Promise.prototype.catch()是为Promise实例添加拒绝处理程序.

实际上这个方法就是一个语法糖,相当于调用了 Promise.prototype.then(null,onRejected)

返回一个新的Promise实例,会通过Promise.resolve()隐式包装来生成新的Promise

10.4.Promise.prototype.finally()

        Promise.prototype.finally()用于给promise添加onFinally处理程序,在promise转换为解决或者拒绝都会执行, 主要是避免在resolve和rejected中出现冗余代码. 但是没办法知道promise的状态是解决还是拒绝.

返回一个新的Promise实例,不同于then()和catch()方法返回的实例.在大多数情况下它将表现为父Promise的传递.

10.5.非重入Promise方法

当Promise进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行.(就是被放到一个微任务里去了)

// 例子
const p = Promise.resolve()

p.then(() => {
    console.log('p.then()');
})

console.log('同步执行');
// 实际输出 同步执行, p.then()

// 例子

let sy;

let p = new Promise((resolve) => {
    sy = function() {
        console.log(1);
        resolve()
        console.log(2);
    }
})

p.then(() => {
    console.log(4);
})

sy()

console.log(3);

/* 
    实际输出: 1,2,3,4 即使Promise的状态发生在添加处理程序之后,
    处理程序也会等到运行的消息队列让出时,才会执行.
*/ 

10.6.临近处理程序的执行顺序

10.7.传递解决值和拒绝理由

        解决的值和拒绝的理由分别通过resolve()和reject()的第一个参数往后传,直到报错

        Promise.resolve() 和Promise.reject() 在被调用时就会接受解决值和拒绝理由.

10.8.拒绝Promise与拒绝错误处理

        拒绝Promise类似于throw()表达式.它们都代表一种程序状态, 即需要中断或者特殊处理

let p1 = new Promise((resolve,reject) => reject( Error('1')))
let p2 = new Promise((resolve,reject) => { throw Error('2') })
let p3 = Promise.resolve().then(() => { throw Error('3') })
let p4 = Promise.reject(() => Error('4'))

console.log(p1);
console.log(p2);
console.log(p3);
console.log(p4);

Promise.resolve().then()的错误最后才出现.

正常情况下,在通过throw()关键字抛出错误时,js运行时的错误处理机制会停止执行抛出错误之后的任何代码(抛出错误之后不解析了)

// 正常情况xxx不会执行
throw Error()
console.log('xxx');
// promise抛出错误时,因为错误实际上是从消息队列中抛出来的,所有不会阻止运行时
Promise.reject(Error('x'))
console.log('xxx'); // 会输出

// 例子:

        let p = new Promise((resolve,reject) => {
            console.log(1);
            reject(Error())
        }).then(() => {
            console.log(2);
        }).catch((e) => {
            console.log(3);
        }).catch(() => {
            console.log(4);
        }).then(() => {
            console.log(5);
        })

/*
    实际输出1,3,5 首先打印1, 没毛病, 接下来执行reject(),打印3, catch 函数会隐式的调用
    Promise.resolve()函数, 因此继续.then()打印5
*/

10.9.Promise连锁与合成

多个Promise组合在一起可以构成强大的代码逻辑.

通过两种方式实现: promise连锁和promise合成,前者是一个promise接一个promise的拼接,后者则是将多个promise组合成一个promise

        //要执行真正的异步任务,让每个执行器都返回一个promise实例
        let p1 = new Promise((resolve,reject) => {
            console.log(1);
            setTimeout(resolve,1000)
        })

        p1.then(() => new Promise((resolve,reject) => {
            console.log(2);
            setTimeout(resolve,1000)
        }))
        .then(() => new Promise((resolve,reject) => {
            console.log(3);
            setTimeout(resolve,1000)
        }))
        .then(() => new Promise((resolve,reject) => {
            console.log(4);
            setTimeout(resolve,1000)
        }))


        // 1 1秒后
        // 2 2秒后
        // 3 3秒后
        // 4 4秒后

   

        Promise的处理程序是按照它们添加的顺序执行的,由于Promise的处理程序是先添加到消息队列, 然后才逐个执行.

将多个Promise实例组合成一个Promise的静态方法. Promise.all()和Promise.race()

Promise.all()方法创建的promise会将一组promise全部解决后,再返回结果.

Promise.all()接收一个可迭代对象,返回一个新Promise

let p1 = Promise.all([
    Promise.resolve(),
    Promise.resolve()
])

// 可迭代对象中的元素会通过Promise.resolve()转换为Promise

let p2 = Promise.all([3,4])

// 空的可迭代对象等价于Promise.resolve()

let p2 = Promise.all([])

// 无效的语法

let p2 = Promise.all() // 报错

有一个Promise待定,则合成的Promise也会待定, 有一个Promise拒绝,则合成的Promise也会拒绝.

// 一次拒绝导致最终Promise的拒绝

let p1 = Promise.all([
    Promise.resolve(),
    Promise.resolve(),
    Promise.reject()
])

// 如果所有的promise都成功解决,则合成promise的解决值就是所有包含promise解决值的数组(按迭代器顺序)

let p1 = Promise.all([
    Promise.resolve(1),
    Promise.resolve(2),
    Promise.resolve()
])

p1.then(res => {
    console.log(res); [1, 2, undefined]
})

// 多个reject的情况

let p1 = Promise.all([
    Promise.reject(1),
    Promise.reject(2),
    Promise.resolve()
])

p1.then(res => {
    console.log(res); 
    /*
        1 会将第一个拒绝的理由作为合成promise的拒绝理由. 之后再拒绝的promise的不会
        影响最终promise的拒绝理由.
    */ 
})

Promise.race()方法创建的任意一个 Promise 对象状态变为 fulfilled 或 rejected 时立即返回该 Promise 对象的值或原因。

语法同all()

只要第一个落定的promise,Promise.race()就会包装其解决值或姐拒绝理由并返回新promise

ES6不支持取消Promise和进度通知,一个主要原因就是这样会导致promise连锁和promise合成过渡复杂化.

async await

功能: 让以同步方式写的代码能够异步执行.

async/await解决利用异步结构组织代码的问题.

1. async 关键字用于声明异步函数 可以用在函数声明,函数表达式,箭头函数和方法上

async function foo() {}

let bar = async function() {}

let baz = async () => {}

class Qux {

    async qux(){}

}

使用async关键字可以让函数具有异步特征,但总体上代码仍然是同步求值的.

而在参数或闭包方面,异步函数仍然具有普通函数js函数的正常行为.

例子:

async function foo() {
    console.log('1');
}

foo()

console.log(2);
// 1,2

异步函数如果使用return 关键字返回了值(没有return 返回undefined),这个值会被Promise.resolve()包装成一个Promise对象.

async function foo() {
    console.log('1');
    // return 666
    return Promise.resolve(666) // 效果同上
}

// 给返回的promise添加一个解决处理程序
foo().then(console.log)

// 1, 666

在异步函数中抛出错误会返回拒绝的Promise

async function foo() {
    console.log('1');
    throw 3
    console.log('4'); // 不执行
}

// 给返回的promise添加一个解决处理程序
foo().catch(console.log)

console.log(2);
// 1, 2, 3

2. 异步函数主要针对不会马上完成的任务,需要一种暂停和恢复执行的能力.

使用await关键字可以暂停异步函数代码的执行,等待Promise的解决.它可以单独使用,也可以在表达式中使用

await命令后面是一个Promise对象. 如果不是,会被转成一个立即resolve的Promise对象.

// 对拒绝promise使用await会释放错误值(将拒绝promise返回)

async function foo() {
    console.log('1');
    await Promise.reject(3)

    // return await Promise.resolve(3) 
    // resolve需要return then才能接受, 而reject不需要return catch也可以接收
    console.log('4'); // 这行代码不会执行
}

// 给返回的promise添加一个解决处理程序
foo().then(console.log).catch(console.log)

console.log(2);

// await Promise.reject(3) 打印输出1,2,3

只要一个await语句后面的Promise变成reject, 那么整个async函数都会中断执行.

// 下面await语句是不会执行的,因为第一个await已经变成了reject
async function foo() {
    await Promise.reject('出错了')
    await Promise.resolve('hello') // 不会打印
}

如果希望前一个异步操作失败,也不要中断后面的异步操作.

这时可以将第一个await放在try...catch 这也不管这个异步是否成功,第二个await都会执行

 async function foo() {
     try {
         await Promise.reject('出错了') // 会打印
     } catch (e) {
         console.log(e);
     }
     return await Promise.resolve('hello') // 会打印
 }
 foo().then(v => console.log(v))

另一种方式是在await后面的promise对象后添加一个catch方法,处理前面可能出现的错误

    async function foo() {
         await Promise.reject('出错了').catch (e => console.log(e))//会打印
         return await Promise.resolve('hello') // 会打印
    }
    foo().then(v => console.log(v))

3.await 的限制

await关键字必须在异步函数中使用.

不允许:await出现在箭头函数中

不允许:await出现在同步函数声明中

不允许:await出现在同步函数表达式中

不允许:IIFE使用同步函数表达式或箭头函数

停止和恢复执行

使用await关键字之后的区别其实看上去还要微妙一些.

async function foo() {
    console.log(await Promise.resolve('foo'));
}

async function bar() {
    console.log(await 'bar');
}

async function baz() {
    console.log('baz');
}

foo()
bar()
baz()
// 打印 baz, bar, foo

async/await中真正起作用的是await. async只是一个标识符,毕竟异步函数如果不包含await关键字,基本和普通函数没什么区别

async function foo() {
    console.log(2);
}

console.log(1);
foo()
console.log(3);

// 打印 1, 2, 3

要完全理解await关键字,必须知道它并非只是等待一个值可用那么简单.

js运行时在碰到await关键字时, 会记录在哪里暂停执行. 等到await后面的值可用了,js运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行

因此, 即使await后面跟着一个立即可用的值,函数的其余部分也会被异步求值.下面例子:

async function foo() {
    console.log(2);
    await null
    console.log(4);
}

console.log(1);
foo()
console.log(3);

// 打印 1, 2, 3, 4
/*
运行时的工作过程:
(1) 打印1;
(2) 调用异步函数foo();
(3) (在foo中)打印2;
(4) (在foo中)await关键字暂停执行,为立即可用的值null向消息队列中添加一个任务;
(5) foo()退出;
(6) 打印3;
(7) 同步线程执行完毕;
(8) js运行时从消息队列中取出任务,恢复异步函数执行;
(9) (在foo中)恢复执行,await取得null值(这里并没有用)
(10) (在foo中)打印4;
(11) foo()返回
*/


 

// TC39对await后面是promise的情况如何处理做过一次修改. 修改后await Promise.resolve(8)只会生成一个异步任务

async function foo() {
    console.log(2);
    console.log(await Promise.resolve(8));
    console.log(9);
}

async function bar() {
    console.log(4);
    console.log(await 6);
    console.log(7);
}

console.log(1);
foo()
console.log(3);
bar()
console.log(5);

// 打印 1, 2, 3, 4, 5, 8, 9, 6, 7

使用注意点:

 1.await命令后面的promise对象的运行结果可能是rejected,最好把await放在try...catch代码块中

 2.多个await命令后面的异步操作如果不存在继发关系,最好让他们同时触发

       

// getFoo和getBar是两个独立的异步操作,被写成激发关系,这样比较耗时.可以让它们同时触发
   let foo = await getFoo()
   let bar = await getBar()

   let [foo, bar] = await Promise.all([getFoo,getBar])

async函数的实现原理:

async函数的实现原理就是将Generator函数和自动执行器包装在一个函数里.

面试官: 来说一下你对Promise的理解?

个人对Promise的理解是, promise是一种异步编程的解决方案, 它比传统的回调函数加事件更加合理合强大,

目前除了使用promise的异步操作外,还使用promise在项目中解决回调地狱等问题.

promise是一个对象可以获取异步操作的信息

promise的特点:

         对象不受外界影响,promise一共有三个状态, 分别是进行中,成功和失败,只有异步操作的结果,可以决定是哪一种状态,任何其他的操作都无法改变这个状态.

        一旦状态改变就不会在变,任何时候都可以得到这个结果,promise的状态改变只有两种可能要么成功要么失败

如果要使用promise必须对promise进行实例化, 实例化之后promise内有一个回调函数,这个函数有两个参数,分别是resolve和reject,

当我们的状态发生变化的时候,如果是成功则会通过resolve将成功的结果返回,如果失败通过reject将错误的信息返回出去.

通过.then方法接收成功的结果,通过.catch方法接收失败的结果

promise常用的方法还用promise.all(),race()方法. 主要是将多个实例包装成一个新的实例.

Promise.all()方法创建的promise会将一组promise全部解决后,再返回结果.

Promise.race()方法哪个接口跑的快,先返回哪个

在项目中一般使用promise来对接口进行封装,以及一些异步的操作都会用到promise.校验

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。