阅读 594

Promise由浅入深

常见Promise面试题

我们看一些Promise的场景面试问法,由浅入深

  • 1、了解Promise吗?
  • 2、Promise解决的痛点是什么?
  • 3、Promise解决的痛点还有其他方法可以解决吗?如果有,请举例
  • 4、Promise如何使用?
  • 5、promise常用的方法有哪些?他们的作用是什么?
  • 6、Promise在事件循环中的执行过程是怎样的
  • 7、Promise的业界实现都有哪些
  • 8、能不能手写一个Promise的polyfill。

Promise出现的原因

在Promise出现之前,我们处理一个异步网络请求,大概是这样子

ajax({
    url:请求地址
    success:function(data){ //成功时的回调
        //处理请求结果
    }
})
复制代码

看起来还不错 但是,需求变化了,我们需要根据第一个网络请求的得到结果,再去执行第二个网络请求,代码大概如下

ajax({
    url: 请求的地址
    success: function(data){ //成功时的回调
        ajax({
            url: 请求的地址
            success:function(data){
                处理请求结果2
            }
        })
    } 
})
复制代码

看起来好像也不复杂 但是,需求是永无止境的,于是乎出现了下面的代码

ajax({
    url:请求的地址
    success: function(data){
        ajax({
            url:请求的地址
            success:function(data){
                ajax({
                    url:请求的地址
                    success:function(data){
                        ajax({
                            url:请求的地址
                            success:function(data){
                                ajax({
                                    url:请求的地址
                                    success:function(data){
                                        ajax({
                                            url:请求的地址
                                            success: function(data){
                                                ...
                                            }
                                        })  
                                    }
                                })
                            }
                        })
                    }
                })
            }
        })
    }
})
复制代码

这回傻眼了吧。。。 臭名昭著的 回调地狱 现身了。

回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套

然而更糟糕的是,我们基本还要对每次请求的结果进行一些处理,导致代码会更加臃肿,在一个团队中,代码review以及后续的维护将会是一个很痛苦的过程。

回调地狱带来的负面作用有以下几点:

  • 代码臃肿
  • 可读性差
  • 耦合度过高,可维护性差
  • 代码复用性差
  • 容易滋生bug
  • 只能在回调里处理异常

为了更加深刻的体验到回调地狱,我们来看一个例子:

案例1:获取李华所在班级的老师的信息

    1. 获取李华的班级id
    2. 根据班级id获取李华所在班级的老师
    3. 根据老师的id查询老师信息
复制代码
//1、首先获取李华所在班级id
ajax({
    url:"./data/students.json",
    success: function(data){
        for(let i = 0; i < data.length; i++){
            if(data[i].name === "李华"){
                const cid = data[i].classId;
                //2、根据班级id获取李华所在班级的老师的id
                ajax({
                    url:"./data/classes.json",
                    success:function(data){
                        for(let i = 0; i < data.length; i++){
                           if (data[i].id === cid) {
                                const tid = data[i].teacherId;
                                // 3. 根据老师的id查询老师信息
                                ajax({
                                    url: "./data/teachers.json?id=" + tid,
                                    success: function(data) {
                                        for (let i = 0; i < data.length; i++) {
                                            if (data[i].id === tid) {
                                                console.log(data[i]);
                                            }
                                        }
                                    }
                                })
                                return;
                            }
                            
                        };
                    }
                })
                return;
            }
        }
    }   
})
复制代码

上面仅仅嵌套了三层回调,代码看起来就已经如此恶心。根本提不起让人提起阅读的欲望。

因此,出现了问题,自然就会有人想去想办法解决。这是,社区里就有人思考了,能不能用一种更加有好的代码组织方式,解决异步嵌套的问题

let 请求结果1 = 请求1();
let 请求结果2 = 请求2(请求结果1); 
let 请求结果3 = 请求3(请求结果2); 
let 请求结果4 = 请求2(请求结果3); 
let 请求结果5 = 请求3(请求结果4); 
复制代码

类似上面这种同步的写法,于是Promise规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点,比如业界著名的Q和号称运行最快的类库bluebird

看官们看到这里,对于上面的问题 2 和问题 7 ,心中是否有了答案呢。^_^

答案2:Promise解决了回调地狱的嵌套问题,但是并没有消除回调,回调依然存在,Promise只是让回调变得可控

什么是Promise

ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。

值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该API,会让异步处理更加的简洁优雅。

理解该 API,最重要的,是理解它的异步模型

  1. ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettledsettled
  • unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
  • settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转

并且,事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。

  1. ES6将事情划分为三种状态:pending,resolved,rejected
  • pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
  • resolved:已出来,已决阶段的一种状态,表示整件事情已经出现结果,并且是一个可以按照正常逻辑进行下去的结果
  • rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法安装正常逻辑进行下去的结果,通常用于表示有一个错误

既然未决阶段有权利决定事情的走向,因此,未决阶段可以决定事情最终的状态!

我们将 把事情变成resolved状态的过程叫做:resolve,推向该状态时,可能会传递一些数据

我们将 把事情变成rejected状态的过程叫做:reject,推向该状态时,同样可能传递一些数据,通常为错误信息 始终记住,无论是阶段,还是状态,是不可逆的!

  1. 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
  • resolved状态:这是一个正常的已决状态,后续处理表示为thenable
  • rejected状态:这是一个非正常的已决状态,后续处理表示为catchable 后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行
  1. 至此, 整件事称之为Promise
const pro = new Promise((resolve, reject)=>{
    // 未决阶段的处理
    // 通过调用resolve函数将Promise推向已决阶段的resolved状态
    // 通过调用reject函数将Promise推向已决阶段的rejected状态
    // resolve和reject均可以传递最多一个参数,表示推向状态的数据
})

pro.then(data=>{
    //这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
    //如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
    //data为状态数据
}, err=>{
    //这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
    //如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
    //err为状态数据
})
复制代码

细节

  1. 未决阶段的处理函数是同步的,会立即执行
  2. thenable 和catchable 函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且加入的是微队列
  3. pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数
  4. 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable捕获
  5. 一旦状态推向的已决阶段,无法在对状态做任何更改
  6. Promise并没有消除回调,只是让回调变得可控

Promise的串联

什么是Promise的串联?就是当后续的Promise需要用到之前的Promise的处理结果时,就需要用到Promise的串联

Promise对象中,无论是then方法还是catch方法,它们都具有返回值,返回的是一个全新的Promise对象,它的状态满足下面的规则:

  1. 如果当前的Promise是未决的,得到的新的Promise是挂起状态
  2. 如果当前的Promise是已决的,会运行响应的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中;如果后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中。

后续的Promise一定会等到前面的Promise有了后续处理结果后,才会变成已决状态

如果前面的Promise的后续处理,返回的是一个Promise,则返回的新的Promise状态和后续处理返回的Promise状态保持一致。

了解了Promise后,再用Promise实现前面案例一

案例:获取李华所在班级的老师的信息(前提需要将ajax异步请求变成Promise对象)

查看案例:codesandbox.io/s/dazzling-…

    //1. 获取李华的班级id   Promise
    //2. 根据班级id获取李华所在班级的老师id   Promise
    //3. 根据老师的id查询老师信息   Promise
复制代码
    ajax({
            url:"./data/student.json";
    }).then(resp=>{                                 //then(1) 辅助说明作用
        for(let i = 0; i < resp.length; i++){
            if(resp[i].name === "李华"){
                return resp[i],classId;
            }
        }
    },err=>{
        console.log(err);
    }).then(cid=>{                                  //then(2)  辅助说明作用
        return ajax({
            url:"./data/classes.json"
        }).then(cls =>{
            for(let i = 0; i< cls.length; i++){
                if(cls[i).id === cid{
                    return cls[i].teacherId;
                }
            }
        })
    }).then(tid=>{                                  //then3   辅助说明作用
        return ajax({
            url: "./data/teachers.json"
        }).then(tls=>{
            for(let i = 0; i < tls.length; i++){
                if(tls[i).id = tid{
                    return tls[i]
                }
            }
        })  
    }).then(info=>{                                 //then4
        console.log(info);
    })
复制代码

运行流程解析:首先,调用ajax请求获取所有学生信息,返回一个Promise对象,如果该Promise对象是已决阶段的resolved状态,调用该Promise对象的then(1)方法时,会立即执行里面的后续处理函数,然后得到一个包含所有学生信息json数据,循环得到的学生数据,返回名字为李华的学生所在的班级id。然后顺序,我们接下来会发送第二次的ajax请求获取班级数据,但这时,我们发现,我们需要用到上一个Promise的then1方法的后续处理函数运行返回的班级id结果,当成第二次ajax请求获取到的所有班级数据中的条件,从而拿到对应班级的老师的id,这是我们就需要用到Promise的串联,根据Promise的串联规则,如果第一次ajax请求得到的Promise状态是未决的,then1得到的新的Promise是挂起状态,如果第一次ajax请求得到的Promise是已决的,会运行then1的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中(then2的Promise),如果then1后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中(then2的promise)。此时then2的后续处理函数的参数cid接受到了then1的后续处理函数返回的结果,then2的promise状态为resolved,所以立即运行then2的后续处理函数,发送第二次ajax请求,返回一个resolved状态的promise对象,调用该then方法,执行后续处理函数。在里面遍历resolved的状态数据,也就是存放所有班级数据的json数据,根据对应的班级id条件,拿到对应班级老师的id,并返回出去,由于then2的后续处理函数返回的是一个Promise对象(第二个ajax请求),则then2返回的新的Promise状态和then2后续处理函数返回的Promise状态保持一致(第二个ajax请求),并将第二次ajax请求的then的后续处理函数返回的结果作为then2返回的新Promise对象的resolved状态数据,then2的后续处理函数运行结束后,开始运行then3的后续处理函数,第三次发送ajax请求,再次返回一个包含resolved状态和数据的Promise对象,运行第三次ajax请求的后续处理函数,得到所有老师信息,根据第二次ajax请求返回的id,得到该id对应的老师信息,并返回给then3返回的新Promise,作为该Promise resolved的状态数据,最后,运行该Promise的then方法(then4),输出该老师的信息。至此,整个运行结束。

API

Promise的常用它PI如下:

原型成员(3)

  • then 注册一个后续处理函数,当Promise为resolved状态时运行该函数
    const pro = new Promise((resolve,reject)=>{
        resolve(1)  ////将状态推向已决,并传递resolved状态数据
    })
    pro.then(data=>{
        //data接收resolved传递的状态数据
        console.log(data);  //1
    })
复制代码
  • catch 注册一个后续处理函数,当Promise为reject状态时运行该函数
    const pro = new Promise((resolve,reject)=>{
        //reject和抛出错误都能将状态推向reject的已决状态
        // reject(2)                //将状态推向已决,并传递reject状态数据
        throw new Error(2)          //将状态推向已决,并传递reject状态数据

    })
    pro.then(data=>{
            
    },err=>{    //then可接收两个后续处理函数,resolve和reject的
        console.log(err);   //Error: 2
    })          //catch只能接受一个reject后续处理函数
    pro.catch(err=>{
        console.log(err);   //Error: 2
    })
复制代码
  • finally [ES2018]注册一个后续处理函数(无参),当Promise为已决时运行该函数
    const pro = new Promise((resolve, reject) => {
        //resolve(1);
        reject(2);
    })
    pro.then(data => {
        console.log(data);  //1
    }, err => {
        console.log(err);   //2
    })

    pro.finally(() => {
        console.log("finally"); //无论是reject状态还是resolve状态,都会执行该后续处理函数
    })
复制代码

始终记住,从未决阶段到已决阶段是不可逆的,通向已决阶段的最终只能有一个状态,不是resolved就是rejected,两种不能共存

构造函数成员(4)

  • resolved(数据):该方法返回一个resolved状态的Promise,传递的数据作为状态数据
    • 特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象,可以理解为复制了该promise的指针
    const pro1 = Promise.resolve(1);
    console.log(pro1);      //Promise {<resolved>: 1}

    // 等效于
    const pro2 = new Promise((resolve,reject)=>{
        resolve(1)  
    })
    pro2.then(data=>{
        console.log(data);  //1
    })
    console.log(pro2);      //Promise {<resolved>: 1}
复制代码
    const pro1 = Promise.resolve(2);
    console.log(pro1);      //Promise {<resolved>: 1}
    
    const pro3 = Promise.resolve(pro1);//如果传递的是一个Promise对象

    console.log(pro3);      //Promise {<resolved>: 1}
    console.log(pro1===pro3);   //true
复制代码
  • reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据
    const pro1 = Promise.reject(2);
    console.log(pro1);      //Promise {<rejected>: 2}

    //  等效于
    const pro3 = new Promise((resolve,reject)=>{
        reject(2);
    })
    console.log(pro3);      //Promise {<rejected>: 2}
复制代码
  • all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才触发成功,一旦有任何一个iterable里面的promise对象失败则立即出发该promise对象的失败。这个新的promise对象咋触发成功状态后,会把一个包含iterable里所有的promise返回值的数组作为成功的回调返回值,顺序跟iterable的顺序保持一致,如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常用与被用于处理多个promise对象的状态集合。 查看例子:codesandbox.io/s/static-el…
    function getRandom(min,max){
        return Math.floor(Math.random()*(max - min) + min); //随机生成一个min -max区间的随机数
    }
    const arr = [];
    for (let i = 0; i < 10; i++) {
        arr.push(new Promise((resolve,reject)=>{
            setTimeout(() => {
                if (Math.random() < 0.9) {
                    console.log(i,"完成");
                    resolve(i)
                }else{
                    console.log(i,"失败");
                    reject(i);
                }
            }, getRandom(1000,5000));
        }))
    }

    const pro = Promise.all(arr);
    pro.then(datas=>{
        console.log(datas);
    })
    pro.catch(err=>{
        console.log(err);
    })
    console.log(arr);
    //接收一个存放Promise的数组,当数组里边的所有Promise状态达到resolved后,返回一个resolve状态的Promised对象,
    //并传递一个由所有promise状态数据组成的数组当做返回的promise的状态数据,如果里边只要有一个promise变成
    //reject状态,则会把第一个触发失败的Promise对象作为他的失败错误信息
复制代码
  • race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象
 <script>
    function getRandom(min,max){
        return Math.floor(Math.random()*(max - min) + min); //随机生成一个min -max区间的随机数
    }
    const arr = [];
    for (let i = 1; i <=10; i++) {
        arr.push(new Promise((resolve,reject)=>{
            setTimeout(() => {
                if (Math.random() < 0.5) {
                    console.log(i,"完成");
                    resolve(i)
                }else{
                    console.log(i,"失败");
                    reject(i);
                }
            }, getRandom(1000,5000));

        }))

    }

    const pro = Promise.race(arr);
    pro.then(data=>{
        console.log(data,"是第一完成的");
        console.log(pro);
    })
    pro.catch(err=>{
        console.log(err,"是第一失败的");
        console.log(pro);

    })
    console.log(arr);

    </script>
复制代码

async 和await

async 和 await 是 ES2016 新增两个关键字,它们借鉴了 ES2015 中生成器在实际开发中的应用,目的是简化 Promise api 的使用,并非是替代 Promise。

async

目的是简化在函数的返回值中对Promise的创建

async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象。

async function test(){
    console.log(1);
    return 2;
}

//等效于

function test(){
    return new Promise((resolve, reject)=>{
        console.log(1);
        resolve(2);
    })
}
复制代码

await

await关键字必须出现在async函数中!!!!

await用在某个表达式之前,如果表达式是一个Promise,则得到的是thenable中的状态数据。

async function test1(){
    console.log(1);
    return 2;
}

async function test2(){
    const result = await test1();
    console.log(result);    //2
}

test2();
复制代码

等效于

function test1(){
    return new Promise((resolve, reject)=>{
        console.log(1);
        resolve(2);
    })
}

function test2(){
    return new Promise((resolve, reject)=>{
        test1().then(data => {
            const result = data;
            console.log(result);    //2
            resolve();
        })
    })
}

test2();
复制代码

如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按照规则运行

利用async和await 获取李华所在班级的老师的信息

查看案例:codesandbox.io/s/static-2v…

//1. 获取李华的班级id   Promise
//2. 根据班级id获取李华所在班级的老师id   Promise
//3. 根据老师的id查询老师信息   Promise
复制代码
  async function getTeacher() {
    //现在处在ajax返回的promise中的then环境中
    const students = await ajax({
      url: "./data/students.json"
    });
    let cid;
    for (let i = 0; i < students.length; i++) {
      if (students[i].name === "李华") {
        cid = students[i].classId;
      }
    }
    //现在处在另一个ajax返回的promise中的then环境中
    const classes = await ajax({
      url: "./data/classes.json"
    });
    let tid;
    for (let i = 0; i < classes.length; i++) {
      if (classes[i].id === cid) {
        tid = classes[i].teacherId;
      }
    }
    //现在处在另一个ajax返回的promise中的then环境中
    const teachers = await ajax({
      url: "./data/teachers.json"
    });
    let info;
    for (let i = 0; i < teachers.length; i++) {
      if (teachers[i].id === tid) {
        console.log(teachers[i]);
      }
    }
  }
  getTeacher();
复制代码

细节:

async:

在函数前面加上async关键字,该函数就会返回一个已决阶段的promise对象,在函数的大括号里面运行的代码相当于在promise对象的未决阶段环境,是同步代码,在这里return是返回resolved状态的状态数据,默认return undefined,如果执行过程中发出错误,会返回一个rejected状态的错误信息

await:

  • 如果await后面跟的是一个promise,则必须等待promise到达已决阶段的resolved状态后才执行,否则会一直等待,包括await在内的所有代码,都是在then的后续处理函数中环境中运行
  • 如果await后面跟的不是Promise, (为了依旧保持异步),会通过Promise.resolve() new一个resolve状态的promise对象,然后在其then的后续处理函数中环境中运行
  • 由于await只在resolve中then中运行,如果已决阶段是reject,await则会报错,导致程序无法正常执行,因此我们需要用try-catch去捕获reject

例如:

    async function getPromise() {
        if (Math.random() < 0.5) {
            return 1
        } else {
            throw 2
        }
    }

    async function test() {
        try {
            const result = await getPromise();
            console.log('正常状态:',result);
        } catch (err) {
            console.log('错误状态:',err);
        }
    }
    test()
复制代码
关注下面的标签,发现更多相似文章
评论