Promise介绍与微任务案例

703 阅读12分钟

简介

Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大,且其链式调用的方式,避免了回调地狱,更是广受大家喜爱

Promise是一个对象,更是一个容器,里面保存着未来才会发生的事件(异步事件),其生成的任务又被称为微任务,其有两个特点:

  1. 其里面保存着三个状态(请求过接口的一般都会了解到):进行中 pending(即请求中)、已成功 fulfilled已失败 rejected,即:默认创建完 Promise 后,状态为pendingresolve 之后为 fulfilledreject之后为 rejected
  2. 此外,后续发生改变后(fulfilled、rejected)状态不会再被更改(这是需要注意的地方,即resolve后,变成fulfilled后,再执行reject也不会再变成rejected了)

ps:Promise 一旦创建创建后会立即执行,且无法取消,内部出现的错误,不会反馈到外面,但可以通过 .catch 捕获到

ps2:Promise中如果提前 resolve(reject),后面还有代码,resolve(reject) 仅仅是将 Promise 标记为 fulfilled(rejected),并不会立即执行后续的.then,而是继续当前代码块往后执行

基本使用

//创建一个promise
function testPromise() {
    return new Promise((resolve, reject) => {
        if (条件) {
            resolve('正常运行')
        }else {
            reject('出现错误了')
        }
    })
}

//使用promise,获取 promise 会立即执行 promise,随后,其状态从 pending 变为其他两个中的一个
//且.catch里面除了能获取到 reject后的结果,还能捕获前面出现的错误(promise、then中的错误)
testPromise().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

.then

此外一些人可能也会看到下面写法,也是没错的,then 函数中,一共有两个参数: resolve 回调、reject 回调,只不过我们会省略

//案例一
testPromise().then(res => {
    console.log(res)
}, err => {
    console.log(123, err)
})

当实现 then 中的 reject 回调后,后面的 .catch 便不会再走了,其为一个不正确的使用方法

//此时.catch不会走了,使用不当
testPromise().then(res => {
    console.log(res)
}, err => {
    console.log(123, err)
}).catch(err => {
    console.log(err)
})

.then的链式使用

其主要是为了解决多个需要顺序执行的 promise,可以避免其陷入回调地狱,让代码看起来更加优雅,看起来更加通俗易懂

下面给出链式调用的案例,只需要在 .then 里面返回下一个要执行的 promise,便可以在下一个 .then 里面获取到我们返回的 promise 的结果了

ps.then 里面返回的内容可以是 promise,也可以是一个非 promise 对象,其均会加工成一个新的 promise 返回,用于后续调用,没有返回值就是返回 undefined

function promise1() {
    return new Promise((resolve, reject) => {
        if (new Date().getTime() % 10 > 5) {
            resolve('1正常运行')
        }else {
            reject('1出现错误了')
        }
    })
}

function promise2() {
    return new Promise((resolve, reject) => {
        if (new Date().getTime() % 10 < 5) {
            resolve('2正常运行')
        }else {
            reject('2出现错误了')
        }
    })
}

function testPromiseNext() {
    //可以链式处理promise回调问题,且合并错误,避免陷入回调地狱
    //缺点是需要单独处理的错误则不太好用,需要随机应变
    promise1().then(res => {
        //promise1执行完了,开始执行promise2
        return promise2()
    }).then(res => {
        //promise2也执行完毕了
        console.log('res:', res)
    }).catch(err => {
        //promise1和promise2有错误都会到这里来
        console.log('catch:', err)
    })
}

promise 执行的时候会生成微任务来保证内部任务有序进行,后续会通过大家讨论比较多的案例,从而间接了解解其任务怎么产生和执行的,会以什么顺序执行下去

.catch

.catch 是用来代替 .then(null, rejection).then(undefined, rejection)

此外,其不但能捕获 reject 结果,还能捕获里面发生的其他异常,也是最推荐的写法

testPromise().then(res => {
    console.log(res)
    throw new Error('我是.then里面报的错误')
}).catch(err => {
    //我能捕获到前面所有promise和.then里面的错误
    //当然如果是testPromise前面还有一个非promise的调用,其错误就捕获不到了
    console.log(err)
})

.finally

.finall 可以解决在 promise 执行完毕后,继续做后续的事情,避免再封装一层了(例如:一次期末考试,无论结果及格不及格,有没有参加考试,我都要记录到本本上😂)

testPromise().then(res => {
    //成功回调
}).catch(err => {
    //失败回调
}).finally(err => {
    console.log('不管成功失败都到我这里')
})

Promise.all 、Promise.allSettled、Promise.race、Promise.any

先准备两个基础 promise 案例

function promise11() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(10)
        }, 500);
    })
}

function promise12() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(11)
        }, 1000);
    })
}

Promise.all 看名字就知道,为一组 promise 全部成功后的回调,成功后回调为结果组成的数组,一旦有一个失败,则直接走 .catch,标识着失败

Promise.all([promise11(), promise12()]).then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

Promise.allSettled 有都执行完毕的意思,与Promise.all 类似,只不过其不关注执行的结果成败,只关注是否执行完毕,即:所有的 promise 状态都 不为 pending 时,返回执行后的 promise 数组(即不管成功失败,我只要知道你们都执行完了,都继续下一步),此时甚至没必要使用 .catch 了

Promise.allSettled([promise1(), promise2()]).then(res => {
    console.log('allSettled', res)
})

Promise.race 名字中带有赛跑的意思,意味着只记录一组 promise 中最快的那个,无论成败,并且将最快 promise 结果返回,其成功走 .then,失败走 .catch

Promise.race([promise11(), promise12()]).then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

Promise.any 内部出现任何一个成功(fulfilled)的 promise,则其状态调整为成功(fullilled),返回其成功的结果;如果内部某个先完成,但为失败(rejected)不更新其状态,即不返回结果,直到所有的 promise 均失败(rejected),此 promise 才会标记为失败(rejected)

简而言之:其内部所有的 promise 失败才会走失败,有一个成功则直接返回结果

Promise.any([promise1(), promise2()]).then(res => {
    console.log('any', res)
}).catch(err => {
    console.log('anyerr', err)
})

Promise.resolve

快速生成一个 Promise 对象,状态为 fulfilled,为一个工厂构建方法,使用简单

Promise.resolve('成功了')
// 相当于
new Promise(resolve => resolve('成功了'))

其可以传递从参数,也可以不传递参数

1.回调函数不传递参数,一般用不到回调参数的时候使用,仅仅用来走成功回调

2.传递普通对象(例如字符串),则包装成一个 promise 返回,因此支持在 .then 中 获取传递的内容

3.传递 promise 对象,原封不对返回

4.传递 thenable 对象(带有 then 方法),直接执行 then 方法,并返回一个新的 promise 对象

Promise.reject

与 resolve 类似,快速生成一个 Promise 对象,状态为 rejected,为一个工厂构建方法,使用简单

Promise.reject('失败了')
// 相当于
new Promise(reject => reject('失败了'))

Promise.try

这个平时用的比较少,一旦使用,会感觉到在一些场景,用的非常舒服,因此了解是必要的,其参数为函数

场景1

需要执行一个 外部方法,但不知道是同步还是异步(例如:带有async,但不一定有 await),想用 promise 来处理他的结果,但最好是保持原来的执行方式,即: 同步方法立即执行,异步方法异步执行,其可以这么写

const f = () => console.log('我是一个未知函数')

//使用匿名函数
(async () => f())().then(res => {
    ...
}).catch(err => {
    ...
})

但看起来不优雅,我们使用 Promise.try 改进一下,看一下是不是很简洁易懂了

const f = () => console.log('我是一个未知函数')

//使用 try 调用函数,传入函数
Promise.try(f).then(res => {
    ...
}).catch(err => {
    ...
})

场景2

有下面一个方法,通过调用一个方法获取一个我们需要的 promise,但获取 promise 之前,有概率也会出现其他错误,需要统一处理

//我们从本地数据库中获取一个用户id,结果返回一个 promise
Database.share.user.get('user_id')
    .then(...)
    .catch(...)

上面案例,我们需要注意的是 我们打开数据库的过程可能会失败,但这个错误由于不是 promise,却不会被后面的 catch 捕获,会导致错误无法被捕获

我们使用 try...catch 改进后变成这样,但不够优雅,且 .catch 还要写两套代码

try {
    Database.share.user.get('user_id')
        .then(...)
        .catch(...)
}.catch(err) {
    ...
}

显然不够优雅,再使用 Promise.try 改进一下,这样就可以统一捕获了

//由于这段代表并不是函数,我们快速生成一个匿名函数即可
Promise.try(() => Database.share.user.get('user_id'))
    .then(...)
    .catch(...)

promise 微任务案例

之前看到过一个 promise 的微任务案例,讨论的很热烈,需要了解的 点击这里,讲的是 Promise.then 中 返回的 Promise 的 疑问与探讨,题目如下所示,可以结合前面讲的,猜一下答案

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4)
}).then(res => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
})


//打印结果为 
0 1 2 3 4 5 6

可能有些人会疑惑,下面就以自己的理解简述述这个过程把,如果感觉有疑问可以点进去上面链接,查看其他人更加详细的介绍

这个问题难就难在大家不知道 .then 中返回新的 Promise 的任务时,会额外新增一个 resolve 和 then 任务

执行过程

ps:说明前,先介绍一下队列,为一个先进先出的数据结构,入队为队尾增加任务,出队为队首减少任务

promise任务变化情况

1、首先执行第一个任务上面一串代码 Promise.resove,其默认返回为 Promise{undefined},继续调用 then 方法,发现Promise{undefined} fulfilled状态,直接入队 console.log(0);return Promise.resolve(4)任务,返回新的promise0(log0里面的 取名 promise0)然后继续调用后面的 then方法,发现为 promise0pending,因此 then不能执行,将其任务追加到 promise0任务所在任务尾部,可以将该任务看做一个链表队列,需要一个个执行,其整体算是生成一个新的任务(第一个resovle代码执行完毕舍弃),然后入队到任务队列中,等待后续执行

2、由于当前代码块任务中没有执行完毕,继续往下执行,到下面的一串代码 Promise.resove,其与上面一样,不多讲,然后走到then,promise1被入队,然后走下一个then,发现promise1pending ,然后后续任务依次入队,promise1promise2promise3promise5promise6,此时他们也形成一个新的任务被打包到一起(保持原有顺序),然后入队到任务队列中,等待后续执行

promise任务执行过程以及顺序

3、第一个任务执行完毕,然后开始执行队列队首任务promise0,随后 console.log(0) 执行,然后promise0 执行 resolve,检查发现,resolve 的值 promise4thenable 类型数据(包含then方法),根据标准,不管该 thenable数据是否执行完毕,都要入队一个包含带返回值和.then的任务,即入队新任务 promise4.then(promise0后续任务也被打包到其尾部,除非执行到其任务才会被拿出),执行完出队promise0,此时任务队列为 Promise1、promise4.then

4、执行 promise1,执行console.log(1),然后 resolve 返回 undefined,没什么异常,出队 promise1,该promise1已经执行完毕,取出打包到后面的promise2入队,此时队列为 promise4.then、promise2

5、执行 promise4.then,执行其 resolve 后,其后面的 .then 任务此时已经被打包插入到自己后面promise0-resolve入队,同时出队 promise4.then,此时队列为 promise2、promise0-resolve

6、执行 promise2,执行console.log(2),随后入队 promise3,出队 promise2,此时队列为 promise0-resolve、promise3

7、执行 promise0-resolve,此时外部无法感知,其执行完毕,后面的 promiseres入队,promise0-resolve出队,队列此时为 promise3、promiseres

8、执行 promise3,执行console.log(3),随后入队 promise5,出队 promise3,此时队列为 promiseres、promise5

9、执行 promiseres,执行console.log(res),打印4,出队 promise3,此时队列为 promise5

10、执行 promise5,执行console.log(5),出队 promise6,此时队列为 promise6

11、执行 promise6,执行console.log(5)

因此打印结果为 0、1、2、3、4、5、6

知识难点总结

new Promise((resolve, reject) => {}) 中 resolve、reject回调仅仅是标记 Promise 的状态,并不会影响下一个任务执行的时机(例如:后面的.then)

下面说一下上面案例的难点所在:

  • Promise 的 .then 中如果返回的是一个正常的文本,则不会生成新任务,会继续沿着原有轨迹将后面的 .then 加入微任务队列中
  • 如果 Promise 的 .then 中返回的是一个 Promise(或者 thenable类型的内容),那么有一个入队操作,新入队一个 job,(resolve, .then),没错是一个任务,但是任务尾部还跟着一个 .then 内容,随着resolve 执行后续的 .then 才会入队,因此相当于分两次执行,每次执行入队一个任务,因此会多两次执行时机
  • 如果说 return 前面还有 Promise.then 呢,那个 .then 也会被注册入队,等待当前任务执行完毕后执行

补充知识(第一版不存在,后续学习了解后,继续补充的):

  • promise.then入队 也分为先注册后执行,先执行后注册
  • 当 Promise 执行代码块时,在里面直接resolve(reject),则promise是完成状态,后面注册 .then,到该 .then 执行的时候发现不是 pending 则执行该任务,这就是先执行后注册
  • 当 Promise 执行代码块时,在里面可能会有异步延迟任务,执行完代码块后,promise仍然是 pending状态,后面注册 .then注册到任务队列,到该.then 执行的时候发现不是 pending 则执行该任务,继续执行其他微任务队列中的任务,等待宏任务队列任务执行时,并发现有注册好的任务,然后执行注册好的任务,这就是先注册后执行

ps:实际任务不分类型,但是消息队列分为微队列、宏队列,浏览器会优先从微队列获取任务执行

最后

promise 也是一个易用难懂的一个数据结构,且想理解一些里面稍微繁琐一点的逻辑,也需要一定的数据结构与算法功底,因此打好基础也是很重要的

然后祝大家有所收获!