在前端的工作中从表面上来看无非就是两个要点,静态的页面结构和动态的页面数据。我们之前出了一套react全家桶的教程,而且在教程的开篇我们就已经有介绍过,react是一个只关注页面的js框架。就是说react框架只为我们解决页面层面的问题,但是数据从哪来呢?react并不关心。

我们大家都知道,现在的web页面大多都是前后端分离的,前端和后端都用数据来进行相应的交互。那么自然而然我们都知道,前端的动态数据都是由页面动态地向后端发送网络请求,然后由后端服务器返回相应请求所需要的相应的数据。也就是说我们的前端框架不关心我们怎么发送请求,那就代表着发送请求我们得自己来实现。

当然了,我们其实对发送请求都并不陌生了,ajax嘛,而且我们也知道发送ajax请求有很多种方法,比如:

  • 原生ajax请求
  • jQuery发送ajax请求
  • fetch发送ajax请求
  • axios发送ajax请求

不过现如今的市场上,前端用axios发送ajax请求这个方法应用的十分广泛。而axios是一个基于Promise的网络请求库。那么我们今天就来介绍一下Promise

什么是Promise

按照我们常规的思路来说,当我们拿到一个陌生的东西,第一件事是问一句,这玩意儿是什么?那么我们这里也一样,什么是Promise

抽象点来说呢,就是大家以及网上面很官方的回答:Promise是一个新技术,是js异步编程的新解决方案。这一句话我们仿佛并没法理解到什么东西,只知道是一个新的解决方案,但是这个解决方案是什么呢?我们知道异步编程的旧方案是单纯地使用回调函数,那么这个新方案是什么?

那么我们具体点来说,Promise从语法上来说其实就是一个构造函数可以进行对象的实例化。从功能上来说呢,这个Promise对象可以用来封装一个异步操作,并且可以获取这个异步操作的成功或者失败时的结果值。

异步编程

我们现在从抽象和具体层面都介绍了Promise是什么,但是可能大家还是有些迷糊,什么意思啊这一大串?我们知道了这个玩意儿是一个构造函数,既然是构造函数那就能实例化成一个对象,那么这个对象里面是什么呢?这个对象可以封装一个异步操作。

那么好,我们说到了这个东西是异步编程的解决方案,而且这个东西实例化的对象可以封装一个异步操作,这里出现了相同的词,异步。那么我们js中的常见的异步操作都有哪些呢?

  • fs文件操作
  • 数据库操作
  • ajax网络请求
  • 定时器

以上这些比如像fs文件操作:

require("fs").readFile("path", (error, data) => {});

这是一个简单的文件读取的demoreadFile的参数第一个是文件路径,第二个是一个回调函数。我们再来看ajax请求的demo

$.get("/path", (data) => {});

我们可以看到,这和文件读取差不多,get方法中的参数,第一个是url请求路径,第二个也是一个回调函数。

但是以上这两个demo中都是用回调函数来做的异步操作,这就是js异步编程的旧方案,单纯地在使用回调函数来完成一系列的异步操作。现在有了Promise这个新的解决方案,是不是就可以把这些异步操作都封装到Promise对象里呢?

为什么要使用Promise

我们知道了Promise是一个构造函数,可以封装异步操作,那么我们为什么要用Promise呢?我们就直接用回调函数也可以实现我们的异步编程啊。当然了,我们就得直接用回调函数来实现也是可以的。但是Promise自然是有一定的优势的。

  1. Promise支持链式调用,可以解决回调地狱的问题

什么意思呢?比如说,我们一个异步操作中的回调函数里如果要再次嵌套一个回调函数,而嵌套的回调函数中依然需要嵌套回调函数比如下面这个demo:

asyncFunc1(opt,(...args1) => {
  asyncFunc2(opt,(...args2) =>{
    asyncFunc3(op,(...args3) => {
      asyncFunc4(opt,(...args4) =>{
        //some operation });
      });
    });
  });
});

我们来看这个demo,一个异步回调里面嵌套了另一个异步操作,被嵌套的异步操作里面又嵌套了异步操作,这样的话我们是很不方便阅读代码的,而且这样的话我们代码如果有了异常也是很不方便处理的。因为我们每一层进入异步任务的时候我们都要对这个错误来进行一个相应的处理,可能会写很多重复性代码。

  1. 指定回调函数的方式更加灵活

怎么说呢?我们在用旧的异步编程方案的时候,在进行异步任务之前就得把回调函数给指定好。但是我们用Promise的话,我们可以先启动异步任务,返回一个Promise对象,然后再在后面给Promise对象来绑定回调函数,甚至我们还可以在异步任务之后来指定多个回调函数。

Promise初体验

上面说了这么多了,那么我们来尝试使用一下Promise吧,我们来看这样一个demo:

image-20220307111524158

我们这边有一个按钮,当我们点击按钮之后,延迟两秒之后弹窗提醒。我们先来用最原始的回调函数来实现以下:

<div class="container">
  <h2 class="page-header">Start Promise</h2>
  <button id="btn" class="btn btn-primary">Click</button>
</div>
<script>
  const btn = document.getElementById("btn");
  btn.addEventListener("click", () => {
    setTimeout(() => {
      alert("You clicked the button")
    }, 2000);
  });
</script>

我们来看这段代码,我们页面上有按钮,首先我们得先获取到按钮,然后添加点击事件,在点击事件中我们用定时器设置让弹窗延迟两秒之后再弹出。那么我们来看一下效果:

iShot2022-03-07 11.28.42

我我这边这个录制软件不太好用,但是也是可以看出来我们是点击之后等了一会儿然后才弹窗的。这便是我们用纯回调函数来完成的异步操作。那么我们接下来用Promise来进行一个封装应该怎么处理呢?

function rand(m, n) {
  return Math.ceil(Math.random() * (n - m + 1)) + m - 1;
}
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      let n = rand(1, 100);
      if (n <= 30) {
        resolve();
      } else {
        reject();
      }
    }, 2000);
  });

  p.then(
    () => { alert("You clicked the button") },
    () => { alert("Click the button failed") }
  );
});

我们来看上面这一段代码,我们说过Promise是一个构造函数,可以实例化成对象,那么我们在绑定点击事件的时候,我们实例化一个Promise对象,在我们实例化Promise对象要传一个参数,这个参数需要是一个函数类型的参数。而且这个函数要求接收两个函数作为参数,这两个函数分别是resolvereject。

这两个函数分别为异步任务成功与失败时所执行的函数。我们用随机数来模拟任务的成功与失败,当我们生成的随机数小于等于30时,我们认为异步任务是成功的,我们调用resolve函数,否则我们认为异步任务是失败的,我们调用reject函数。

这两个函数是有一个特点的,就是resolvereject函数在执行之后分别会将Promise对象的状态修改为成功或者失败。然后接下来我们调用Promise对象的then方法,而then方法会接收两个函数,并且会根据Promise对象的状态来判断执行哪个函数,如果状态为成功,则会执行第一个函数,否则会执行第二个函数。

那么我们来看一下效果:

iShot2022-03-07 15.08.06

我们看到我两次点击分别延迟了2秒而且两次点击一次成功一次失败。这就是Promise的基本流程:

  1. 实例化Promise对象
  2. 实例化Promise对象要接收一个函数类型的参数
  3. 该参数要接收两个函数resolvereject,异步任务成功执行resolve,失败执行reject,并且这两个函数会修改Promise对象的状态
  4. Promise对象包裹一个异步操作
  5. Promise对象调用then方法,并传入我们自定义的两个函数。
  6. then方法将通过Promise对象的状态来判断执行哪一个函数,如果状态为成功则执行第一个,否则执行第二个。

Promise获取异步任务的结果值

我在前文中提到,用Promise来处理异步操作问题是可以拿到异步任务成功或者失败的结果值的。那么怎么拿呢?我们用案例来说明一下,比如我现在还是之前的那个点击按钮,延迟两秒之后弹窗提醒,我们前文的案例中是根据我们生成的随机数来判断是成功还是失败,我们也成功实现了这个需求,但是我们现在进一步加一个需求,我们要在弹窗中显示出我们生成的随机数是多少。那么怎么处理呢?

btn.addEventListener("click", () => {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      let n = rand(1, 100);
      if (n <= 30) {
        resolve();
      } else {
        reject();
      }
    }, 2000);
  });

  p.then(
    () => { alert("You clicked the button") },
    () => { alert("Click the button failed") }
  );
});

我们先来回顾一下之前的代码,我们的随机数是在什么时候生成的?是不是在实例化Promise对象的时候,我们在我们的异步任务中声明定义了一个随机数n?那么我们是在什么时候弹窗的呢?是不是异步任务启动了之后,然后根据异步任务的成功与否执行了resolve或者reject函数?然后我们在通过Promise对象调用then方法的时候根据Promise对象的状态来执行我们传入的函数才弹出的窗口?

那么我们在then方法中定义的函数可以直接拿到实例化Promise对象时声明的n变量吗?是不是没法直接拿到?但是,我们知道Promise是可以拿到的,那么我们怎么处理呢?

btn.addEventListener("click", () => {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      let n = rand(1, 100);
      if (n <= 50) {
        resolve(n);
      } else {
        reject(n);
      }
    }, 2000);
  });

  p.then(
    (value) => { alert("You clicked the button " + value); },
    (reason) => { alert("Click the button failed " + reason); }
  );
});

我们来看这段代码,我们一个函数想要在函数体中拿到一个函数之外的变量是不是只有一个办法?那就是作为参数传进来。所以说我们要想在then方法接收到的函数中使用我们在其他地方声明的变量,那么这两个函数是不是就得预留一个参数来接收?

但是我们虽然预留了这个参数的位置了,那么谁会把数据传进来呢?这两个函数是传给Promise对象的then方法的,而且要根据Promise对象的状态来决定执行哪个函数,那么会不会这个数据就是要由Promise对象的状态传进来呢?我们来想一下,我们的随机数是在异步任务中声明的,就目前而言,这个异步任务不论成功与否,都会有且仅有一个特别的函数被执行,而且这个函数会修改Promise对象的状态,那么我们是不是只要把这个随机数传给这个能够修改Promise对象的状态的函数就行了呢?那么我们来看一下效果验证一下。

image-20220307155406779

我们看见我们成功在弹窗中展示了我们生成的随机数,也就代表着我们将结果值传给resolvereject函数,这两个函数中任意一个被执行之后,在调用then方法的时候,then方法中的函数都能接收到这个结果值。

以上便是Promise的初体验

总结

  • Promise可以封装一个异步操作
  • 实例化Promise对象
  • 实例化Promise对象要接收一个函数类型的参数
  • 该参数要接收两个函数resolvereject。
  • Promise对象包裹一个异步操作
  • 异步任务成功执行resolve,失败执行reject,这两个函数都可以接收异步任务执行的结果值,并修改Promise对象的状态
  • Promise对象调用then方法,并传入我们自定义的两个函数。
  • 我们定义的函数可以接收我们异步任务的结果值作为参数
  • then方法将通过Promise对象的状态来判断执行哪一个函数,如果状态为成功则执行第一个,否则执行第二个。

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/introduce_and_start/