浅谈Promise

236 阅读10分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

Promise

Promises 是异步编程的另一种选择,它的工作方式类似于在其他语言中延迟并在将来执行作 业。一个 Promise 指定一些稍后要执行的代码(就像事件与回调函数一样),并且也明确标 示了作业的代码是否执行成功。你能以成功处理或失败处理为基准,将 Promise 串联在一起,让代码更易理解、更易调试

异步编程的背景

js 的单线程是异步的原因

JS 引擎建立在单线程事件循环的概念上。单线程(Single-threaded )意味着同一时刻只能 执行一段代码,与 Java 或 C++ 这种允许同时执行多段不同代码的多线程语言形成了反差。 多段代码可以同时访问或修改状态,维护并保护这些状态就变成了难题,这也是基于多线程 的软件中出现 bug 的常见根源之一

事件循环(event loop )是 JS 引擎的一个内部处理线程,能监视代码的执行并管理作业队列(后面会讲到事件循环)

以往的异步编程模式

早期只支持定义回调函数来表明异步操作的完成,这样往往会嵌套多层回调,也就是俗称的回调地狱

  1. 异步操作的返回值
  • 首先,对于一个异步操作,在他完成后假设会返回一个有用的数据,那么怎么更好的获取并使用这个数据呢?我们可能首先会想到给这个异步操作提供一个回调,用于接收异步操作产生的返回值。
  • 其次,假如说这个异步操作,他也可能会失败,那么我们也需要对这个失败的结果做出反应,那么也需要传入一个回调用于处理失败的返回值。

所以:

/**
 * @param {*} value 传入的值
 * @param {*} success 一个用于成功状态的回调函数
 * @param {*} reject 一个失败状态的回调函数
 */
function isString(value, success, reject) {
  setTimeout(() => {
    if (typeof value === "string") {
      success(value);
    } else {
      reject(value);
    }
  }, 1000);
}

const onSuccess = (e) => {
  console.log(`success:${e}`);
};
const onReject = (e) => {
  console.log(`reject:${e}`);
};
// 调用
isString(6, onSuccess, onReject); //reject: 6

如果异步返回值又会依赖于另一个异步操作,那么可想而知这个回调将会非常的恶心、复杂。

Promise 基础

ES6 新增的引用类型 Promise,可以通过 new 操作符来实例化,但是创建一个新的Promise的时候必须传入一个执行器函数作为参数,否则的话会报错SyntaxError

执行器函数(是同步执行的)

Promise 的状态的改变都是私有的,需要在内部进行操作,而内部操作就是在执行器函数中完成,(我的理解就是这个执行器函数里面就是对这个任务对象的描述)执行器函数提供两个函数,通常命名为resolve()以及reject() 调用resolve()能够把状态改为 fullfilled,调用reject()能够把状态改为rejected,

let pro = new Promise((resolve, reject) => {});
console.log(pro); //Promise { <pending> }

任何一个异步场景都可以看作是一个 Promise 对象,一个 Promise 对象也可以看作是一个任务对象

Promise 的生命周期

每创建一个 Promise 就会产生一个任务对象,这个任务对象存在两个阶段三种状态

注意:

一个任务只能未决阶段->已决阶段,反之不可 任务的状态一经确定后续不可以改变

内部的 PromiseState 属性会被设置为 "pending" 、 "fulfilled" 或 "rejected" ,以 反映 Promise 的状态

  • 挂起状态->完成状态,任务成功可能会需要一个data,这个过程叫ersolve
  • 挂起状态->失败状态,任务失败可能会传出一个reason,这个过程叫regect

Promise API

then()

// 创建一个任务对象,该任务立即进入 pending 状态
const pro = new Promise((resolve, reject) => {
  // 任务的具体执行流程,该函数会立即被执行
  // 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
  // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});

pro.then(
  (data) => {
    // onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
  },
  (reason) => {
    // onRejected 函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
  }
);

当 Promisie 状态改变时,允许我们对 Promise 的返回值做出相应的操作 then()方法是 Promise 原型上的方法,任何由 Promise 实例化的 Promise 对象都拥有该方法。

then()任何传递的非函数类型的参数都会被静默忽略

  • 第一个参数是 Promise 被完成时要调用的函数,与异步操作关联的任何附加数据都会被传入这个完成函数。
  • 第二个参数 则是 Promise 被拒绝时要调用的函数,与完成函数相似,拒绝函数会被传入与拒绝相关联的任何附加数据。

这两个参数都是可选的

同时还提供了方法catch()相当于只给then()函数提供了拒绝的函数 以下代码的执行结果是一样的

let pro = new Promise((resolve, reject) => {
  reject("错误");
});
// 以下只捕捉错误信息
pro.then(null, (res) => {
  console.log(res); //错误
});
pro.catch((err) => {
  console.log(err); //错误
});

创建未决的 Promise

Promise 构造器就是创建未决的 Promise 的最好方式。这个构造器接受一个参数,一个被称为执行器函数的参数

创建已决的 Promise

有两种方法可使用指定值来创建已决的 Promise 。 其实就是在创建之初就已经确定了这个 Promise 的状态是完成状态 或 失败状态

  • 使用 Promise.resolve()方法 Promise.resolve() 方法接受单个参数并会返回一个处于完成态的 Promise 。这意味着没有 任何作业调度会发生,并且你需要向 Promise 添加一个或更多的完成处理函数来提取这个参 数值。例如:
let promise = Promise.resolve(42);
promise.then(function (value) {
  console.log(value); // 42
});
  • 使用Promise.reject()方法 方法和Promise.resolve()一样,唯一不同的地方就是,创建的 Promise 处于拒绝状态
let promise = Promise.resolve(42);
promise.catch(function (value) {
  console.log(value); // 42
});

若你传递一个 Promise 给 Promise.resolve() 或 Promise.reject() 方法,该 Promise 会不作修改原样返回。

Promise 链式调用

  1. then 方法必定会返回一个新的 Promise

    可理解为后续处理也是一个任务

  2. 新任务的状态取决于后续处理:

    • 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据

    • 若有后续处理但还未执行,新任务挂起。

    • 若后续处理执行了,则根据后续处理的情况确定新任务的状态

      • 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
      • 后续处理执行有错,新任务的状态为失败,数据为异常对象
      • 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致

then 方法必定会返回一个新的 Promise

处理场景1then()方法的后续处理仍然是一个新的任务

let pro1 = new Promise((resolve, reject) => {
  console.log("学习");
  resolve();
});
let pro2 = pro1.then(() => {
  console.log("考试");
});

console.log(pro2);
//学习
//Promise { <pending> }
//考试

新任务的状态取决于后续处理

处理场景2:若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据

let pro1 = new Promise((resolve, reject) => {
  console.log("学习");
  // 失败了
  reject();
});

// 后续处理没有对失败做下一步处理
let pro2 = pro1.then(() => {
  console.log("考试");
});

setTimeout(() => {
  console.log(pro1, pro2);
}, 100);
//学习
//Promise { <rejected> undefined } Promise { <rejected> undefined }
处理场景3:若有后续处理但还未执行,新任务挂起

let pro1 = new Promise((resolve, reject) => {
  console.log("学习");
  // 失败了
  setTimeout(() => {
    reject();
  }, 1000);
});

// 后续任务对失败做下一步处理,但是还未执行
let pro2 = pro1.catch(() => {
  console.log("复习,补考");
});

setTimeout(() => {
  console.log(pro1, pro2);
}, 100);
//学习
//Promise { <pending> } Promise { <pending> }
//复习,补考

若后续处理执行了,则根据后续处理的情况确定新任务的状态

打个比方就是,我们先是学习学习的任务完成了,随后我们考试考试这个新的任务的状态可能成功可能失败,所以新的任务的状态取决于这个后续处理的情况

处理场景4:后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值

let pro1 = new Promise((resolve, reject) => {
  console.log("学习");
  resolve(1);
});

let pro2 = pro1.then(() => {
  console.log("考试");
  return "考试顺利";
});

setTimeout(() => {
  console.log(pro2);
}, 100);
//学习
//考试
//Promise { '考试顺利' }
处理场景5:后续处理执行有错,新任务的状态为失败,数据为异常对象

let pro1 = new Promise((resolve, reject) => {
  console.log("学习");
  resolve(1);
});

// 后续任务对失败做下一步处理,但是还未执行
let pro2 = pro1.then(() => {
  console.log("考试");
  throw new Error("睡着了");
});

setTimeout(() => {
  console.log(pro2);
}, 100);
//学习
//考试
//Promise {<rejected> Error: 睡着了}
处理场景6:后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致

let pro1 = new Promise((resolve, reject) => {
  console.log("学习");
  resolve(1);
});

let pro2 = pro1.then(() => {
  return new Promise((resolve, reject) => {});
});

setTimeout(() => {
  console.log(pro2);
}, 100);
//学习
//Promise { <pending> }

由于链式任务的存在,异步代码拥有了更强的表达力

// 常见任务处理代码

/*
 * 任务成功后,执行处理1,失败则执行处理2
 */
pro.then(处理1).catch(处理2);

/*
 * 任务成功后,依次执行处理1、处理2
 */
pro.then(处理1).then(处理2);

/*
 * 任务成功后,依次执行处理1、处理2,若任务失败或前面的处理有错,执行处理3
 */
pro.then(处理1).then(处理2).catch(处理3);

Promise 的静态方法

方法名含义
Promise.resolve(data)直接返回一个完成状态的任务
Promise.reject(reason)直接返回一个拒绝状态的任务
Promise.all(任务数组)返回一个任务
任务数组全部成功则成功
任何一个失败则失败(找失败)
Promise.any(任务数组)返回一个任务
任务数组任一成功则成功(找成功)
任务全部失败则失败
Promise.allSettled(任务数组)返回一个任务
任务数组全部已决则成功
该任务不会失败
Promise.race(任务数组)返回一个任务
任务数组任一已决则已决,状态和其一致(具体的状态取决于,任务数组中第一个有结果的 Promise)

消除回调

有了 Promise,异步任务就有了一种统一的处理方式

有了统一的处理方式,ES 官方就可以对其进一步优化

ES7 推出了两个关键字asyncawait,用于更加优雅的表达 Promise

async

async 关键字用于修饰函数,被它修饰的函数,一定返回 Promise

async function method1() {
  return 1; // 该函数的返回值是Promise完成后的数据
}

method1(); // Promise { 1 }

async function method2() {
  return Promise.resolve(1); // 若返回的是Promise,则method得到的Promise状态和其一致
}

method2(); // Promise { 1 }

async function method3() {
  throw new Error(1); // 若执行过程报错,则任务是rejected
}

method3(); // Promise { <rejected> Error(1) }

await

await关键字表示等待某个 Promise 完成,它必须用于async函数中

async function method() {
  const n = await Promise.resolve(1);
  console.log(n); // 1
}

// 上面的函数等同于
function method() {
  return new Promise((resolve, reject) => {
    Promise.resolve(1).then((n) => {
      console.log(n);
      resolve(1);
    });
  });
}

await也可以等待其他数据

async function method() {
  const n = await 1; // 等同于 await Promise.resolve(1)
}

如果需要针对失败的任务进行处理,可以使用try-catch语法

async function method() {
  try {
    const n = await Promise.reject(123); // 这句代码将抛出异常
    console.log("成功", n);
  } catch (err) {
    console.log("失败", err);
  }
}

method(); // 输出: 失败 123