【面试高频知识点】promise的理解(通俗易懂)

2,805 阅读6分钟

前一段时间找工作,面试了大大小小十几家公司,其中也包含了腾讯、blue等知名公司。总结面试经历,发现自己还有很多不足的地方,许多知识点都知其然不知其所以然。趁着最近事比较少,会陆陆续续总结一些面试的高频知识点,提升自己知识的深度和广度。本文是系列文章之一:promise的理解。

一、何为promise,我们为何要使用它?

由于JavaScript语言特性,所有程序都是单线程执行的。由于这个特性,JavaScript的一些浏览器事件、请求事件都是异步执行的,通过回调函数处理异步的结果。这是很常见的语法,但是在一些场景下,就会形成回调函数嵌套回调函数,有的情况甚至套用多层,形成了“回调地狱”,这样使得代码臃肿可读性差而且难以维护。

<!--一个地狱回调的例子,上一个函数的回调结果被下一个函数所依赖-->
const verifyUser = function(username, password, callback){
   require.verifyUser(username, password, (error, userInfo) => {
       if (error) {
           callback(error)
       }else{
           require.getRoles(userInfo, (error, roles) => {
               if (error){
                   callback(error)
               }else {
                   require.logAccess(roles, (error) => {
                       if (error){
                           callback(error);
                       }else{
                           callback(null, userInfo, roles);
                       }
                   })
               }
           })
       }
   })
}

为了解决这种问题,社区提出了一些解决方案,采用链式调用的方法,来解决异步回调,并在在ES6被统一成规范。可以说Promise 是异步编程的一种解决方案。

二、Promise的基本用法

基本用法

作为新的规范,promise采用更加直观也更加易读的方式解决回调嵌套。ES6规定,promise对象是一个构造函数,通过new关键字来生成实例。下面是promise的基本用法

 <!--promise的基本用法-->
 const promise = new Promise((resolve, reject) => {
  // 异步操作的代码
  if (success){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject,他们是JavaScript引擎提供的两个函数。异步操作有两种结果:成功或失败

promise内部状态转变

  • resolve函数在异步操作由pending状态(执行进行中)变为resolved(成功状态)时触发,传递操作成功后的结果;
  • reject函数在异步操作从pending状态(执行进行中)变为rejected(失败状态)时触发,传递操作失败后的结果。

注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变

promise对象方法

Promise.prototype.then()

那么问题来了,刚才我们说到promise状态改变后出触发相应的函数,那么我们处理状态改变的代码要写在哪里呢? 没错,就是then()方法。then方法是定义在原型对象Promise.prototype上的,它的作用是为 Promise 实例添加状态改变时的回调函数then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

 <!--promise then方法-->
 const promise = new Promise((resolve, reject) => {
   resolve('fulfilled'); // 状态由 pending => fulfilled
 }).then(result => { 
    console.log(result); // 'fulfilled' 
 }, reason => { 
    
 })
const promise = new Promise((resolve, reject) => {
  reject('rejected '); // 状态由 pending => rejected
}).then(result => { 
    
}, reason => { 
   console.log(reason); // 'rejected'
})

上边说过,promise状态一旦修改就不能再变 只能由 pending => fulfilled或者 pending => rejected

promise采用链式调用,then()为 Promise 注册回调函数,参数为上一个任务的返回结果,所以链式调用里then 中的函数一定要 return 一个结果或者一个新的 Promise 对象,才可以让之后的then 回调接收。

 const promise = new Promise((resolve, reject) => {
     resolve("success")
 })
.then(result => {
    return result
})
.then(result => {
    console.log(result)  // "success"
})
.catch(err => {})

Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的别名,也就是异步操作发生错误时的回调函数,另外,then()方法里的回调函数发生错误也会被catch()捕获。

 <!--promise catch方法-->
 const promise = new Promise((resolve, reject) => {
   throw new Error('err');
   // 或者reject(new Error('err'));
 }).then(result => { 
    console.log(result); 
 }).catch(err => {
    // 处理前两个promise产生的错误
     console.log(err)
 })

到这里,细心的同学会发现,既然我then()方法第二个参数可以用来抛出错误,干嘛还要用这个catch()方法。 其实还是有区别的,在链式操作里,任何promise抛出的同步或异步错误都可以被then()方法捕获,而reject则处理当前promise的错误。因此,建议不要在then方法里面定义 reject 状态的回调函数(即then的第二个参数),总是使用catch方法,这样也更接近同步的写法(try/catch)。

<!--promise catch方法-->
const promise = new Promise((resolve, reject) => { 
    // some code
})
// good
promise.then(result => { 
   // success
}).catch(err => {
    // err
})
// not recommend
promise.then(result => {
    //success
},err => {
    //err
});

Promise.prototype.finally()

finally()方法是在ES2018引入标准的,该方法表示promise无论什么状态,在执行完then()或者catch()方法后,最后都会执行finally()方法。

<!-- promise finally方法-->
const promise = new Promise((resolve, reject) => {})
.then(result => {})
.catch(err => {})
.finally(() => {})

Promise.all()

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

<!-- promise.all方法-->
const promise1 = new Promise((resolve, reject) => {resolve("promise1")}),
     promise2 = new Promise((resolve, reject) => {resolve("promise2")}),
     promise3 = new Promise((resolve, reject) => {resolve("promise3")});
     
Promise.all([promise1,promise2,promise3]).then(data => { 
  console.log(data); 
  // ["promise1", "promise2", "promise3"] 结果顺序和promise实例数组顺序是一致的
}).catch(err => {
  consolo.log(err)
});

只有promise1、promise2、promise3的状态都变成fulfilled,Promise.all的状态才会变成fulfilled,此时promise1、promise2、promise3的返回值组成一个数组,传递给Promise.all的回调函数。

只要promise1、promise2、promise3之中有一个被rejected,Promise.all的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给Promise.all的回调函数

在做项目的时候我们经常会碰到一个页面要有多个请求,我们可以使用promise.all封装,便于请求管理。

类似的axios也有axios.all()方法处理并发请求

Promise.race()

Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

<!-- promise.race方法-->
const promise = Promise.race([promise1, promise2, promise3]);

上面代码中,只要promise1、promise2、promise3之中有一个实例率先改变状态,promise的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

三、Promise的应用

与axios结合

项目中我们经常会遇到需要根据业务将axios再封装的,比如请求拦截设置token以及Content-Type,响应拦截根据不同的状态码设置不同的响应。此外我们还可以将axios再封装

import axios from "./axios"
import qs from "qs";
export default {
    get: function(url, params) {
      return new Promise((resolve, reject) => {
        axios.get(url, {params: params})
          .then(res => {
            resolve(res)
          })
          .catch(err => {
           console.log(err)
          })
      })
    },
    post: function(url, params) {
      return new Promise((resolve, reject) => {
        axios.post(url, qs.stringify(params))
          .then(res => {
            resolve(res);
          })
          .catch(err => {
              console.log(err)
          })
      });
    }
}
<!--使用 整个user模块的请求都在此文件管理-->
import require from "@/utils/require"
const user = {
    userList() {
      return require.post("/api.php", {}).then(res => res.result)
    },
    userInfo() {
      return require.post("/api.php?&uid=20", {}).then(res => res.result)
    },
    ...
}
export default user

异步加载图片

用promise实现异步加载图片的例子

function loadImageAsync(url) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      image.onload = () => {
        resolve(image);
      };
      image.onerror = () => {
        reject(new Error('Could not load image at ' + url));
      };
      image.src = url;
    });
}
const loadImage = loadImageAsync(url);

写在最后

前端技术近年来发展迅速,新技术层出不穷,作为一个程序员er也要有持续学习的觉悟。 欢迎大家关注我的公众号:前端Readhub 。😄😄