前端开发日记 # Promise & Async/await

153 阅读10分钟

简介

是ES6提供的一种异步编程新解决方案

作用

可以使用链式调用来进行异步编程,主要解决回调地狱问题

异步编程

应用场景

  • IO操作

文件操作网络请求数据库操作

  • 定时器

setTimeout()setInterval()

上面两种情况都非常适合使用异步编程,因为可以提高CPU执行效率

编码方式

  1. 回调函数方式
  2. Promise方式
  3. async/await方式 [推荐]

Promise 初体验

创建Promise对象用于封装异步操作,并且可以把异步操作的结果值传递下去

const p = new Promise(function(resolve, reject){
    setTimeout(()=>{
         // 获取一个随机数
        const num = rend(1,100);
        
        if(num > 30){
            resolve(num);
        }else{
            reject(num);
        }
    }, 2000);
})

p.then((value) => {
    // Promise对象状态已变为成功
    alert("成功的值:"+value);
}, (reason) => {
    // Promise对象状态已变为失败
    alert("失败的值:"+reason);
})

创建Promise实例所需传的函数体内代码并非异步执行,而是同步执行,但函数体内需放异步代码,否则没意义

练手小案例

封装fs文件读取操作(Node端)

function myReadFile(path){
    return new Promise((resolve, reject) => {
        require('fs').readFile(path, (err, data)=>{
            if(err){
                reject(err)
            }else{
                resolve(data);
            }
        });
    })
}

// 使用
myReadFile('./assets/content.txt').then(value=>{
    xonsole.log(value);
}, reason=>{
    console.log(reason);
})

封装ajax请求(浏览器端)

function sendRequest(url, method){
    return new Promise((resolve, reject)={
        const xhr = new XMLHttpRequest();
        xhr.open(mehtod, url);
        xhr.send();
        // 处理结果
        xhr。onreadystatechange = function(){
            if(xhr.readyState === 4){
                if(xhr.status >= 200 && xhr.status < 300){
                    resolve(xhr.response);
                }else{
                    reject(xhr.status)
                }
            }
        }
    })
}

// 使用
sendRequest('http://www.xxx.xxx/api/getList').then(value=>{
    xonsole.log(value);
}, reason=>{
    console.log(reason);
})

两个属性

Promise对象上有两个属性 [[PromiseStatus]][[PromiseValue]]

PromiseStatus

Promise对象的状态,只存在3种状态

1、pending 未决定(初始值状态)
2、resolved/fullfilled 成功
3、rejected 失败

状态发生变化,只有2种情况:

1、调用 resolve() 方法,pending 变为 resolved
2、调用 reject() 方法,pending 变为 rejected

注意:一个Promise对象有且仅有一次修改机会,而且无论成功还是失败,只能传递一个结果

PromiseValue

保存Promise对象成功/失败的结果,只能有一个,不传默认是undefined

只有 resolve()reject() 这2个函数可以保存结果值,且只能保存一个

工作流程

相关API

Promise 构造函数

构造函数接收一个函数(执行器)作为参数,如下:

const p = new Promise(function(resolve, reject){
    // 同步执行
})

执行器会在Promise内部立刻同步调用,异步操作只要写在执行器内部就可以

then

Promise.prototype.then函数,接收2个回调函数作为参数,两个参数都是函数类型

p.then(value=>{},reason=>{})

返回的值是一个Promise对象

catch

Promise.prototype.catch函数,接收1个回调函数作为参数,是then方法的语法糖

p.catch(reason=>{})

等同于

p.then(null, reason=>{})

返回的值是一个Promise对象

finally

不管Promise状态是成功还是失败,都会执行的操作(如果是Pending状态就不会执行

Promise.prototype.finally函数,接收1个回调函数作为参数,回调函数不接受参数

必须写在最后,否则不会在最后执行

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

Promise.resolve

接收一个参数,返回一个成功/失败的 Promise 对象

1、当参数为非Promise对象时,返回的 Promise 对象的状态一定是成功的
2、当参数为Promise对象时,返回的 Promise 对象的状态由传入的 Promise 对象状态决定

非Promise对象的方法,而是Promise函数对象的方法

const p = Promise.resolve(123)
const p = Promise.resvlve(new Promise((resolve, reject) => {
    resolve(123)
}))

Promise.reject

Promise.resolve一样,是Promise函数对象的方法

收一个参数,无论参数是什么类型,总返回一个失败的 Promise 对象

const p = Promise.reject(123)
const p = Promise.reject(new Promise((resolve, reject) => {
    resolve(123)
})) // 返回失败状态的Promise对象

Promise.all

Promise.resolve一样,是Promise函数对象的方法

接收一个参数,参数是一个Promise数组,返回一个Promise对象,状态由参数中所有Promise对象决定,规则如下:

1、参数中所有Promise对象都是成功状态时,返回的Promise对象才是成功状态,而结果是一个包含所有Promise对象成功结果的数组
2、参数中所有Promise对象只要有一个是失败状态时,返回的Promise对象才是失败状态,而结果是按数组顺序第一个失败状态的Promise对象的失败结果

const p = Promise.resolve('123'),
       b = Promise.reject('我是第二错'),
       c = Promise.reject('我是第三错')

const res = Promise.all([p, c, b]) // res是失败状态的,结果是我是第三错

Promise.race

Promise.all一样,是Promise函数对象的方法

接收一个参数,参数是一个Promise数组,返回一个Promise对象,状态由参数中最先改变状态的Promise对象决定

const p = Promise.resolve(123),
       b = Promise.reject('我是第二错'),
       c = Promise.reject('我是第三错')

const res = Promise.race([p, c, b]) // res是成功状态的,结果是123

关键问题

1、修改Promise对象的状态有几种方式

有3种方式:

const p = new Promise((resolve, reject) => {
    // 第一种方式:pending -> fulfilled 成功
    resolve("ok");
    
    // 第二种方式:pending -> rejected 失败
    reject("error")
    
    // 第三种方式:pending -> rejected 失败
    throw '出错了';
})

2、同一个Promise对象指定多个成功/失败的回调函数时,都会调用吗?按什么顺序调用?

Promise对象状态发生改变时,根据状态,所有回调函数都会调用,而且按照代码上从上到下的顺序来执行回调函数

const p = Promise.resolve('123')

p.then(value => {
    alert('第一' + value)
})

p.then(value => {
    alert('第二' + value)
})

3、改变Promise状态与指定回调函数,执行上谁先谁后?

两种情况都有可能,一般是先指定回调函数,然后再改变状态,但也有相反的情况

先改变状态再指定回调,有2种情况会发生

  1. 在执行器中直接调用resolve()/reject()
  2. 延迟时间再调用then()
const p = new Promise(function(resolve, reject){
    resolve()
})

p.then(value => {}, reason => {})

先指定回调再改变状态,只有一种情况:执行器函数内执行了异步操作

const p = new Promise(function(resolve, reject){
    setTimeout(()=>{
         // 获取一个随机数
        const num = rend(1,100);
        
        if(num > 30){
            resolve(num);
        }else{
            reject(num);
        }
    }, 2000);
})

p.then((value) => {
    // Promise对象状态已变为成功
    alert("成功的值:"+value);
}, (reason) => {
    // Promise对象状态已变为失败
    alert("失败的值:"+reason);
})

那什么时候调用指定的回调函数呢?

  1. 如果是先指定回调再改变状态的话,当状态发生改变时,回调函数就会调用

  2. 如果是先改变状态的话,当指定回调时,就会马上调用回调函数

无论是哪种情况,执行效果都是一样

4、then/catch方法返回的Promise对象的状态由什么决定

then/catch方法指定的回调函数执行的结果决定

回调函数执行返回的结果有4种情况:

  1. 抛出异常then/catch返回的新Promise对象状态变为rejected,结果为抛出的异常
const p = new Promise(function(resolve, reject){
    resolve('ok')
})

const res = p.then((value) => {
    // 抛出异常
    throw '我错了'
}, (reason) => {
    console.log(123)
})

// res状态为失败,结果为‘我错了’
  1. 返回非Promise类型的值then/catch返回的新Promise对象状态变为resolve,结果为返回的值
const p = new Promise(function(resolve, reject){
    resolve('ok')
})

const res = p.then((value) => {
    // 返回非Promise类型的值
    return {
        name: 'gogogo',
        msg: value
    }
}, (reason) => {
    console.log(123)
})

// res状态为成功,结果为{ name: 'gogogo',msg: 'ok' }
  1. 返回Promise类型的对象,此 Promise 的结果就会成为新 Promise 的结果
const p = new Promise(function(resolve, reject){
    resolve('ok')
})

const res = p.then((value) => {
    // 返回Promise类型的值
    return Promise.reject('出错了')
}, (reason) => {
    console.log(123)
})

// res状态为失败,结果为‘出错了’
  1. 无返回

相当于返回一个undefinal值,按第二种情况来说,新Promise状态永远是成功,结果为undefinal

5、串联多个任务

开发经常遇到串联任务需求,例如:后一个任务的执行时机是前一个任务执行完并等到结果时才开始执行

由于then返回Promise对象,所以可以进行多任务串联操作

const p = new Promise(function(resolve, reject){
    $.ajax('url', {
        success(res){
            resolve(res)
        }
    })
})

p.then(value => {
    return new Promise((resolve, reject) => {
        $.ajax('url', {
            success(res1){
                resolve(res1)
            }
        })
    })
}).then(value => {})

6、异常穿透

原理:调用then方法没有传对应状态的回调函数时,那么then就会返回一个与调用thenPromise一模一样的新Promise对象

利用上面的原理,就可以实现异常穿透(then不能定义失败回调函数,否则catch就没意义了)

const p = new Promise(function(resolve, reject){
    reject('err')
})

p.then(value => {
    return Promise.resolve("ok");
}).then(value => {
    return Promise.resolve("ok1");
}).catch(reason => {
    // 错误处理 reason为'err'
});

除了异常/错误会传递之外,成功也一样会传递(调用then方式时,只定义失败回调函数),但一般很少这样使用

7、中断Promise链

只有利用Promise对象的Pending状态,才能中断Promise

const p = new Promise(function(resolve, reject){
    resolve('err')
})

p.then(value => {
    // 中断
    return new Promise(() => {})
}).then(value => {
    return Promise.resolve("ok1");
}).catch(reason => {
    // 错误处理 reason为'err'
}).finally(() => {
    // 最后处理
});

8、finally 方法返回的Promise对象的状态由什么决定

由调用finally方法的Promise对象决定,如果Promise对象是成功状态的,结果是123,那么finally()返回的就是新Promise对象就是成功状态的,结果是123,反之,失败状态也是一样

注意只有一种情况特殊,就是finally的回调函数执行时抛出错误,finally方法就会返回一个失败状态,结果为抛出的错误的Promise,代码如下

// p为失败状态,结果为‘err’
const p = Promise.resolve('我对了').finally(() => {
    throw 'err'
})

Async/await

目前编写 异步操作 最佳的解决方案,基于Promise,但解决Promise链式调用不够优雅可读性问题,异步操作可以用同步编码进行编写

Async 函数

是一个定义函数的关键字,类似 function ,而 async 函数返回值为 Promise 对象

Promise对象的状态和结果由async函数运行的返回值决定,有3种情况:

1、返回值是一个非Promise类型的数据

async函数返回一个成功状态的Promise对象,而结果是async函数返回的值

如果async函数没有返回值,其实默认是return undefinal;

async function test(){
    return 521;
}

const res = test(); // res是一个Promise对象,状态是成功,结果是521

2、返回值是一个Promise对象

async函数返回的Promise对象就是async函数体内returnPromise对象

async function test(){
    return new Promise((resolve, reject) => {
        reject('Error');
    });
}

const res = test(); // res是一个Promise对象,状态是失败,结果是'Error'

3、抛出异常

返回的Promise对象,状态是失败,结果是抛出的数据

async function test(){
    throw '出错了'
}

const res = test(); // res是一个Promise对象,状态是失败,结果是'出错了'

await 表达式

  • await右侧的表达式一般为promise对象,但也可以是其他值(例如:1+1、'abc'等,但很少这样用)
  • 如果表达式是Promise对象,await返回的是Promise对象成功状态的结果
  • 如果表达式是其他值,直接将此值作为await的返回值

注意:
1、await必须写在async函数中,但async函数中可以没有await
2、如果awaitpromise对象失败了,就会抛出异常,需要通过try...catch...捕获,必须捕获,否则浏览器会报错 3、如果awaitpromise对象失败了,就会终止后面的代码执行,直接报错

async function test(){
    const p = new Promise((resolve, reject) => {
        reject('我错了')
    })
    
    try{
        const res = await p;
    }catch(e){
        console.log(e);
    }
}

优雅地解决try/catch问题

请参考:async/await 优雅的错误处理方法