前后端交互系列之promise详解

前言

我们在前端学习中遇见果大量的回调函数,很多回调函数聚在一起会使得逻辑混乱以及代码可读性差。于是有了promise这一门技术。

promise在面试中也是非常重要的一个知识点。本篇文章涵盖了promise日常使用中的基本操作。希望可以给读者带来帮助。另外如果需要提升的话,可以参考文章深入理解Promise之一步步教你手写Promise构造函数

备注:本节博客需要在了解Ajax基础的前提下食用!可参考前端后端交互系列之原生Ajax的使用来复习相关内容。

一,Promise概述

1.1 理解Promise

Promise是什么:Promise是一门符合ES6规范的新技术。是JS中进行异步编程的新解决方案。

从语法及功能上说:Promise是一个构造函数,Promise对象用来封装一个异步操作并可以获取其异步操作成功或者失败的返回值。

其中,异步操作包括:fs文件操作(nodejs模块),数据库操作,Ajax网络请求,定时器。

1.2 Promise的优势

面试找工作要回答出来:

支持链式调用,可以解决回调地狱问题。比传统的回调函数更加灵活。

什么是回调地狱:一个回调函数套着另一个回调函数,无线套娃,不便于阅读,也不便于异常处理,见如下代码:

        setTimeout(function () {
    
      //第一层
            console.log('111');
            setTimeout(function () {
    
      //第二程
                console.log('222');
                setTimeout(function () {
    
       //第三层
                    console.log('333');
                }, 1000)
            }, 2000)
        }, 3000)

这就是一个很典型的回调地狱,一层接着一层套娃。它的出现是为了解决代码的顺序性问题,比如这段代码运行出来是111222333,之所以是这个顺序是因为定时器的缘故。这样做顺序可以保障,但是代码的可读性实在太差,不建议使用。

1.3 promise的初体验

我们看一个案例,需求如下:
页面上有一个按钮,1s后查看是否中奖(30%的概率),若中将弹出中奖信息,未中则弹出”再接再厉“。

思路:利用1-100随机数,如果是三十以内则中奖,否则不中。

原生js写法:

    <!-- 需求:点击按钮,2s后显示是否抽奖(百分之三十的概率中奖) -->
    <button class="btn">点击查看是否中奖</button>
    <script>

        //求随机数
        function rand(m, n) {
    
    
            return Math.ceil(Math.random() * (n-m+1)) + m - 1;
        }
        var btn = document.querySelector('.btn');
        btn.addEventListener('click', () => {
    
    
            //30%中奖概率 1-100
            //获取从1-100的一个随机数
            //定时器
            setTimeout(() => {
    
    
                let n = rand(1, 100)
                if(n <= 30) {
    
    
                    alert('中奖了');
                } else {
    
    
                    alert('再接再厉')
                }
            }, 1000)
        })
    </script>

现在我们要用promise来做这个案例,按照以下步骤来,首先,写好求随机数的函数,绑定按钮:

        //求随机数
        function rand(m, n) {
    
    
            return Math.ceil(Math.random() * (n - m + 1)) + m - 1;
        }
        var btn = document.querySelector('.btn')

接着,用promise去写,promise是一个构造函数=。所以,我们先写一个promise对象,内部用一个箭头函数来代替。

并且,该构造函数有两个参数,一个是resolve函数,还有一个是reject函数。在promise中,执行异步操作。如果异步操作 执行成功则调用resolve函数,异步操作失败则调用reject函数,见如下代码:

 var p = new Promise((resolve, reject) => {
    
    })

接着,我们在promise构造函数中执行异步操作,这里的操作是一个一秒钟的中奖定时器:

            var p = new Promise((resolve, reject) => {
    
    
                //包裹异步操作
                setTimeout(() => {
    
    
                    let n = rand(1, 100)

                    if (n <= 30) {
    
    
                        //异步任务调取成功,中奖了,使用resolve将promise的状态设为成功
                        resolve();
                    } else {
    
    
                        //异步任务调取失败,没中奖,使用reject将promise的状态设为失败
                        reject();
                    }
                }, 1000);
            })

上面代码中,如果中奖则异步操作调用成功,调用resolve,没中奖则异步操作失败,调用reject。resolve和reject设置的其实是promise的状态。

下面我们要写resolve和reject对应的内容了:如果是resolve,则代表操作结果成功,显示中奖;如果是reject,则代表操作结果失败,未中奖,再接再厉。这里用到的是p.then,见如下代码:

            //对成功和失败做出回应
            p.then(() => {
    
    
                //对象成功时的回调
                alert('中奖了')
            }, () => {
    
    
                //对象失败时的回调
                alert('失败了')
            })

以上就是按步骤来的对promise的基本使用。下面是完整代码:

<body>
    <!-- promise是一个构造函数 -->
    <button class="btn">抽奖</button>

    <script>
        //求随机数
        function rand(m, n) {
    
    
            return Math.ceil(Math.random() * (n - m + 1)) + m - 1;
        }
        var btn = document.querySelector('.btn')

        //开始利用promise解决问题,promise是一个构造函数
        // resolve表示异步任务调用成功
        // reject表示异步任务调用失败
        btn.addEventListener('click', () => {
    
    
            var p = new Promise((resolve, reject) => {
    
    
                //包裹异步操作
                setTimeout(() => {
    
    
                    let n = rand(1, 100)

                    if (n <= 30) {
    
    
                        //异步任务调取成功,中奖了,使用resolve将promise的状态设为成功
                        resolve();
                    } else {
    
    
                        //异步任务调取失败,没中奖,使用reject将promise的状态设为失败
                        reject();
                    }
                }, 1000);
            })

            //对成功和失败做出回应
            p.then(() => {
    
    
                //对象成功时的回调
                alert('中奖了')
            }, () => {
    
    
                //对象失败时的回调
                alert('失败了')
            })

        })

    </script>
</body>

1.3 获取参数

在上一个小节中,我们了解到了promise的基本用法。现在增加一个需求,我希望能够把兑奖数字n给alert出来。

分析一下,我们希望异步操作中的n值能够alert出来,也就是说,希望n出现在resolve和reject中。这个时候就需要靠参数传递

参数传递的方式很简单,把n在异步操作调用resolve或者reject时传值。

如下图,传递代码:
在这里插入图片描述
接收代码,注意,这里的value和reason只是个形参,可以任意取名,但一般都是按照value和reason来的:
在这里插入图片描述

效果:
在这里插入图片描述

1.4 promisify介绍

util是nodejs的一个模块。该模块下有个api:promisify。

util.promisify是一个函数,作用是:传入一个遵循常见的错误优先的回调风格函数,并返回一个promise版本。

这里以读取文件为例子:

const util = require('util');
//引入fs
const fs = require('fs');
let mineReadFile = util.promisify(fs.readFile);

mineReadFile('./1.中奖案例.html').then(value => {
    
    
    console.log(value.toString())
});

在这段代码中,我们无需手动把读取文件封装成一个promise对象,只需要把fs.readFile这个api传给promisify函数中,其返回的mineReadFile就是一个封装好的promise对象。

我们可以直接在mineReadFile上then相应的value和reason。

二,Promise的状态属性

2.1 PromiseState的介绍

Promise的状态非常重要。Promise的状态,具体的说,是Promise实例对象中的一个属性,这个属性名叫做PromiseState。

下面我们打印一个promise对象看一下:
在这里插入图片描述
promisestate有三种可能的值:
pending 未决定的;
resolved、fullfilled 表示成功;
rejected 表示失败。

状态一共有两种变化方式:由pedding变为成功,或者由pedding变为失败。它不可能在失败和成功中跳转。并且这种状态智能改变一次。

2.2 PromiseResult的介绍

PromiseResult也是一个属性值,它保存着对象成功或者失败的结果。怎么理解,下面是一段发起ajax请求的代码,成功的时候会返回response,失败的时候会返回状态码。以上返回的都是成功或者失败状态下对应的结果:

        const btn = document.querySelector('button')
        //用promise封装
        btn.addEventListener('click', () => {
    
    
            let p = new Promise((resolve, reject) => {
    
    
                const xhr = new XMLHttpRequest();
                xhr.open('GET', 'http://127.0.0.1:8000/server');
                xhr.send();
                xhr.onreadystatechange = function () {
    
    
                    if (xhr.readyState === 4) {
    
    
                        if (xhr.status >= 200 && xhr.status < 300) {
    
    
                            resolve(xhr.response)
                        } else {
    
    
                            reject(xhr.status)
                        }
                    }
                }
            })

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

而PromiseResult保存的就是这个结果,如下面这张图片,由于是成功的状态,所以对应的结果是”Hello Ajax“,被PromiseResult保存。
在这里插入图片描述
如何对PromiseResult进行修改,resolve(),或者reject()函数的调用可以更改Promise对象的状态。

三,Promise相关API

3.1 Promise构造函数参数函数

有我们刚刚学习的resolve函数:内部定义成功时我们调用的函数 value => {};

以及reject函数:内部定义失败时我们调用的函数reason => {};

以上两个函数是回调函数,构造函数内部还有一个执行器executor函数:(resolve, reject) => {}。

executor会在Promise内部立即同步调用,异步操作在执行器中执行。

上面这一点非常重要!所以这里来讲解下,请看以下代码:

    <script>
        let p = new Promise((resolve, reject) => {
    
    
            console.log('111')
        })

        console.log('222')
    </script> 

请看效果,当我没有调用resolve,reject的时候,仅仅只是创建了p这个对象的时候,111就会出现。说明,执行器函数会在Promise被同步调用的。当我们创建Promise对象的时候,执行器就会被调用!!而且,执行器也是同步的!执行器里面可以放同步操作也可以放异步操作。如console.log就是个同步操作:
在这里插入图片描述

3.2 then和catch方法

then方法,之前已经接触很多次了:

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

本节主要讲解catch。catch也是一个回调函数,但是catch只能用来捕获失败时的回调,而不能捕获成功时的回调。

    <script>
        let p = new Promise((resolve, reject) => {
    
    
           //修改状态
           reject('error') 
        })

        p.catch(reason => {
    
    
            console.log(reason)
        })
    </script>

3.3 resolve方法

resolve方法,属于Promise对象,并不属于实例对象。这句话意味着什么呢,意味着我们需要这样去调用:

Promise.resolve()

如果传入的参数未非Promise类型的对象,则返回的结果是成功的promise对象,见下面代码:

    <script>
        let p1 = Promise.resolve(521)
        console.log(p1)
    </script>

在这里插入图片描述
如果传入参数是promise对象,则参数结果决定了result结果:

        let p2 = Promise.resolve(new Promise((resolve, reject) => {
    
    
            resolve('ok')
        }))
        console.log(p2)

在这里插入图片描述

3.4 reject方法

和上面的resolve用法几乎一样,会返回一个失败的promise对象,转化成一个promise数据。

如果传入的是非promise对象,则会返回一个promise对象,且对象的状态为rejected:

    <script>
        let p1 = Promise.reject(521)
        console.log(p1)
    </script>

在这里插入图片描述
==如果传入的是一个成功的promise对象,结果依旧失败。==见如下代码:

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

在这里插入图片描述

3.5 all方法

all方法依旧属于Promise对象,所以使用格式依旧是:

Promise.all()

all方法内部放置的是多个promise对象,也就是一个数组,如下面代码:

    <script>
    <script>
        let p1 = new Promise((resolve, reject) => {
    
    
            resolve('ok');
        })
        let p2 = Promise.resolve('Sucess')
        let p3 = Promise.resolve('yeah')
        const result = Promise.all([p1, p2, p3])
        console.log(result)
    </script>
    </script>

如果这三个都为resolved状态,则result的状态也是resolved,如果有一个不是,则最后的状态是rejected。

如果成功,则最后返回值是三个返回值连在一起的,如下图:
在这里插入图片描述
现在我把其中一个的状态变为rejected:
在这里插入图片描述
最后结果是失败,所以返回值是失败的那一个:
在这里插入图片描述

3.6 race方法

race的意思是赛跑。依旧是promise对象的一个api。里面同样是一个数组,包含了多个promise。

返回结果:promise对象,结果状态由第一个完成的promise的结果状态决定。

    <script>
        let p1 = new Promise((resolve, reject) => {
    
    
            resolve('ok');
        })

        let p2 = Promise.resolve('Sucess')
        let p3 = Promise.reject('yeah')

        const result = Promise.race([p1, p2, p3])
        console.log(result)
    </script>

上面这段代码,由p1先改变状态。所以最后结果:
在这里插入图片描述
这种里面可以放置多个定时器。

四,Promise的几个关键问题

4.1 如何改变Promise对象的状态

三种方式:
1.调用resolve方法;
2.调用reject方法;
3.抛出错误,使用throw关键字:

    <script>
        let p = new Promise((resolve, reject) => {
    
    
            throw('出问题了')
        })
    </script>

4.2 能否执行多个回调

当有多个回调函数的时候是不是都会调用呢?答案是肯定的,只要状态发生改变对应的回调都会执行。见如下代码,有两个回调函数:

        let p = new Promise((resolve, reject) => {
    
    
            resolve('OK');
        })

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

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

最后的执行效果:
在这里插入图片描述

4.3 改变promise状态与指定回调函数谁先谁后

这里假设改变状态用的是resolve,回调用的是then,则问题可以简化为:promise状态发生改变和指定回调函数被调用谁先谁后。

指定回调:p.then被部署

答案是都有可能。

如果是同步任务,则先改变状态后执行then;如果是异步任务则先执行回调后改变状态。

注意,上面说的是调用then,不是拿到数据。

一定是先改变promise状态,然后才能拿到数据的。

4.4 then方法返回结果由什么来决定

请看下面代码:

    <script>
        let p = new Promise((resolve, reject) => {
    
    
            resolve('ok');
        })

        //执行then方法
        let result = p.then(value => {
    
    
            console.log(value);
        }, reason => {
    
    
            console.warn(reason)
        })

        console.log(result)
    </script>

这个问题相当于是在问,result的结果是什么。

result的结果是一个promise对象,它的结果由谁来决定?是由指定的回调函数的结果来决定的:
情况一,抛出错误,结果报错:
在这里插入图片描述
在这里插入图片描述
情况二,由返回的jpromise对象决定:
在这里插入图片描述
在这里插入图片描述
情况三,是promise对象,return的promise决定其状态
在这里插入图片描述
在这里插入图片描述
完整版代码:

    <script>
        let p = new Promise((resolve, reject) => {
    
    
            resolve('ok');
        })

        //执行then方法
        let result = p.then(value => {
    
    
            //1.抛出错误
            // throw('错误')
            //2.返回结果是非Promise对象
            // return 521;
            //3.返回一个promise对象
            return new Promise((resolve, reject) => {
    
    
                resolve('sucess')
            })
        }, reason => {
    
    
            console.warn(reason)
        })

        console.log(result)
    </script>

4.5 串联多个操作任务

之前在初学promise的时候就了解到了,promise是支持链式调用的。

本节就来了解如何支持链式回调。

如下面代码:

   <script>
    let p = new Promise((resolve, reject) => {
    
    
        setTimeout(() => {
    
    
            resolve('ok');
        }, 1000)
    })

    p.then(value => {
    
    
        return new Promise((resolve, reject) => {
    
    
            resolve('sucess');
        })
    }).then(value => {
    
    
        console.log(value)
    })
   </script> 

最后的结果是sucess。由其指定的回调函数返回值决定。

后面可以用很多个then去调用。

4.6 promise异常穿透

什么是异常穿透?

当使用promise的then链式调用时,在中间简短,不再调用后面的回调函数。

方法:返回一个pendding状态的promise对象。

如下面代码:

    <script>
        let p = new Promise((resolve, reject) => {
    
    
            setTimeout(() => {
    
    
                reject('Err');
            }, 1000)
        })

        const result = p.then(value => {
    
    
            console.log('111')
        }).then(value => {
    
    
            console.log('222')
        }).then(value => {
    
    
            console.log('333')
        }).then(value => {
    
    
            console.log('444')
        }).catch(reason => {
    
    
            console.log('reason')
        })
    </script>

只要前面是错误的,后面会跳过中间一堆,由最终catch处理来捕获,这就叫做异常穿透:
在这里插入图片描述

五,async与await

5.1 async与await的介绍

async和await是一个配套用法,能够更优雅的去写异步编程。

以下是其意义:
1.它是消灭异步回调的终极武器
2.它是同步语法,也就是用同步的写法写异步的代码

下面我们就来详细学习。

5.2 async函数

async函数的返回值为一个promise对象。

promise对象的结果由async函数执行的返回值决定。

这里给出一段很简单的代码:

    <script>
        async function main() {
    
    

        }

        let result = main();
        console.log(result)
    </script>

打印出来的值是:
在这里插入图片描述
如果内部是一个非promise类型的数值,则promise状态为成功,返回值为此非promise类型的数值;

如果内部是一个promise类型的数值,则状态和结果都由这个数值来决定。

5.3 await表达式

await右侧表达式一般为promise对象,但也可以是其他值。

表达式的值:如果表达式是promise对象,await返回的是promise成功的值;如果表达式是其他值,直接将此值作为await返回值。

await表达式必须写在async函数中。但async函数中可以没有await。

如果await的promise是失败状态会抛出异常。需要通过try…catch捕获处理。

看第一段代码,内部是一个promise对象:

    <script>
        async function main() {
    
    
            //右侧为promise
            let p = new Promise((resolve, reject) => {
    
    
                resolve('ok')
            })

            let res = await p;
            console.log(res)
        }

        main()
    </script>

最后结果:
在这里插入图片描述
如果内部为其他类型数据:

    <script>
        async function main() {
    
    
            //右侧为promise
            let p = new Promise((resolve, reject) => {
    
    
                resolve('ok')
            })

            let res = await p;
            let res2 = await 20;
            console.log(res)
            console.log(res2)
        }

        main()
    </script>

最后结果:
在这里插入图片描述
如果promise失败状态:

    <script>
        async function main() {
    
    
            //右侧为promise
            let p = new Promise((resolve, reject) => {
    
    
                // resolve('ok')
                reject('Error')
            })

            let res = await p;      
            console.log(res)
        }
        main()
    </script>

最后结果:
在这里插入图片描述
现在进行错误捕获:

    <script>
        async function main() {
    
    
            //右侧为promise
            let p = new Promise((resolve, reject) => {
    
    
                // resolve('ok')
                reject('Error')
            })

            try {
    
    
                let res3 = await p;
            } catch(e) {
    
    
                console.log(e);
            }

            let res = await p;      
            console.log(res)
        }
        main()
    </script>

最后结果:
在这里插入图片描述

5.4 async与await结合实践

做一个案例,回到最开始的地方,要求输出111,222,333。并且三者不是一起的,而是间隔1s先后输出。

我们先来看一下最开始的那一个地狱回调的代码:

        setTimeout(function () {
    
      //第一层
            console.log('111');
            setTimeout(function () {
    
      //第二程
                console.log('222');
                setTimeout(function () {
    
       //第三层
                    console.log('333');
                }, 1000)
            }, 2000)
        }, 3000)

接着我们用async和await去写:

    <script>
        // 这里我们来写个延迟1s的函数
        function delay(i) {
    
    
            return new Promise((resolve, reject) => {
    
    
                setTimeout(() => {
    
    
                    resolve(i)
                }, 1000)
            }).then(value => {
    
    
                console.log(value)
            })
        }

        // 现在我们来调用它
        async function main() {
    
    
            let res1 = await delay(111);
            let res2 = await delay(222);
            let res3 = await delay(333);
        }

        main();

    </script>

这里要注意,异步任务一定是写在promise中的哦,不要忘记。

后记

赋上promise工作的图片,供大家作深入理解:
在这里插入图片描述
以上就是promise的讲解内容。这个系列文章还有ajax,jquery下的ajax,axios等内容,欢迎关注!!

猜你喜欢

转载自blog.csdn.net/zxdznyy/article/details/129939306