zoukankan      html  css  js  c++  java
  • 面向面试题和实际使用谈promise

    面向面试题和实际使用谈promise

     

      “金三银四,金九银十”,都是要收获的季节。面对各种面试题,各种概念、原理都要去记,挺枯燥的。本文是面向面试题和实际使用谈一下Promise。

    Promise是什么?

      Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一。这句话说的很明白了,Promise是一种用于解决异步问题的思路、方案或者对象方式。在js中,经常使用异步的地方是Ajax交互。比如在es5时代,jQueryajax的使用success来完成异步的:

    $.ajax({
       url:'/xxx',
       success:()=>{},
       error: ()=>{}
    })

      这种方法可以清楚的让读代码的人明白那一部分是Ajax请求成功的回调函数和失败的回调函数。但是问题来了,当一次请求需要连续请求多个接口时,这段代码仿佛进入了一团乱麻中:

    复制代码
    // 第一次 
    $.ajax({
         url:'/xxx',
         success:()=>{
             // 第二次
             $.ajax({
                 url:'/xxx',
                 success:()=>{
                   // 第三次
                   $.ajax({
                      url:'/xxx',
                      success:()=>{
                       // 可能还会有
                      },
                      error: ()=>{}
                    })
                 },
                 error: ()=>{}
            })
         },
         error: ()=>{}
    }) 
    复制代码

      也许因为success和error这两个函数的存在,理解这段代码会很简单,但是当我们更改需求的时候,这将成为一个棘手的问题。这就是回调地狱。

      当然,这是es5时代。当js这门语言发展到es6时代时,Promise的出现给异步带来了变革。Promise提供一个then,来为异步提供回调函数:

    复制代码
    $.ajax({
        url:'/xxx',
    }).then( ()=>{
       // 成功的回调
    }, ()=>{
      // 失败的回调 
    })
    复制代码

      而其先进之处则是,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

    Promise的用法

      说完了Promise是什么,下面让我们研究一下Promise怎么使用。首先,Promise是一个对象,因此,我们使用new的方式新建一个。然后给它传一个函数作为参数,这个函数呢也有两个参数,一个叫resolve(决定),一个叫reject(拒绝),这两个参数也是函数。紧接着,我们使用then来调用这个Promise:

    复制代码
    const fn = new Promise(function (resolve, reject) {
      setTimeout(()=>{
        let num = Math.ceil(Math.random() * 10) // 假设num为7
        if (num > 5) {
          resolve(num) //返回7
        } else {
          reject(num)
        }
      },2000)
    })
    fn.then((res)=>{
      console.log(res) // 7
    },(err)=>{
      console.log(err)
    })
    复制代码

      这就是最简单的Promise的使用。假设2秒钟之后生成随机数为7,因此resolve回调函数运行,then走第一个函数,console.log(7)。假设2秒钟之后生成随机数为3,因此reject回调函数运行,then走第二个函数,console.log(3)。

      那你可能说了,Promise要是就这点能耐也没什么大不了的啊?我们上面说了Promise的先进之处在于可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作:

    复制代码
    fn = new Promise(function (resolve, reject) {
      let num = Math.ceil(Math.random() * 10)
      if (num > 5) {
        resolve(num)
      } else {
        reject(num)
      }
    })
    // 第一次回调
    fn.then((res)=>{
      console.log(`res==>${res}`)
      return new Promise((resolve,reject)=>{
        if(2*res>15){
          resolve(2*res)
        }else{
          reject(2*res)
        }
      })
    },(err)=>{
      console.log(`err==>${err}`)
    }).then((res)=>{ // 第二次回调
      console.log(res)
    },(err)=>{
      console.log(`err==>${err}`)
    })
    复制代码

      这就可以代替了上面类似es5时代的jQurey的success的嵌套式的回调地狱的产生,让代码清爽了许多。这里的resolve就相当于以前的success。

    Promise的原理

      在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。

        (1) promise 对象初始化状态为 pending。

        (2) 当调用resolve(成功),会由pending => fulfilled。

        (3) 当调用reject(失败),会由pending => rejected。

      因此,看上面的的代码中的resolve(num)其实是将promise的状态由pending改为fulfilled,然后向then的成功回掉函数传值,reject反之。但是需要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变(记住,一定要记住,下面会考到)。

      当状态为fulfilled(rejected反之)时,then的成功回调函数会被调用,并接受上面传来的num,进而进行操作。promise.then方法每次调用,都返回一个新的promise对象 所以可以链式写法(无论resolve还是reject都是这样)。

    Promise的几种方法

    then

      then方法用于注册当状态变为fulfilled或者reject时的回调函数:

    // onFulfilled 是用来接收promise成功的值
    // onRejected 是用来接收promise失败的原因
    promise.then(onFulfilled, onRejected);

      需要注意的地方是then方法是异步执行的。

    复制代码
    // resolve(成功) onFulfilled会被调用
    const promise = new Promise((resolve, reject) => {
       resolve('fulfilled'); // 状态由 pending => fulfilled
    });
    promise.then(result => { // onFulfilled
        console.log(result); // 'fulfilled' 
    }, reason => { // onRejected 不会被调用
    })
    
    // reject(失败) onRejected会被调用
    const promise = new Promise((resolve, reject) => {
       reject('rejected'); // 状态由 pending => rejected
    });
    promise.then(result => { // onFulfilled 不会被调用
    }, reason => { // onRejected 
        console.log(rejected); // 'rejected'
    })
    复制代码

    catch

      catch在链式写法中可以捕获前面then中发送的异常。

    复制代码
    fn = new Promise(function (resolve, reject) {
      let num = Math.ceil(Math.random() * 10)
      if (num > 5) {
        resolve(num)
      } else {
        reject(num)
      }
    })
    fn..then((res)=>{
      console.log(res)
    }).catch((err)=>{
      console.log(`err==>${err}`)
    })
    复制代码

      其实,catch相当于then(null,onRejected),前者只是后者的语法糖而已。

    resolve、reject

      Promise.resolve 返回一个fulfilled状态的promise对象,Promise.reject 返回一个rejected状态的promise对象。

    复制代码
    Promise.resolve('hello').then(function(value){
        console.log(value);
    });
    
    Promise.resolve('hello');
    // 相当于
    const promise = new Promise(resolve => {
       resolve('hello');
    });
    
    // reject反之
    复制代码

    all

      但从字面意思上理解,可能为一个状态全部怎么样的意思,让我看一下其用法,就可以看明白这个静态方法:

    复制代码
    var   p1 = Promise.resolve(1),
          p2 = Promise.reject(2),
          p3 = Promise.resolve(3);
    Promise.all([p1, p2, p3]).then((res)=>{
        //then方法不会被执行
        console.log(results);
    }).catch((err)=>{
        //catch方法将会被执行,输出结果为:2
        console.log(err);
    });
    复制代码

      大概就是作为参数的几个promise对象一旦有一个的状态为rejected,则all的返回值就是rejected。

      当这几个作为参数的函数的返回状态为fulfilled时,至于输出的时间就要看谁跑的慢了:

    复制代码
    let p1 = new Promise((resolve)=>{
      setTimeout(()=>{
        console.log('1s') //1s后输出
        resolve(1)
      },1000)
    })
    let p10 = new Promise((resolve)=>{
      setTimeout(()=>{
        console.log('10s') //10s后输出
        resolve(10)
      },10000)
    })
    let p5 = new Promise((resolve)=>{
      setTimeout(()=>{
        console.log('5s') //5s后输出
        resolve(5)
      },5000)
    })
    Promise.all([p1, p10, p5]).then((res)=>{
        console.log(res); // 最后输出
    })
    复制代码

      这段代码运行时,根据看谁跑的慢的原则,则会在10s之后输出[1,10,5]。over,all收工。

    race

      promise.race()方法也可以处理一个promise实例数组但它和promise.all()不同,从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,就向下传递谁的状态和异步结果。但是,其余的还是会继续进行的。

    复制代码
    let p1 = new Promise((resolve)=>{
      setTimeout(()=>{
        console.log('1s') //1s后输出
        resolve(1)
      },1000)
    })
    let p10 = new Promise((resolve)=>{
      setTimeout(()=>{
        console.log('10s') //10s后输出
        resolve(10) //不传递
      },10000)
    })
    let p5 = new Promise((resolve)=>{
      setTimeout(()=>{
        console.log('5s') //5s后输出
        resolve(5) //不传递
      },5000)
    })
    Promise.race([p1, p10, p5]).then((res)=>{
        console.log(res); // 最后输出
    })
    复制代码

      因此,在这段代码的结尾我们的结果为

    1s
    1
    5s
    10s

      我们可以根据race这个属性做超时的操作:

    复制代码
    //请求某个图片资源
    let requestImg = new Promise(function(resolve, reject){
            var img = new Image();
            img.onload = function(){
                resolve(img);
            }
        });
    //延时函数,用于给请求计时
    let timeOut = new Promise(function(resolve, reject){
            setTimeout(function(){
                reject('图片请求超时');
            }, 5000);
        });
    
    Promise.race([requestImg, timeout]).then((res)=>{
        console.log(res);
    }).catch((err)=>{
        console.log(err);
    });
    复制代码

    Promise相关的面试题

    1.

    复制代码
    const promise = new Promise((resolve, reject) => {
        console.log(1);
        resolve();
        console.log(2);
    });
    promise.then(() => {
        console.log(3);
    });
    console.log(4);
    复制代码

      输出结果为:1,2,4,3。

      解题思路:then方法是异步执行的。

    2.

    复制代码
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('success')
        reject('error')
      }, 1000)
    })
    promise.then((res)=>{
      console.log(res)
    },(err)=>{
      console.log(err)
    })
    复制代码

      输出结果:success

      解题思路:Promise状态一旦改变,无法在发生变更。

    3.

    Promise.resolve(1)
      .then(2)
      .then(Promise.resolve(3))
      .then(console.log)

      输出结果:1

      解题思路:Promise的then方法的参数期望是函数,传入非函数则会发生值穿透。

    4.

    复制代码
    setTimeout(()=>{
      console.log('setTimeout')
    })
    let p1 = new Promise((resolve)=>{
      console.log('Promise1')
      resolve('Promise2')
    })
    p1.then((res)=>{
      console.log(res)
    })
    console.log(1)
    复制代码

      输出结果:

        Promise1
        1
        Promise2
        setTimeout

      解题思路:这个牵扯到js的执行队列问题,整个script代码,放在了macrotask queue中,执行到setTimeout时会新建一个macrotask queue。但是,promise.then放到了另一个任务队列microtask queue中。script的执行引擎会取1个macrotask queue中的task,执行之。然后把所有microtask queue顺序执行完,再取setTimeout所在的macrotask queue按顺序开始执行。(具体参考https://www.zhihu.com/question/36972010

     5.
    复制代码
    Promise.resolve(1)
        .then((res) => {
            console.log(res);
            return 2;
        })
        .catch((err) => {
            return 3;
        })
        .then((res) => {
            console.log(res);
        });
    复制代码

      输出结果:1  2

      解题思路:Promise首先resolve(1),接着就会执行then函数,因此会输出1,然后在函数中返回2。因为是resolve函数,因此后面的catch函数不会执行,而是直接执行第二个then函数,因此会输出2。

    6.

    复制代码
    const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    console.log('开始');
    resolve('success');
    }, 5000);
    });
     
    const start = Date.now();
    promise.then((res) => {
    console.log(res, Date.now() - start);
    });
     
    promise.then((res) => {
    console.log(res, Date.now() - start);
    });
    复制代码

      输出结果:

        开始

        success 5002

        success 5002

      解题思路:promise 的.then或者.catch可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用.then 或者.catch都会直接拿到该值。

    7.

    复制代码
    let p1 = new Promise((resolve,reject)=>{
      let num = 6
      if(num<5){
        console.log('resolve1')
        resolve(num)
      }else{
        console.log('reject1')
        reject(num)
      }
    })
    p1.then((res)=>{
      console.log('resolve2')
      console.log(res)
    },(rej)=>{
      console.log('reject2')
      let p2 = new Promise((resolve,reject)=>{
        if(rej*2>10){
          console.log('resolve3')
          resolve(rej*2)
        }else{
          console.log('reject3')
          reject(rej*2)
        }
      })
      return p2 }).then((res)=>{ console.log('resolve4') console.log(res) },(rej)=>{ console.log('reject4') console.log(rej) })
    复制代码

      输出结果:

        reject1
        reject2
        resolve3
        resolve4
        12

      解题思路:我们上面说了Promise的先进之处在于可以在then方法中继续写Promise对象并返回。

    8.重头戏!!!!实现一个简单的Promise
    复制代码
    function Promise(fn){
      var status = 'pending'
      function successNotify(){
          status = 'fulfilled'//状态变为fulfilled
          toDoThen.apply(undefined, arguments)//执行回调
      }
      function failNotify(){
          status = 'rejected'//状态变为rejected
          toDoThen.apply(undefined, arguments)//执行回调
      }
      function toDoThen(){
          setTimeout(()=>{ // 保证回调是异步执行的
              if(status === 'fulfilled'){
                  for(let i =0; i< successArray.length;i ++)    {
                      successArray[i].apply(undefined, arguments)//执行then里面的回掉函数
                  }
              }else if(status === 'rejected'){
                  for(let i =0; i< failArray.length;i ++)    {
                      failArray[i].apply(undefined, arguments)//执行then里面的回掉函数
                  }
              }
          })
      }
      var successArray = []
      var failArray = []
      fn.call(undefined, successNotify, failNotify)
      return {
          then: function(successFn, failFn){
              successArray.push(successFn)
              failArray.push(failFn)
              return undefined // 此处应该返回一个Promise
          }
      }
    }
    复制代码

      解题思路:Promise中的resolve和reject用于改变Promise的状态和传参,then中的参数必须是作为回调执行的函数。因此,当Promise改变状态之后会调用回调函数,根据状态的不同选择需要执行的回调函数。

    总结

      首先,Promise是一个对象,如同其字面意思一样,代表了未来某时间才会知道结果的时间,不受外界因素的印象。Promise一旦触发,其状态只能变为fulfilled或者rejected,并且已经改变不可逆转。Promise的构造函数接受一个函数作为参数,该参数函数的两个参数分别为resolve和reject,其作用分别是将Promise的状态由pending转化为fulfilled或者rejected,并且将成功或者失败的返回值传递出去。then有两个函数作为Promise状态改变时的回调函数,当Promise状态改变时接受传递来的参数并调用相应的函数。then中的回调的过程为异步操作。catch方法是对.then(null,rejectFn)的封装(语法糖),用于指定发生错误时的回掉函数。一般来说,建议不要再then中定义rejected状态的回调函数,应该使用catch方法代替。all和race都是竞速函数,all结束的时间取决于最慢的那个,其作为参数的Promise函数一旦有一个状态为rejected,则总的Promise的状态就为rejected;而race结束的时间取决于最快的那个,一旦最快的那个Promise状态发生改变,那个其总的Promise的状态就变成相应的状态,其余的参数Promise还是会继续进行的。

      当然在es7时代,也出现了await/async的异步方案,这会是我们以后谈论的。

  • 相关阅读:
    POJ-3083 Children of the Candy Corn (BFS+DFS)
    HDU-1429 胜利大逃亡(续) (BFS+状态压缩)
    【JS】408- 看一看 JavaScript 引擎是什么
    【HTTPS】407- 记住 HTTPS!
    【React】406- React Hooks异步操作二三事
    【Git】405- 分享:大牛总结的 Git 使用技巧
    【性能优化】404- 从 12.67s到1.06s 性能优化实战
    【HTTP】402- 深入理解http2.0协议,看这篇就够了!
    【Web技术】401- 在 React 中使用 Shadow DOM
    7.app和app后端的通讯
  • 原文地址:https://www.cnblogs.com/niuzijie/p/11267838.html
Copyright © 2011-2022 走看看