JS中异步编程

知识点

  • Promise 对象基础应用
  • Promise 对象中的方法
  • async 关键字
  • await 关键字

解决异步方法:

  1. 回调函数
  2. promise
  3. async await

Promise 对象基础应用

关于 Promise 对象的基础应用,本文会给大家介绍以下几个方面的内容:

  • 地狱式回调
  • 定义 Promise 对象
  • Promise 对象的 then 方法
  • 解决地狱式回调

地狱式回调

在日常开发中,往往会遇到这样的需求:通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。为了实现这个需求,通常会使用回调函数来完成,即把函数作为参数进行层层嵌套。其实现代码如下:

首先,打开我们的线上环境,新建一个 index.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

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 对象

为了解决这种地狱式的回调,可以使用 Promise 对象,且代码更优雅,由于 Promise 对象是一个构造函数,因此,必须通过实例化来生成,它的定义格式如下代码:

let p = new Promise(function (resolve, reject) {
  // 此处做一个异步的事情
});

在定义格式的代码中,需要说明的几个问题:

  • 在实例化中,参数为函数,函数中又有两个用于回调的函数
  • 两个回调函数中,resolve 为异步执行成功时的回调,其参数可以传递执行的结果。
  • reject 为异步执行失败时的回调,其参数可以传递失败的错误信息。

使用 resolve 和 reject 方法传递出去的参数被谁接收到了,是以何种方式接收的?接下来说下 then 方法。

Promise 对象的 then 方法

Promise 对象实例化后,可以调用 then 方法获取两个回调函数中的传参值,该方法接收两个回调函数作为参数,第一个参数是必选参数,表示异步成功后执行的 resolve 回调函数,第二个参数是可选参数,表示异步失败后执行的 reject 回调函数,它的调用格式如下:

p.then(
  function () {},
  function () {}
);

回调函数带参数的格式如下:

p.then(
  function (v) {},
  function (e) {}
);

其中参数 v 值表示 resolve 回调函数中的参数值,e 值表示 reject 回调函数中的参数值,如下列代码所示:

let n = 6;
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    if (n > 5) {
      resolve(n);
    } else {
      reject("必须大于5");
    }
  });
});
p2.then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);
// 执行代码后,由于 n 值大于 5 ,因此,在控制台中输出数字 6 。

此外,一个 then 方法被执行后,如果仍然返回一个 Promise 对象,则可以继续再执行 then 方法,形成链式写法效果,代码如下所示:

p1.then(function (v) {
  return p1;
}).then(function (v) {
  return p1;
});

解决地狱式回调

在学习完 Promise 对象的定义和 then 方法调用后,接下来,我们使用 Promise 来实现开头提到的需求,从而解决由此引起的回调地狱问题,实现过程如下:

首先,打开我们的线上环境,新建一个 index2.html文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

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.all 方法
  • Promise.race 方法

Promise.all 方法

日常开发过程中,往往会遇到这种问题,当首次加载某个页面时,由于数据庞大需要分别同时发送多个异步请求向服务器获取数据,最终所有数据返回之后做下一步操作(如“隐藏页面的加载 loading 动画”)。由于很难捕获这些异步请求全部成功的时机,导致这个需求实现起来相当困难。难道就没有解决办法了吗?🤔 这时使用 Promise.all 方法就可以解决这种问题。

使用格式

Promise.all 方法中的参数是一个数组,数组中的每个元素是实例化后的 Promise 对象,格式如下代码:

Promise.all([p1,p2,p3,...]).then(res=>{
  // 所有请求成功后的操作步骤
},error=>{
  // 某一个请求失败后的操作步骤
});

上述代码中,p1、p2、p3 都是实例化后的 Promise 对象,并且该方法可以通过链式写法直接调用 Promise.all 中的 then 方法,当全部的实例化对象都执行成功后,进入 then 方法的第一个执行成功的回调函数中,函数参数是每个任务执行成功后的结果,以数组形式保存,如下图所示:

如果在调用 Promise.all 方法时,有一个 Promise 实例对象(比如:p1)的任务执行失败了,则会直接进入 Promise.all 后的 then 方法的失败回调函数中,如下图所示: 

通过 Promise.all 方法可以并列完成多个异步的请求,只有当全部请求成功后,才进入 then 方法中的成功回调函数中,否则,进入失败的回调函数中,因此,当首次加载页面时,可以将各种的异步请求放入 Promise.all 方法中,如果全部完成,则在 then 方法中的成功回调函数中执行下步操作,否则,直接进入失败回调函数中。

实战应用

下列通过一个实战来演示 Promise.all 方法的使用过程,功能说明:

  1. 定义一个函数 p1,返回一个 Promise 对象,在返回过程中,执行一个延时操作,定义一个参数 n ,如果参数 n 大于 0 ,则返回该数据,否则,则返回 “ 不能小于 0“ 的字符信息。
  2. 调用 Promise.all 方法,使用不同的参数,调用三次 p1 函数,当全部执行成功或有一个执行失败后,分别查看控制台的输出信息。

为了实现这个功能,首先,打开我们的线上环境,新建一个 index3.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

function p1(n) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      if (n > 0) {
        resolve(n);
      } else {
        reject("不能小于0");
      }
    }, 1000);
  });
}

先传入三个执行成功的任务,在新建页面中的 script 元素中添加如下代码:

Promise.all([p1(5), p1(6), p1(7)]).then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);

从上述效果可以看出,如全部任务执行成功,则将各个执行结果保存在数组中,可以通过 then 方法中的成功回调函数返回。

此外,传入一个执行失败的任务,二个执行成功的任务,在新建页面中的 script 元素中再添加如下代码,

Promise.all([p1(5), p1(-2), p1(7)]).then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);

上述代码执行后的效果如下图所示:

从上述效果可以看出,如有一个任务执行失败,则通过 then 方法中的失败回调函数返回错误信息。

Promise.race 方法

与 Promise.all 方法不同,Promise.race 方法是多个 Promise 实例化对象在比赛, 执行最快的那个任务的结果,将返回给 then 方法中的对应回调函数中,通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。

使用格式

与 Promise.all 方法一样,Promise.race 中的参数也是一个数组,每个元素也是实例化后的 Promise 对象,格式如下代码:

Promise.race([p1,p2,p3,...]).then(
    function(v){
      //获取最快任务成功时的返回值
  },
  function(){
      //获取最快任务失败时的返回值
  }
)

Promise.then 方法的缺点

Promise 对象虽然很优雅地解决了地狱回调的情形,使代码更简洁和易读,但通过 then 方法取值时,代码还是不够时尚和前沿,多层嵌套取值时也不够高效,如下列代码所示:

var p = function (msg) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(msg);
    }, 1000);
  });
};
p("明明")
  .then(function (v) {
    return p(v + ",男");
  })
  .then(function (v) {
    return p(v + ",今年18岁");
  })
  .then(function (v) {
    console.log(v);
  });

async 关键字和 await 关键字

在上述代码中,then 方法在取值和传值时,如果层级多时,它的代码的结构并不易读,下面我们通过 async 和 await 来解决这个问题,一起来看下它们的用法~

async 关键字

async 英文单词的意思是异步,虽然它是 ES7 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写化,它能方便程序员的代码开发),async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值,下面通过一个简单示例来说明它的使用。

首先,打开我们的线上环境,新建一个 index5.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

async function fn() {
  return "12345";
}
fn().then((val) => {
  console.log(val);
});

在上述代码中,定义一个名称为 fn 的函数,但由于在函数前添加了关键字 async ,使这个函数将返回一个 Promise 对象,因此,函数执行后,可以直接调用 then 方法;同时,fn 函数中的返回值,就是 then 方法中,执行成功回调函数时的参数值,因此,执行上述代码后,将在页面的控制台输出 “12345” 字符,效果如下所示:

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

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

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

await 关键字

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

首先,打开已经构建好的 index6.html 文件,找到 script 元素,先注释原有代码,再添加如下代码:

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

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

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

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

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

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

层嵌套传参数的优化

基于 await 的特性,可以将异步请求的代码变成同步请求时的书写格式,代码会更加优雅,特别是处理多层需要嵌套传参时,使用 await 的方式,代码会更少,更易于阅读,如下列需求。

需求分析

需要发送三次异步请求,第一次请求,成功后获取返回 1,并将该值作为参数并加 2,发送第二次请求,成功后获取返回值,并将该值作为参数并加 3,发送第三次请求,成功后输出全部的返回值,如果三次请求都成功了,则在控制台输出 “登录成功!”的字样。

实现代码

首先,打开我们的线上环境,新建一个 index7.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  let p1 = await p("1");
  let p2 = await p(p1 + "2");
  let p3 = await p(p2 + "3");
  console.log(p3);
  console.log("登录成功!");
}
fn();

上述代码执行后的效果如下图所示:

从上述页面效果可以看出,在 fn 函数中,第一次发送请求时,返回值为 “1”,并保存在变量 p1 中,然后,将变量 p1 作为参数,并加 “2” 发送第二次请求,返回值为“12”,并保存在变量 p2 中,然后,将变量 p2 值作为参数,并加 ”3“ 发送第三次请求,返回值为 ”123“,并保存在变量 p3 中,最后,在控制台输出的内容是 p3 的值,即字符 “123”,同时,输出 “登录成功!”的字样。

多个并列异步请求的调优

await 在处理多个异步请求时,如果请求之间有嵌套关系,可以一次一次按顺序发送请求,但是如果各个请求之间无任何关联,则可以将这些请求借助 Promise.all 一次性并列发送,使用 await 关键字获取请求的结果,并根据返回的结果进行下一步操作。如下列需求。

需求分析

页面首次加载时,将会发送三次无任何关联的异步请求,当这三次请求成功后,在控制台输出“隐藏加载动画!”字样。

实现代码

为了实现这个功能,首先,打开我们的线上环境,新建一个 index8.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

// 函数 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 对象,都会以并行的方式发送异步请求,当所有请求都成功后,才会去执行输出字符内容的代码,许多初次学习 async 的同学们,可能会将 fn 函数的内容修改成如下代码所示:

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

需要说明的是,无论是函数修改之前还是修改之后 ,都使用了 async 和 await,并且两次的执行结果都是一样的,但在 fn 函数修改之前,所有的异步请求都是并行发送的,而在函数修改之后,所有的异步请求都是按顺序执行的。

从性能上来看,fn 函数修改之前的异步请求并发执行明显高于修改之后的阻塞式异步请求,因此,虽然,我们学习了 async 和 await ,但也不能完全取代 Promise 对象,需要结合实际需求场景去使用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值