Promise之介绍与基本使用

一、Promise 产生的原因

Promise未诞生以前,我们通过回调表达程序异步和管理并发,当然现在一些老的项目为了保证兼容性仍在使用。回调是JavaScript中实现异步最简单的方式,你可以将回调理解为程序的延续,即在当前同步代码执行完毕以后才会在未来某个时间执行。当我们的回调需要用到上一个回调的结果时,就产生了嵌套,类似下面这样:

ajax1(null, function (value1) {
    ajax2(value1, function(value2) {
        ajax3(value2, function(value3) {
            ajax4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

嵌套的层级多了,就产生了所谓的回调地狱。但实际情况可能远比上面展现的要复杂得多,这使得追踪代码执行顺序的难度成倍增加。你也一定遇到过结尾少写}),排查大半天的情况,这就是因为代码太复杂了。

二、Promise的介绍

Promise 是一个 ECMAScript 6 提供的类,是JS中进行异步编程的一种解决方案。 使用它能更加优雅地书写复杂的异步任务。

1. Promise 与 promise 是什么?

⏰ 官方解释:

Promise是一个构造函数,promise对象用来表示一个异步操作的最终完成(或失败)及其结果值,它能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来,使得异步方法可以像同步方法那样有返回值,异步方法不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

⏰ 通俗解释:

Promise在编程中就像一个贴心的快递服务,只不过它送的不是包裹,而是异步操作的结果。想象一下,你在网上订购了一份礼物,你不会立刻收到它,而是会收到一个订单号(这就好比是promise对象)。这个订单号代表着你将来的礼物,它承诺在礼物准备好时,无论成功送达还是遇到什么问题,都会通知你。

⏰ Promise初体验:

  • 常规的定时器异步操作

    <body>
        <h1>Promise 初体验</h1>
        <hr>
        <button id="btn">点击抽奖</button>
    
        <script>
            //1. 生成随机数
            function rand(m, n) {
                return (Math.ceil(Math.random() * (n - m + 1)) + (m - 1))
            }
            //2. 示例需求
            //点击按钮, 2s 后显示是否中奖(30%概率中奖)
            //若中奖:弹出“恭喜中奖,奖品为 10万RMB 劳斯莱斯优惠券”
            //未中奖:弹出“再接再厉!”
            //获取元素对象
            const btn = document.querySelector('#btn')
            //绑定单机事件
            btn.addEventListener('click', function () {
                //定时器模拟延迟操作
                setTimeout(() => {
                    //获取1--100随机数
                    let n = rand(1, 100)
                    //判断是否中奖
                    if (n < 30) {
                        alert("恭喜中奖,奖品为 10万RMB 劳斯莱斯优惠券")
                    } else {
                        alert('再接再厉!')
                    }
                }, 2000)
            })
        </script>
    </body>
  • 使用Promise优化的定时器异步操作

    <body>
        <h1>Promise 初体验</h1>
        <hr>
        <button id="btn">点击抽奖</button>
    
        <script>
            //1. 生成随机数
            function rand(m, n) {
                return (Math.ceil(Math.random() * (n - m + 1)) + (m - 1))
            }
            //2. 示例需求
            //获取元素对象
            const btn = document.querySelector('#btn')
            //绑定单机事件
            btn.addEventListener('click', function () {
                // 1. Promise 形式实现
                // resolve 解决  函数式数据
                // reject  拒绝  函数式数据
    
                // Promise 实例化能够包裹一个异步操作
                let promise = new Promise((resolve, reject) => {
                    //定时器模拟延迟操作
                    setTimeout(() => {
                        //获取1--100随机数
                        let n = rand(1, 100)
                        //判断是否中奖
                        if (n < 30) {
                            //将promise 对象状态设置为【成功】
                            resolve("恭喜中奖,奖品为 10万RMB 劳斯莱斯优惠券")
                        } else {
                            //将promise 对象状态设置为【失败】
                            reject('再接再厉!')
                        }
                    }, 2000)
                })
    
                //调用then方法
                //第一个参数:promise状态为【成功】时的回调
                //第二个参数:promise状态为【失败】时回调
                promise.then((value) => {
                    alert(value) //"恭喜中奖,奖品为 10万RMB 劳斯莱斯优惠券")
                }, (reason) => {
                    alert(reason) //再接再厉!
                })
            })
        </script>
    </body>

2. promise 对象属性

打印一下promise 对象信息,发现以下结构信息

⏰ promise 的状态属性 [PromiseState] 

[PromiseState] 是实例对象中的一个属性  ,拥有 pending(等待态),fullfilled(成功态),rejected(失败态) 三种状态。

⏰ promise 对象值属性 [PromiseResult]

 [PromiseResult] 是实例对象中另外一个属性,保存着对象 fullfilled / rejected 状态下的结果。仅resoluve,reject 函数方法操作时,能够修改这个值。

3. promise 的状态改变

一个Promise必然会处于以下三个状态之一:

  • pending(等待态): 初始状态,既没有被兑现,也没有被拒绝
  • fullfilled(成功态): 意味着操作成功完成;
  • rejected(失败态): 意味着操作失败;

其中这三种状态之间有两条关系线。只要达到其中一个终极状态,就不能改变了。

  • Resolved: pending --> fulfiled
  • Rejected: pending --> rejected

如下图:

我们通过代码解释一下这三种状态在代码中的体现

⏰ 1. 通过new创建一个promise对象,我们需要传入一个回调函数

这个回调函数我们称之为executorresolverejectexecutor接收的两个回调函数
let promise = new Promise((resolve, reject) => {
    console.log('异步任务');
})
console.log(promise)

从这段代码我们可以看到executor函数体只打印了一句话,我们执行这段代码,结果如下,可以看到executor里面的代码被立即执行,同时 promise 处于初始状态pending 

⏰ 2. 接着我们在executor里面调用resolve回调函数

let promise = new Promise((resolve, reject) => {
    console.log('异步任务');
    // 调用resolve后 状态修改为【成功】, then传入的第一个回调函数会执行
    resolve()
})
console.log(promise)

在控制台查看打印结果如下吗,可以看到,此时promise的状态从pending变成fullfilled

⏰ 3. 接着我们注释掉resolve,改为调用reject回调函数

let promise = new Promise((resolve, reject) => {
    console.log('异步任务');
    // 调用reject后 状态修改为【失败】,then传入的第二个回调函数会执行
    reject()
})
console.log(promise)

在控制台查看打印结果如下吗,可以看到,此时p1的状态从pending变成rejected

🔉 通过以上代码演示,可以知道当我们的executor没有调用任何回调函数之前,promise会处于pending状态,调用resolve之后状态变为fullfilled,即异步操作成功,调用reject之后状态变为rejected,也就是异步操作失败

🤔️ 那如果我同时调用resolverejected呢?

let promise = new Promise((resolve, reject) => {
    console.log('异步任务');
    resolve()
    reject()
})
console.log(promise)

当我们在executor同时调用两个回调函数时,不管哪个先调用,都只有一个会生效

  • 先调用resolve()

  • 先调用reject()

从这个示例也可以得出一个结论,promise的状态只能从 pending => fullfilled 或者从 pending => rejected

⏰ 4. 总结

Executor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数,通常我们会在Executor中确定我们的Promise状态,通过resolve,可以兑现(fulfilledPromise的状态,我们也可以称之为已决议,通过reject,可以拒绝(rejectPromise的状态。

一旦状态被确定下来,Promise的状态会被锁死,该Promise的状态是不可更改的,在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise(resolve传参的区别后续会讲到),那么会将该Promise的状态变成 兑现(fulfilled),在之后我们去调用reject时,已经不会有任何的响应了

三、promise的使用

执行resolve或reject之后发生了什么

根据上图Promise工作流程我们可以知道:

当我们调用resolve回调函数时,Promise对象的状态会改成fulfilled, 然后执行Promise对象的then方法传入的第一个回调函数;
当我们调用reject回调函数时,Promise对象的状态会改成reject, 然后执行promise对象的then方法传入的第二个回调函数 或 catch方法传入的回调函数。

🔊:then方法和catch方法是Promise原型上的一个方法:then方法会返回一个Promise,最多可以接收两个参数,分别是Promise成功和失败情况的回调函数;catch方法也返回一个Promise,当我们的Promise被拒绝时(reject)调用catch里的回调函数。

下面通过代码演示一下then方法和catch方法,注释掉reject(),打印的是‘失败的回调’,注释掉resolve(),打印的是‘成功的回调

// Promise 实例化能够包裹一个异步操作
let promise = new Promise((resolve, reject) => {
    console.log('异步任务');
    //resolve()
    reject()
})
//【调用then方法】
//第一个参数:promise状态为【成功】时的回调
//第二个参数:promise状态为【失败】时回调
promise.then(() => {
    console.log('成功的回调')
}, () => {
    console.log('失败的回调')
})

以上代码等价于

// 【Promise 实例化能够包裹一个异步操作】
let promise = new Promise((resolve, reject) => {
    console.log('异步任务');
    //resolve()
    reject()
})
//【调用then方法】:
//第一个参数:promise状态为【成功】时的回调。
//第二个参数:我们不传。
//⚠️:如果我们传了then函数中的第二个参数,catch回调函数就不会再调用了; 如果我们不传then函数的第二个参数,catch回调函数才会被调用。

//【调用catch方法】:
promise.then(() => {
    console.log('成功的回调')
}).catch(() => {
    console.log('失败的回调')
})

resolve回调函数参数的处理

情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数

  • 普通的值

    // Promise 实例化能够包裹一个异步操作
    let promise = new Promise((resolve, reject) => {
        console.log('异步任务');
        resolve('情况一:普通值')
    })
    
    //调用then方法:
    promise.then((res) => {
        console.log('res') //'情况一:普通值'
    })
  • 对象

    // Promise 实例化能够包裹一个异步操作
    let promise = new Promise((resolve, reject) => {
        console.log('异步任务');
        resolve({ obj: '情况一:普通值或对象' })
    })
    
    //调用then方法:
    promise.then((res) => {
        console.log(res) //{obj: '情况一:普通值或对象'}
    })

情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态,并且新Promiseresolvereject的参数也会传给原来的promise

//1.Promise对象一
const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('我是新的promise')
    }, 1000)
})

//2.Promise对象二
const p1 = new Promise((resolve, reject) => {
    resolve(p) // 传入p
})

p1.then((res) => {
    console.log(res) // 我是新的promise
})

在这段代码中,p是 p1 resolve中传入的Promise,因此,p1的状态是由p的状态决定的,由于p执行了resolve回调函数,因此状态是fullfilled,所以p1的状态也是fullfilled,运行之后控制台在1s后打印出“我是新的promise”

//1.Promise 对象一
const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('我是新的promise并且拒绝')
    }, 1000)
})
//2.Promise 对象二
const p1 = new Promise((resolve, reject) => {
    resolve(p)
})

p1.then((res) => {
    console.log("res:" + res)
}).catch((err) => {
    console.log("err:" + err) //err:我是新的promise并且拒绝
})

修改p回调的函数为reject之后,控制台打印“err:我是新的promise并且拒绝”,进一步验证以上结论

情况三:resolve传入的是一个对象,并且该对象有实现then方法,那么会执行这个then方法,并且根据then方法的结果来决定Promise的状态

//1.对象一
const p = {
    name: 'javascript',
    then: function (resolve) {
        resolve("情况三")
    }
}
//2.对象二
const p1 = new Promise((resolve, reject) => {
    resolve(p)
})

p1.then((res) => {
    console.log("res:" + res)
}).catch((err) => {
    console.log("err:" + err) //res:情况三
})

以上代码在控制台输出“res:情况三”

Promise的.then和.catch方法的调度

我们前面对then和catch方法的调用都只有一次,假如我们调用了多次呢🤔

⏰ .then

const p1 = new Promise((resolve, reject) => {
    resolve("成功的回调")
})

p1.then((res) => {
    console.log("res1:" + res)
})

p1.then((res) => {
    console.log("res2:" + res)
})

p1.then((res) => {
    console.log("res3:" + res)
})
【打印结果】
res1:成功的回调
res2:成功的回调
res3:成功的回调

打开控制台会发现每个then里面的回调都被执行了,说明then是可以被多次调用的,每次调用我们都可以传入对应的fulfilled回调,当Promise的状态变成fulfilled的时候,这些回调函数都会被执行。

⏰ .catch 

const promise = new Promise((resolve, reject) => {
    reject("failure")
})

promise.then(res => {
    console.log("成功的回调:", res)
}).catch(err => {
    console.log("失败的回调1:", err)
})

promise.catch(err => {
    console.log("失败的回调2:", err)
})

promise.catch(err => {
    console.log("失败的回调3:", err)
})

promise.catch(err => {
    console.log("失败的回调4:", err)
})

promise.catch(err => {
    console.log("失败的回调5:", err)
})
【打印结果】
失败的回调2: failure
失败的回调3: failure
失败的回调4: failure
失败的回调5: failure
失败的回调1: failure

那么catch是否也会出现相同的结果呢,当我们在执行上面这段代码的时候,控制台会打印所有catch回调里面的内容,因此catch也是可以被多次调用的,每次调用我们都可以传入对应的reject回调;当Promise的状态变成reject的时候,这些回调函数都会被执行;

then和catch方法的返回值

1. then

then方法本身是有返回值的,他的返回值是一个Promise,所以我们可以对其进行链式调用

const p1 = new Promise((resolve, reject) => {
    resolve("成功的回调")
})

p1.then((res) => {
    console.log("res:" + res)
    return res + 'abc'
}).then((res1) => {
    console.log("res1:" + res1)
})
【打印结果】
res:成功的回调
res1:成功的回调abc

那么.then方法返回的这个Promise的状态是由什么决定的呢,事实上,这个新Promise的决议是等到then方法传入的回调函数有返回值时, 才会进行决议。

🔊: 当then方法中的回调函数在执行的时候,返回的promise处于pending状态,当返回一个结果时,会处于fullfilled状态,并且将结果作为resolve的参数,因此链式调用的then方法里的回调函数的参数是上一个then方法的返回值

下面我们来谈谈then方法返回值的几种情况吧😁

⏰ 情况一:返回一个普通的值a。将这个值a作为resolve的参数,因此在后面的.then方法里的回调函数获取到的参数就是a

const p1 = new Promise((resolve, reject) => {
    resolve("成功的回调")
})
p1.then((res) => {
    return 'aaaaa'
}).then((res1) => {
    console.log("res1:" + res1) //res1:aaaaa
    return 'bbbbb'
}).then((res2) => {
    console.log("res2:" + res2) //res2:bbbbb
})

⏰ 情况二:返回一个Promise。如果返回了一个PromiseA,那么then返回的PromiseB的状态会由PromiseA的状态决定,并且将PromiseA的状态的回调函数的参数作为PromiseB的状态的回调函数的参数

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('ccccc')
    }, 1000)
})

const p1 = new Promise((resolve, reject) => {
    resolve("成功的回调")
})

p1.then((res) => {
    return p
}).then((res1) => {
    console.log("res1:" + res1) //res1:ccccc
    return 'ddddd'
}).then((res2) => {
    console.log("res2:" + res2) //res2:ddddd
})

⏰ 情况三:返回一个thenable对象,如果then方法里面的回调函数返回了一个带有then方法的对象,那么then方法返回的PromiseAd的状态是由then方法里的结果决定的

const promise = new Promise((resolve, reject) => {
    resolve("aaaaaaa")
    // reject()
})

promise.then(res => {
    return {
        then: function (resolve, reject) {
            resolve("thenable")
        }
    }
}).then(res => {
    console.log("thenable test:", res) // thenable test: thenable
})

2. catch

catch也会返回一个Promise,因此也是支持链式调用的,且catch后面可以调用then或者catch方法,我们先来看下面一段代码

const p1 = new Promise((resolve, reject) => {
    reject("失败的回调")
})

p1.catch((res) => {
    console.log('catch里面的回调函数') //catch里面的回调函数
}).then((res1) => {
    console.log("res1:" + res1) //res1:undefined
}).catch((res2) => {
    console.log("res2:" + res2)
})

事实上,在p1.catch执行完后是会执行.then后面的回调而不是.catch后面的回调,这是因为为catch传入的回调在执行完后,默认状态依然会是fulfilled的;

如果我们想让.catch后面继续执行catch该怎么做呢❓答案是我们需要抛出一个异常,如下面代码所示

const p1 = new Promise((resolve, reject) => {
    reject("失败的回调")
})
p1.catch((res) => {
    console.log('catch里面的回调函数') //catch里面的回调函数
    throw new Error('error message')
}).then((res1) => {
    console.log("res1:" + res1)
}).catch((res2) => {
    console.log("res2:" + res2) //res2:Error: error message
})

控制台会输出

catch里面的回调函数
res2:Error: error message

综上:链式调用中的.catch方法的执行时机,是由上一个promise是否抛出异常决定的,如果上一个Promise照常返回一个值,执行的是链式调用中的then方法

Promise的finally

finally是ES9中新增的一个特性,无论promise变成fullfilled状态还是rejected状态,都会执行finally里面的回调,而且finally不接收任何参数,也用代码演示一遍

let p1 = new Promise((resolve, reject) => {
    resolve('abc')
})

p1.then((res) => {
    console.log(res) //abc
}).catch((err) => {
    console.log(err)
}).finally(() => {
    console.log('finally') //finally
})

控制台会输出

abc
finally

四、promise的实践

1. fs读取文件

⏰ 常规操作

//1.获取文件操作模块fs对象
const fs = require('fs');

//2.Promise 实例化能够包裹一个异步操作
let promise = new Promise((resolve,reject)=>{
    fs.readFile('../../resoure/content.text',(err,data)=>{
        //1.如果出错,则抛出错误
        if(err){
            reject(err)
        } 
        //2.如果正确,则输出内容
        else {
            resolve(data.toString())
        }
    })
})

//3.调用then,catch方法
promise.then((ref)=>{
    console.log(ref)
}).catch((reason)=>{
    console.log(reason)
})

⏰ 封装操作

/**
 * 封装一个函数 mineReadFile 读取文件内容
 * 参数: path  文件路径
 * 返回: promise 对象  
 */
function mineReadFile(path) {
    return new Promise((resolve, reject) => {
        require('fs').readFile(path, (err, data) => {
            //1.如果出错,则抛出错误
            if (err) reject(err)
            //2.如果正确,则输出内容
            else resolve(data.toString())
        })
    })
}

//使用
mineReadFile('../../resoure/content.text').then((ref) => {
    console.log(ref)
}, (reason) => {
    console.log(reason)
})

2. AJAX请求

⏰ 常规操作

//Promise 实例化能够包裹一个异步操作
const promise = new Promise((resolve, reject) => {
    //1.创建请求对象
    const xhr = new XMLHttpRequest()
    //2.初始化
    xhr.open('get', 'http://iwenwiki.com/api/blueberrypai/getChengpinInfo.php')
    //3.发送
    xhr.send()
    //4.处理响应结果
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response)
            } else {

                reject(xhr.status)
            }
        }
    }
})

promise.then((ref) => {
    //控制台输出响应体
    console.log(xhr.response)
}).catch((reason) => {
    //控制台输出响应状态码
    console.log(xhr.status);
})

⏰ 封装操作

/**
 * 封装一个函数 sendAJAX 发送 GET AJAX请求
 * 参数: URL 网址
 * 返回: promise 对象  
 */
function sendAJAX(url) {
    return new Promise((resolve, reject) => {
        //1.创建请求对象
        const xhr = new XMLHttpRequest()
        //2.初始化
        xhr.open('get', url)
        //3.发送
        xhr.send()
        //4.处理响应结果
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(xhr.response)
                } else {
                    reject(xhr.status)
                }
            }
        }
    })
}

//使用
sendAJAX('http://iwenwiki.com/api/blueberrypai/getChengpinInfo.php').then((ref) => {
    console.log(ref)
}).catch((reason) => {
    console.log(reason)
})

 

posted on 2024-07-13 18:21  梁飞宇  阅读(29)  评论(0)    收藏  举报