Promise及其应用

612 阅读10分钟

Promise及其应用

什么是Promise

Promise是JavaScript中的一种实现异步的机制,它被广泛应用于浏览器和服务器端的JavaScript代码中,可以帮助我们更好地处理异步操作。

我们知道可以通过回调函数的方式在JS中实现异步操作,那么为什么要引入Promise的机制来实现异步呢?首先我们要搞清楚Promise和异步编程的关系。

异步编程与Promise的关系

异步编程是一种编程范式,主要用于处理一些需要等待时间才能返回结果的操作。比如,从服务器端获取数据、读取本地文件等。在传统的同步编程中,这些操作会阻塞程序的执行,导致程序变慢。

而 Promise 是一种异步编程模型,它可以让我们更加方便地处理异步操作。Promise 可以将一个异步操作封装成一个 Promise 对象,然后通过 Promise 的 then 方法来处理异步操作的结果。这样一来,我们就不必再使用回调函数来处理异步操作的结果了。

明白了这些,我们来直观地看看,Promise方法相对传统回调的方式有什么优势。

回调函数 VS Promise

在日常开发中,往往会遇到这样的需求:

通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。

如果采用回调函数的方式,其实现代码如下:

var outnum = function (n, callback) {
  setTimeout(function () {
    console.log(n);
    callback();
  }, 1000);
};
outnum("1", function () {
  outnum("2", function () {
    outnum("3", function () {
      console.log("0");
    });
  });
});

通过上述代码我们不难发现,虽然可以通过回调函数层层嵌套的形式达到最终数据请求的目的,但代码结构不甚明朗,可读性极差,这就是传说中的回调地狱。

如果我们用Promise的方式,就可以较为优雅的解决这个问题:

var outnum = function (order) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(order);
      resolve();
    }, 1000);
  });
};
outnum("1")
  .then(function () {
    return outnum("2");
  })
  .then(function () {
    return outnum("3");
  })
  .then(function () {
    console.log("0");
  });

执行上述代码之后的页面效果与使用地狱式回调方式是完全一样的,但 Promise 对象实现的代码可读性更强,还可以很方便地取到异步执行后传递的参数值,因此,这种代码的实现方式,更适合在异步编程中使用。

Promise的作用与用法

通过上述例子,我们已经了解到了Promise方法处理异步操作的优势,那么下面正式介绍一下它的用法。

Promise 可以将一个异步操作封装成一个 Promise 对象,Promise有三种状态:pending(初始状态)、fulfilled(操作成功完成)和rejected(操作失败)。通过使用then()和catch()方法,我们可以根据Promise对象的状态来执行相应的回调函数,进而处理异步操作的结果。

Promise 的基本用法如下:

const promise = new Promise((resolve, reject) => {
  // 异步操作
});
promise.then(result => {
  // 处理异步操作成功的结果
}).catch(error => {
  // 处理异步操作失败的结果
});

其中,Promise 构造函数接受一个函数作为参数,该函数包含一个 resolve 参数和一个 reject 参数。

  • 当异步操作成功时,我们需要调用 resolve 函数,并将异步操作的结果作为参数传入;
  • 当异步操作失败时,我们需要调用 reject 函数,并将错误信息作为参数传入。

then 方法会在异步操作成功时被调用,它接受一个回调函数作为参数,该回调函数的参数就是异步操作的结果。

catch 方法则会在异步操作失败时被调用,它也接受一个回调函数作为参数,该回调函数的参数就是错误信息。

Promise的静态方法

Promise.resolve()方法

  • 返回一个具有给定值的 Promise 对象。
  • 如果传入的参数是一个 Promise 实例,则该实例将被直接返回(见下面的实例);
  • 否则,将创建一个新的 Promise 对象,并将传入的参数作为 Promise 对象的fulfilled值。
let p = new Promise((resolve) => {
      setTimeout(resolve, 1000, "hello");
    });
let p2 = Promise.resolve(p);
console.log(p == p2); //true,直接返回实例没有新创建对象

Promise.reject()方法

Promise还具有reject()方法,用于返回一个rejected状态的Promise实例,但一般情况下没有太大用处。

Promise.reject( new Error( "a special error"))
.catch((err) =>{
console.log(err.message);return "done" ;
})
    .then((value) =>{console.log(value);});

Promise.all( )方法

Promise.all( )实参是所有Promise实例的字面量组成的数组,执行完毕的==结果是所有输出结果的所组成的数组==

  var p1 = new Promise((res, rej) => {
    setTimeout(() => {
      res("p1");
    }, 1000);
  });
  var p2 = new Promise((res, rej) => {
    setTimeout(() => {
      res("p2");
    }, 2000);
  });
  var p3 = new Promise((res, rej) => {
    setTimeout(() => {
      res("p3");
    }, 3000);
  });
  Promise.all([p1, p2, p3]).then((r) => {
      console.log(r);//输出数组,顺序和all中参数顺序相同
    }).catch((err) => console.log(err.messsage));

Promise.race(iterable) 方法

race即为‘赛跑‘之意,比哪个异步任务先完成

iterable为包含了多个promise对象的可迭代数据结构,如数组。

该方法返回一个 promise。

一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

const promise1 = new Promise( (resolve, reject) =>{setTimeout(resolve,5e00,"one");
});
const promise2 = new Promise( (resolve, reject) =>{setTimeout(resolve,100,"two");
});
const promise3 = new Promise((resoLve, reject)=>{
setTimeout(reject,200new Error( "some thing is wrong" ));});
Promise.race([promise1,promise2, promise3])
.then((value) =>{
console.log(value);})
.catch((err) =>{
console.log(err.message);});

Promise在实际环境下的应用

Promise 在实际环境中的应用非常广泛,比如:

  • Ajax 请求:使用 Promise 可以方便地处理 Ajax 请求的结果。
  • 加载图片:使用 Promise 可以方便地处理图片的加载。
  • 文件读取:使用 Promise 可以方便地处理文件读取的结果。
  • ...

Ajax 请求的示例:

function request(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
  });
}

request('/api/data')
  .then(data => console.log(data))
  .catch(error => console.error(error));

异步加载图片的示例:

function loadImageAsync(url){
  return new Promise(function(resolve, reject){
    var image=new Image();
      image.onload=function(){
        image.src=url;
        resolve(this);
      };
      image.onerror=function(){
        reject(new Error('Counld not load image at ' + url));
      };
    });
}
loadImageAsync('http://p.ananas.chaoxing.com/star3/origin/56eba451498edbe2900a334f.png').then(function(image){
    document.body.appendChild(image);
    console.log(`Image loaded:${image.src}`);
}, function(error){
    console.log(error);
});

文件加载的示例:

function loadScript(url) {
  return new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = url;
    script.onload = function() {
      resolve();
    };
    script.onerror = function() {
      reject(new Error('Load error'));
    };
    document.head.appendChild(script);
  });
}

loadScript('https://example.com/script.js')
  .then(function() {
    console.log('Script loaded');
  })
  .catch(function(error) {
    console.error(error);
  });

async/await的作用与用法

async/await 是 ES2017 引入的新特性,它可以让异步代码看起来像同步代码一样。

async/await 实际上是基于 Promise 的封装,所以async/await 其实是Promise的语法糖,只有Promise可以用的场景才能用async/await

async 关键字

async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值

async function fn() {
  return "12345";
}
fn().then((val) => {
  console.log(val);
    //执行上述代码后,将在页面的控制台输出 “12345” 字符
});

通过上述示例,我们明确以下两点:

  • 使用 async 关键字定义的函数,将会返回一个 Promise 对象。
  • 函数中有返回值,则相当于执行了 Promise.resolve(返回值) 函数,没有返回值,则相当于执行了 Promise.resolve() 函数。

虽然 async 关键字简化了我们之前实现异步请求中返回 Promise 实例对象的那一步,直接返回了一个 Promise 对象,但是仍然需要在 then 方法中处理异步获取到的数据。有没有什么办法可以继续优化呢?比如省去 then 方法的调用,让异步操作写起来更像同步操作那么简洁明了?

await 关键字

在异步函数中,可以使用await操作符等待异步操作完成并返回结果。

await 必须在 async 定义的函数中,不能单独使用,await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码

// 函数 fetchData 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function fetchData(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 fetchData 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}

// 一个用于正常输出内容的函数
function log() {
  console.log("2.正在操作");
}

async function main() {
  console.log("1.开始");
  await log();
  let p1 = await fetchData("3.异步请求");
  console.log(p1);
  console.log("4.结束");
}
main();

执行上述代码后,页面在控制台输出的效果如下所示:

图片描述

根据页面效果,源代码解析如下:

  • fn 函数执行后,首先,会按照代码执行流程,先输出“1.开始”。
  • 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出“2.正在操作”。
  • 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。
  • 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。
  • 所以,最后的执行顺序是,先输出 “3.异步请求”,再输出 "4.结束",在 async 函数中的执行顺序,如下图所示。

图片描述

async/await 优化的应用

基于 await 的特性,可以将异步请求的代码变成同步请求时的书写格式,代码会更加优雅,特别是处理多层需要嵌套传参时,使用 await 的方式,代码会更少,更易于阅读,现在我们来解决之前用Promise解决过的需求:

需要发送三次异步请求,通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。

function outnum(order) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(order);
      resolve();
    }, 1000);
  });
}

async function main() {
  try {
    await outnum("1");
    await outnum("2");
    await outnum("3");
    console.log("0");
  } catch (error) {
    console.error(error);
  }
  //在异步函数中,如果出现了错误,则需要使用try/catch捕获并处理错误。
}
main();

由此可见通过async/await 优化后,代码更加简洁,再一次增加了可读性。但是async/await并不能完全取代Promise,需要根据实际场景选取,比如下面这个例子:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  await Promise.all([p("a"), p("b"), p("c")]);
  console.log("隐藏加载动画!");
}
fn();

在上述实现的代码中,方法 Promise.all 中每个实例化的 Promise 对象,都会以并行的方式发送异步请求,当所有请求都成功后,才会去执行输出字符内容的代码

如果不用Promise中的静态方法:

async function fn() {
  await p("a");
  await p("b");
  await p("c");
  console.log("隐藏加载动画!");
}
fn();

从结果上看,无论是函数修改之前还是修改之后 ,结果都一样,但在 fn 函数修改之前,所有的异步请求都是并行发送的,而在函数修改之后,所有的异步请求都是按顺序执行的。

从性能上来看,fn 函数修改之前的异步请求并发执行明显高于修改之后的阻塞式异步请求,因此,虽然有了 asyncawait ,但也不能完全取代 Promise 对象。


通过本文的介绍,相信你已经对 Promise 有了更深入的了解,并且掌握了如何使用 Promise 来处理异步操作。但在实际开发中,我们应该合理地运用 Promise ,这样才能提高代码的可读性和可维护性,同时也能够提高性能和响应速度。