Promise学习笔记(长文)【手写Promise;异步编程;Promise的API;异常穿透;链式调用;async和await】

本文详细介绍了Promise在JavaScript中的概念、应用场景、状态转换及为何使用,涵盖基本使用、封装异步操作、链式调用、异步回调处理、自定义Promise等内容,助你理解并高效运用Promise进行异步编程。
摘要由CSDN通过智能技术生成

文章目录

在这里插入图片描述

尚硅谷Promise笔记

参考课程:尚硅谷Web前端Promise教程从入门到精通(2021抢先版)_哔哩哔哩_bilibili

一、什么是Promise

1、对Promise的两种表达

  • 抽象表达:
  1. Promise 是一门新的技术(ES6 规范)
  2. Promise 是 JS 中进行异步编程的新解决方案,比传统的解决方案更合理和更强大。
    备注:旧方案是单纯使用回调函数
  • 具体表达:
  1. 从语法上来说:Promise 是一个构造函数
  2. 从功能上来说:Promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值

2、异步编程场景:

  1. fs(Node.js中的一个内置模块,可以操作本地文件)文件操作:

    // 使用回调函数的方式处理异步问题
    require('fs').readFile('./xxxx.txt', (err, data) => {});
    
  2. AJAX请求:

    // jQuery中的GET请求封装,使用回调函数的方式处理异步问题
    $.get('/xxxx', (data) => {});
    
  1. 定时器:

    // 使用回调函数的方式处理异步问题
    setTimeout(() => {}, 2000);
    
  2. 数据库操作

  3. ……

以上场景中均可以使用 Promise 代替回调函数的方式来处理异步编程问题。有关异步的概念,可到我的另一篇博客中查看相关描述:

参考:AJAX学习笔记_赖念安的博客-CSDN博客

3、Promise对象的三种状态

可以把 Promise 对象当成一个状态存储器,存储着某个事件的执行结果,当然,这个事件是当前没有发生的。当创建Promise对象时,Promise 对象的状态为 pending ——进行中/未决定;执行异步操作后,如果该操作的结果为成功,则调用 resolve() 函数,此时 Promise 对象状态变为 fulfilled ——已成功(或是叫做 resolved ——已定型/已解决);如果该操作的结果为失败,则调用 reject() 函数,此时 Promise 对象状态变为 rejected ——已失败。

阮一峰的《ES入门教程》中的描述:

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

实际上这三种状态都是Promise对象实例身上的一个属性 PromiseState 的值,该属性在不同的时候有相应的值。

Promise对象的三种状态的转换过程图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmnRBSCm-1629742202965)(https://i.loli.net/2021/08/21/2LZ5mb8XNlU9pC3.png)]

图片源自尚硅谷相关课程

二、为什么要用Promise

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

  1. 旧的: 必须在启动异步任务前指定
  2. promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定/多个)

2、支持链式调用, 可以解决回调地狱问题

根据上面的Promise状态变换的示意图,我们可以看到最后调用了 then() 函数中的回调函数后, then() 函数又会返回一个新的Promise对象。这就意味着我们可以紧接着再一次调用then()函数来处理这个新的Promise对象异步操作。于是我们就可以写像下面一样的代码:

let p = new Promise((resolve, reject) => {
  ……	// 异步操作
});

p.then((value) => {……	/* 操作成功后的回调 */}, (reason) => {……	/* 操作失败后的回调 */})
	.then((value) => {……}, (reason) => {……})
	.then((value) => {……}, (reason) => {……})

而使用传统的回调函数进行异步编程的话,就可能会在回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调执行的条件,也就是说在一个回调函数中嵌套另外一个异步任务,导致出现所谓的回调地狱

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RjQYlOoR-1629742202974)(https://i.loli.net/2021/08/21/lrpoQFOVRwKc4Tq.png)]

回调地狱

这样不但不利于阅读(缩进太过频繁),还不利于异常处理(可能会重复写某些异常处理的代码)。

三、Promise基本使用

调用Promise构造函数创建一个Promise类型的对象实例,并传入一个回调函数作为参数。这个回调函数中有 resolvereject 两个函数类型的参数,这两个函数由 JavaScript 引擎提供,不用自己部署,其名称可以自定义,但一般是叫做resolvereject

当异步操作成功时调用 resolve() 函数从而将Promise对象的状态由 pending 变为 fulfilled/resolved(成功),并将异步操作的结果 value 作为参数(其名称可以自定义,但一般是叫做 value)传递出去。

失败时则调用 reject() 函数从而将Promise对象的状态由 pending 变为 rejected(失败)。并将异步操作报出的错误原因 reason 作为参数(其名称可以自定义,但一般是叫做 reason)传递出去。

// 调用Promise构造函数创建一个Promise类型的对象,并传入一个回调函数作为参数
let promise = new Promise((resolve, reject) => {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(reason);
});

随后调用Promise对象的 then() 方法,传入的两个参数都是函数类型。其中第一个函数是作为异步操作成功时的回调;而第二个参数是作为异步操作失败时的回调。

promise.then((value) => {
  /* 异步操作成功(即:Promise对象状态为 fulfilled/resolved 的时回调 */
}, (reason) => {
  /* 异步操作失败(即:Promise对象状态为 rejected 时的回调 */
});

四、使用Promise封装异步操作

其实主要套路还是和上面的基本使用是一样的,把异步操作放在一个Promise构造函数内部的函数参数中,在失败时调用 reject() ,在成功时调用 resolve ,同时还可以传入相应的操作成功的结果和操作失败的原因。最后把整个Promise对象作为返回值返回。

1、封装Node.js中的fs文件操作模块

/**
 * 封装一个函数 myReadFile 读取文件内容
 * 参数:  path  文件路径
 * 返回:  promise 对象
 */

function myReadFile(path) {
  return new Promise((resolve, reject) => {
    require('fs').readFile(path, (error, data) => {
      if (error) {
        reject(error);
      }
      resolve(data);
    })
  });
}

myReadFile('./resource/content.txt')
.then(value => {
  console.log(value.toString());
}, reason => {
  console.warn(reason);
})

2、封装AJAX请求

/**
 * 封装一个函数 sendAJAX 发送 AJAX 请求
 * 参数  url  资源地址
 * 参数  method 请求方法
 * 返回结果 Promise 对象
 */

function sendAJAX(method, url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.send();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(xhr.response);
        }
        reject(xhr.status);
      }
    }
  });
}

sendAJAX('GET', 'https://api.apiopen.top/getJoke')
  .then(value => {
    console.log(value);
  }, reason => {
    console.log(reason);
  })

注意:Node.js环境中没有 XMLHttpRequest 对象,所以上述代码不能用 node 命令执行,否则会报【ReferenceError: XMLHttpRequest is not defined】这个错。

3、使用Node.js中的 util.promisify() 封装错误优先的异步操作

util模块是Node.js中的内置模块,util.promisify()函数可以将一个采用错误优先风格的回调函数包装成另一个函数,且包装后的函数的返回值为Promise类型。

官方文档中的描述:

采用遵循常见的错误优先的回调风格的函数(也就是将 (err, value) => ... 回调作为最后一个参数),并返回一个返回 promise 的版本。

参考:util.promisify() 实用功能

比如Node.js中fs模块的 readFile(path, (error, data) => {}) 函数就是典型的错误优先的异步操作。那么就可以通过下面的操作将 readFile() 函数包装为一个返回值为Promise对象的函数。

/**
 * util.promisify 方法
 */
//引入 util 模块
const util = require('util');
//引入 fs 模块
const fs = require('fs');
//返回一个新的函数
let mineReadFile = util.promisify(fs.readFile);

mineReadFile('./resource/content.txt').then(value=>{
  console.log(value.toString());
});

五、PromiseState与PromiseResult

1、PromiseState 存储的是Promise的状态(pending、fulfilled/resolved、rejected)

有关Promise状态的解释可参看前面第一部分【什么是Promise】中所讲的:【一-3、Promise对象的三种状态(按住Ctrl键单击即可跳转)】。

2、PromiseResult 存储的是异步操作的结果

无论异步操作是失败还是成功,都会生成一个结果,而这个结果就保存在Promise对象的 PromiseResult 属性中。只有 resolve(value)reject(reason) 这两个函数可以修改这个属性的值(其实就是将 PromiseResult 修改为这两个函数的参数: valuereason)。此后可以在 then() 函数的回调中读取这个属性的值。

2021年8月21日22:33:46-补充:突然发现在Edge浏览器【版本 80.0.361.62 (官方内部版本) (64 位)】中显示的不是像Chrome浏览器中的 PromiseStatePromiseResult ,而是PromiseStatusPromiseValue 。而且Edge中表示成功的状态为 resolved ,Chrome中表示成功的状态为 fulfilled 。但是这些都影响不大。

六、Promise中的几个API介绍

1、executor 执行器函数

执行器函数就是在创建Promise对象时往Promise构造函数传入的回调函数:(resolve, reject) => {}

其中,resolve() 函数是内部定义成功时我们调用的函数: value => {};而 reject 函数是内部定义失败时我们调用的函数:reason => {}

注意:executor 执行器会在 Promise 内部立即同步调用,异步操作在执行器中执行。这一点比较重要。

比如下面这段代码:

let promise = new Promise((resolve, reject) => {
  console.log('executor内部同步调用了');
});
console.log('Promise对象外部代码执行了');

// 输出结果
executor内部同步调用了
Promise对象外部代码执行了

2、then() 方法

then() 是在Promise原型上(Promise.prototype.then)的一个方法:(onResolved, onRejected) => {}

其中,onResolved 函数是成功时的回调函数:(value) => {};而 onRejected 函数是失败时的回调函数:(reason) => {}then() 方法返回一个新的 promise 对象。

有关其返回结果的几种情况的讨论,请到 【then() 方法的返回结果(按住Ctrl键单击即可跳转)】查阅。

3、catch() 方法

catch() 是在Promise原型上(Promise.prototype.catch)的一个方法:(onRejected) => {}

其中,onRejected 函数是失败时的回调函数:(reason) => {},相当于: then(undefined, onRejected)。其底层也是用then()封装的。像是阉割版的 then(),只能在promise对象的状态被改为了失败时触发回调。

let p = new Promise((resolve, reject) => {
  //修改 promise 对象的状态
  reject('error');
});

//执行 catch 方法
p.catch(reason => {
  console.log(reason);
});

// 输出结果
error

4、resolve() 方法

resolve() 是在Promise函数对象上(Promise.resolve)的一个方法:(value) => {}

其中,value 是成功状态下的数据或成功的promise对象。它返回的也是一个Promise类型的对象(但其状态可能是成功的也可能失败的)。

// 如果传入的参数为 非Promise类型的对象, 则返回的结果为成功promise对象
let p1 = Promise.resolve(521);
// p1此时的状态(PromiseState)为fulfilled,且结果(PromiseResult)为521

// 如果传入的参数为 Promise 对象, 则参数的结果决定了 resolve 的结果
let p2 = Promise.resolve(new Promise((resolve, reject) => {
  // resolve('OK');
  reject('Error');
}));
// 如果传入的promise对象的状态为成功,则p2的状态也为成功,且其结果为内部promise对象的结果值
// 如果传入的promise对象的状态为失败,则p2的状态也为失败,且其结果为内部promise对象的结果值

5、reject() 方法

reject() 是在Promise函数对象上(Promise.reject)的一个方法:(reason) => {}

其中,reason 是失败的原因。它返回的是一个状态为失败的Promise类型的对象。

Promise.resolve 不同,无论你传入什么类型的数据,它返回的都是失败状态的Promise对象,且你传入什么,则其返回的那个失败的Promise对象的结果也是什么。

// 无论你传入什么类型的数据,它返回的都是失败状态的Promise对象,
// 且该对象的结果(PromiseResult)就是我们传入的数据
let p = Promise.reject(521);
let p2 = Promise.reject('iloveyou');

// 就算你传入一个Promise对象,且把它的状态设置为成功,reject返回的Promise对象也是失败的,
// 且该对象的结果(PromiseResult)就是我们传入的那个Promise对象
let p3 = Promise.reject(new Promise((resolve, reject) => {
  resolve('OK');
}));

6、all() 方法

all() 是在Promise函数对象上(Promise.all)的一个方法:(promises) => {}

其中,promises 是一个包含若干promise对象的数组。它返回一个新的 promise,只有传入的 promises 数组中的所有 promise 都成功才成功, 只要有一个失败了就直接失败

而且如果所返回的promise对象是成功的,则其结果值是 promises 数组中的所有 promise 的结果值所组成的数组;如果所返回的promise对象是失败的,则其结果值是 promises 数组中的第一个状态为失败的promise对象的结果值。

// p1、p2、p3都是成功状态
let p1 = new Promise((resolve, reject) => {
  resolve('OK');
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');

// p4、p5都是失败状态
let p4 = Promise.reject('Error');
let p5 = Promise.reject('Oh NO');

// result1是promise类型的对象,且其状态(PromiseState)为fulfilled/resolved,
// 其结果(PromiseResult)为['OK','Success','Oh Yeah']
const result1 = Promise.all([p1, p2, p3]);

// result2是promise类型的对象,且其状态(PromiseState)为rejected,
// 其结果(PromiseResult)为 Error
const result2 = Promise.all([p1, p4, p5]);

image-20210821222938811

Chrome浏览器【Chromium 86.0.4240.198】中的显示结果

image-20210821222551865

Edge浏览器【版本 80.0.361.62 (官方内部版本) (64 位)】中的显示结果


注意到在Edge和Chrome浏览其中,对于promise对象状态和结果的描述有所差别,Chrome浏览器中的 `PromiseState` 和 `PromiseResult` ,对应Edge浏览器中的`PromiseStatus` 和 `PromiseValue` 。而且Edge中表示成功的状态为 `resolved` ,Chrome中表示成功的状态为 `fulfilled` 。

上图中,有一个报错【Uncaught (in promise) Error】,这是因为在上面的代码中我们没有给失败的情况添加对应的回调。

7、race() 方法

race() 是在Promise函数对象上(Promise.race)的一个方法:(promises) => {}

其中,promises 是一个包含若干promise对象的数组。它返回一个新的 promise,传入的 promises 数组中最先完成状态改变的Promise对象的状态和结果就作为这个返回的promise的状态和结果。就像数组中的几个Promise对象在赛跑一样,看谁先完成状态的转变,那么函数的返回结果就和谁一样。race这个单词也有竞赛的意思。

// p1、p2、p3都是成功状态
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK');
  }, 1000);
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');

// p4、p5都是失败状态
let p4 = Promise.reject('Error');
let p5 = Promise.reject('Oh NO');

// result1是promise类型的对象,且其状态(PromiseState)与p2一样,都为fulfilled/resolved,
// 其结果(PromiseResult)与p2一样,都为 Success
const result1 = Promise.race([p1, p2, p3]);

// result2是promise类型的对象,且其状态(PromiseState)与p4一样,都为rejected,
// 其结果(PromiseResult)与p4一样,都为 Error
const result2 = Promise.race([p1, p4, p5]);

七、几个关键问题

1、改变Promise状态的三种方式

此前,在讲 【一-3、Promise对象的三种状态(按住Ctrl键单击即可跳转)】时,阮一峰《ES6入门教程》中有这样一句话:只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。而能产生结果的操作就有三种。

1.1、resolve() 函数

在异步操作中主动调用 resolve() 函数,我们可以让当前的这个Promise对象由 pending 状态转为 fulfilled/resolved 状态。

1.2、reject() 函数

在异步操作中主动调用 reject() 函数,我们可以让当前的这个Promise对象由 pending 状态转为 rejected 状态。

1.3、用 throw 语句主动抛出异常

在异步操作中主动使用 throw 语句抛出异常,可以强制让当前的这个Promise对象由 pending 状态转为 rejected 状态。

let p = new Promise((resolve, reject) => {
  //1. resolve 函数
  // resolve('ok'); // pending   => fulfilled (resolved)
  //2. reject 函数
  // reject("error");// pending  =>  rejected 
  //3. 抛出错误,可以抛出一个错误信息字符串,也可以抛出一个Error对象
  throw '出问题了';
});
console.log(p);

// Edge中的控制台输出结果
Promise {<rejected>: "出问题了"}
__proto__: Promise
[[PromiseStatus]]: "rejected"
[[PromiseValue]]: "出问题了"

2、为一个Promise对象指定多个回调

为一个Promise对象指定多个成功或失败的回调时,这些回调函数在Promise对象改为相应的状态时都会被调用。

let p = new Promise((resolve, reject) => {
  resolve('OK');
});
// 多次调用then()方法给p指定多个成功的回调
// 指定回调 - 1
p.then(value => {
  console.log('回调函数1', value);
});

// 指定回调 - 2
p.then(value => {
  console.log('回调函数2', value);
});

// 控制台输出
回调函数1 OK
回调函数2 OK

3、Promise对象改变状态和指定相应的回调函数的先后顺序

Promise对象是先被改变状态,还是先被指定相应的回调函数?假设我们调用 resolve() 改变Promise状态并用 then() 指定相应的回调函数。那么问题就是:到底是 resolve() 先被调用,还是 then() 先被调用?答案是都有可能。

3.1、先改状态,再指定回调的情况

①在 executor 执行器的同步代码中直接调用 resolve()/reject()

let p = new Promise((resolve, reject) => {
  // 下面代码是同步的,而不是包裹在异步操作中的,
  // 此时Promise对象的状态先改为fulfilled,然后才在下方then()函数指定成功时的回调函数
  resolve('OK');
});

p.then(value => {
  console.log(value);
}, reason => {
  console.log(reason);
});

②把 resolve() 包裹在了异步操作中(比如定时器),但是同时也给 then() 函数加上一个等待时间更长的异步操作(比如一个时间更长的定时器)。

let p = new Promise((resolve, reject) => {
  // 下面代码是异步的,等待1秒后,Promise对象的状态改为fulfilled,
  // 而下方的then()函数才要等2秒后才执行,所以状态先被修改,然后才指定了相应的回调
  setTimeout(() => {
    resolve('OK');
  }, 1000);
});

setTimeout(() => {
  p.then(value => {
    console.log(value);
  }, reason => {
    console.log(reason);
  });
}, 2000);
3.2、先指定回调,再改状态的情况

resolve() 包裹在了异步操作中(比如定时器),但是 then() 函数是同步代码。

let p = new Promise((resolve, reject) => {
  // 下面代码是异步的,等待1秒后,Promise对象的状态改为fulfilled,
  // 而下方的then()函数是同步代码,所以先指定了相应的回调,然后状态才被修改
  setTimeout(() => {
    resolve('OK');
  }, 1000);
});

p.then(value => {
  console.log(value);
}, reason => {
  console.log(reason);
});
3.3、成功或失败的回调被执行且得到数据的时机(重点理解)

注意,上面讨论的都是指定成功或失败的回调的时机,也就是先告诉Promise对象在状态改变时该干嘛,而执行时机就是当Promise对象状态改变时要真正开始干之前指定的活儿了。那成功或失败的回调什么时候被执行并得到传递过来的数据呢?

① 如果先指定的回调,那当状态发生改变后,回调函数就会马上调用并得到数据。

② 如果先改变的状态,那在指定回调的同时,回调函数就会马上调用并得到数据。

4、then() 方法的返回结果

此前,我们讨论了 thne() 方法的两个函数类型的参数,它的返回结果也值得注意。

首先,then() 方法返回的是一个Promise对象,而这个对象的状态 PromiseState 和 结果 PromiseResultthen() 指定的回调函数执行的结果决定

可以类比此前提到的 Promise.all()Promise.race() 这两个API的返回结果。

4.1、then 的回调返回的是非Promise类型的任意值

如果 then() 中指定的回调函数返回的是非Promise类型的任意值,那么 then() 返回的新Promise(也就是下方代码中的result)的状态 PromiseState 变为 fulfilled/resolved, 其结果值 PromiseResult 则为 then() 中被调用的那个回调函数所返回的值。

let p = new Promise((resolve, reject) => {
  // p的状态和结果不会影响下面的result的状态和结果
  resolve('ok');
});

// 执行 then 方法
let result = p.then(value => {
  // then()中的回调函数返回的结果是非 Promise 类型的对象
  // result的状态 PromiseState 变为 fulfilled, 
  // result的结果值 PromiseResult 则为下面返回的 521
  return 521;
}, reason => {
  console.warn(reason);
});
console.log(result);

// 输出结果
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 521
4.2、then 的回调返回的是Promise类型的值

如果 then() 中指定的回调函数返回的是另一个新的Promise,则此Promise的状态和结果就会成为 then() 返回的新Promise(也就是下方代码中的result)的状态和结果。

let p = new Promise((resolve, reject) => {
  // p的状态和结果不会影响下面的result的状态和结果
  resolve('ok');
});

// 执行 then 方法
let result = p.then(value => {
  // then()中的回调函数返回的结果是 Promise 类型的对象
  return new Promise((resolve, reject) => {
    // result的状态 PromiseState 变为 fulfilled, 
  	// result的结果值 PromiseResult 则为 success
    // resolve('success');
    
    // result的状态 PromiseState 变为 reject, 
  	// result的结果值 PromiseResult 则为 error
    reject('error');
  });
}, reason => {
  console.warn(reason);
});

// 这是指定result的成功和失败时的回调,
// 如果不指定失败的回调的话,在Chrome中会因报错而无法输出result
result.then(value => {
  console.log(value);
}, reason => {
  console.log(reason);
})

console.log(result);

// 输出结果
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "rejected"
[[PromiseResult]]: "error"
4.3、then 的回调中用 throw 语句手动抛出一个异常

如果在 then() 指定的回调函数中用 throw 语句手动抛出一个异常,那么 then() 返回的新Promise(也就是下方代码中的result)的状态 PromiseState 变为 rejected, 其结果值 PromiseResult 则为抛出的异常字符串或是异常对象。

let p = new Promise((resolve, reject) => {
  // p的状态和结果不会影响下面的result的状态和结果
  resolve('ok');4});
	// 执行 then 方法
let result = p.then(value => {
  // then()中的回调函数中用 throw 语句手动抛出一个异常
  // result的状态 PromiseState 变为 reject, 
  // result的结果值 PromiseResult 则为 出问题了!
  throw '出问题了!'
}, reason => {
  console.warn(reason);
});
// 这是指定result的成功和失败时的回调,
// 如果不指定失败的回调的话,在Chrome中会因报错而无法输出result
result.then(value => {
  console.log(value);
}, reason => {
  console.log(reason);
})
console.log(result);
// 输出结果
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "rejected"
[[PromiseResult]]: "出问题了!"

5、Promise的链式调用

此前在讲述为什么要用Promise时,我们提到了一个重要的原因:【Promise支持链式调用(按住Ctrl键单击即可跳转)】。由于 then() 方法会返回一个新的Promise对象,所以我们可以紧接着再次对这个返回的Promise对象调用 then() 方法。而且根据我们刚刚对 then() 方法的返回结果的讨论,这个返回的Promise对象的结果是由 then() 指定的回调函数执行的结果所决定的。

那么当我们进行链式调用时,返回的Promise的状态和结果又是怎样的呢?

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK');
  }, 1000);
});

p.then(value => {
  return new Promise((resolve, reject) => {
    resolve("success");
  });
}).then(value => {
  // 控制台输出为 success,因为第一个then()函数返回的是一个成功的Promise对象且结果值为success
  console.log(value);
});

p.then(value => {
  return new Promise((resolve, reject) => {
    resolve("success");
  });
}).then(value => {
  // 控制台输出为 success,因为第一个then()函数返回的是一个成功的Promise对象且结果值为success
  console.log(value);
}).then(value => {
  // 控制台输出为 undefined,因为第二个then()函数中的回调函数中没有返回具体的值,
  // 则相当于是返回了 undefined,且返回的是一个成功的Promise对象
  // 所以第二个then()函数返回的Promise对象的结果值为 undefined
  console.log(value);
});

可以看到,当进行链式调用时,仍然严格按照此前总结的 【then() 方法返回的结果】中的那三种情况来返回Promise对象。

6、异常穿透

在进行链式调用时,可以在最后指定一个失败的回调。这样的话,如果前面的几个 then() 中出现了失败状态的Promise,并且都没有指定失败的回调的话,就会调用最后指定的这个失败的回调。最后所指定的这个失败回调就像是一个兜底的,要是前面的兄弟都不给力的话,最后就交给它来处理。

这个最后指定的失败回调,我们一般会用 catch() 方法指定失败的回调。

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK');
    // reject('Err');
  }, 1000);
});

p.then(value => {
  // console.log(111);
  throw '失败啦!';
}).then(value => {
  console.log(222);
}).then(value => {
  console.log(333);
}).catch(reason => {
  console.warn(reason);
});

// 输出结果
失败啦!

而且,失败的结果是逐层传递的,也就是说,如果在最后的 catch() 前已经有了处理失败的回调,那么最后的 catch() 中所指定的失败的回调就不会被调用。可以参阅下方的博客加深理解:

参考:测试分析promise异常穿透原理 - 17135131xjt - 博客园

7、中断Promise链

在下面的链式调用中,如果我们只想要输出111,应该怎么办?那就意味着得中断后续的链式调用。

中断链式调用有且只有一种方法:在 then() 的回调中返回一个状态为 pending 的Promise对象

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK');
  }, 1000);
});

p.then(value => {
  console.log(111);
  // 返回一个状态为 pending 的Promise对象以中断调用链
  return new Promise(() => {});
}).then(value => {
  console.log(222);
}).then(value => {
  console.log(333);
}).catch(reason => {
  console.warn(reason);
});

// 没有中断时的输出结果
111
222
333
// 中断后的输出结果
111

八、手写Promise

1、观察Promise对象的结构

仔细观察Promise对象的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CMgh2Rvr-1629742202992)(https://i.loli.net/2021/08/24/clhAPRE7S9BIUyY.png)]

Promise对象结构

2、分解思路

如果我们要手写代码实现Promise的功能,那么就要大概分这么几个模块:

①写一个名为Promise的构造函数。

②声明两个内置的变量:PromiseState(用来存储Promise对象当前的状态)和 PromiseResult (用来存储Promise对象的结果值)。

③给Promise构造函数传入一个 executor 执行器函数。并在Promise中对 executor 进行同步调用(此前在讲到 【六-1、executor执行器函数 (按住Ctrl键单击即可跳转)】时,提到了executor 执行器会在 Promise 内部立即同步调用)。同时也要给 executor 执行器函数传入两个函数类型的参数:resolvereject (函数名可以随意取,只要在调用时也用同样的名字即可)。

④在构造函数中声明两个函数 resolvereject ,分别传入一个数据(该数据将作为该Promise对象的结果值),并在函数体内完成对当前Promise实例对象的状态(即:PromiseState 属性)的改变和结果(即:PromiseResult 属性)的赋值。

⑤在讲 【七-1、改变Promise对象状态的三种方式(按住Ctrl键单击即可跳转)】时,除了④中提到的 resolvereject 两个函数可以改变Promise对象的状态,手动抛出异常也可以。所以我们应该给 executor 函数包裹 try...catch 语句块以捕捉错误信息,同时调用 reject 函数把状态改为 rejected ,并把错误信息作为参数传递给 reject 函数。

⑥因为Promise对象只允许修改一次状态,所以我们必须对 resolvereject 两个函数中对 PromiseState 属性的修改做出条件限制。

⑦当Promise对象状态被修改后,应该触发相应的回调函数,这是Promise实现异步的重要一环。也就是在实现 then 函数中的成功回调和失败回调。

处理 then() 函数的回调结果

⑨实现 catch 函数并处理异常穿透。

⑨分别实现Promise函数对象上的几个方法(resolve、reject、all、race)。

then() 中的回调函数进入异步队列

3、实现代码

3.1、声明Promise构造函数并设置两个内置对象
function Promise() {
  // 创建的Promise对象的默认状态应为 pending
  this.PromiseState = 'pending';
  this.PromiseResult = null;
}
3.2、给Promise构造函数传入 executor 执行器函数并同步调用
function Promise(executor) {
  // this.PromiseState = 'pending';
  // this.PromiseResult = null;

  executor();
}
3.3、声明 resolve 和 reject 函数,并传入 executor

executor 传入 resolvereject 函数。并事先在构造函数中声明这两个函数。

function Promise(executor) {
  // this.PromiseState = 'pending';
  // this.PromiseResult = null;

  // 注意这里特意用self变量保存当前Promise实例对象的this的指向
  // 是因为在用构造函数实例化Promise对象时,那里的this会指向window
  // 这就会导致调用resolve和reject函数时不能修改Promise实例上的两个属性
  let self = this;

  // resolve函数用于修改Promise对象的状态为fulfilled并赋予结果值
  function resolve(data) {
    self.PromiseState = 'fulfilled';
    self.PromiseResult = data;
  }

  // reject函数用于修改Promise对象的状态为rejected并赋予结果值
  function reject(data) {
    self.PromiseState = 'rejected';
    self.PromiseResult = data;
  }

  executor(resolve, reject);
}
3.4、处理实例化时抛出的错误

3.3中实现了用 resolvereject 函数改变Promise对象的状态,但是实例化时,如果在 executor 中抛出错误也应该改变状态,且应该改为 rejected。所以我们要在调用 executor 处进行错误的捕获和处理。

function Promise(executor) {
  /* this.PromiseState = 'pending';
  this.PromiseResult = null;
  let self = this;
  function resolve(data) {
    self.PromiseState = 'fulfilled';
    self.PromiseResult = data;
  }
  function reject(data) {
    self.PromiseState = 'rejected';
    self.PromiseResult = data;
  } */

  try {
    executor(resolve, reject);
  } catch (error) {
    reject(error);
  }
}
3.5、保证一个Promise对象状态只能被修改一次

此前,在讲 【一-3、Promise对象的三种状态(按住Ctrl键单击即可跳转)】时,阮一峰《ES6入门教程》中对Promise对象的特点做了两点描述,其中有一点就是:

Promise对象有以下两个特点。

……

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。

所以说,我们必须保证一个Promise对象状态只能被修改一次。也就是在修改Promise对象的 PromiseState 属性前,必须判断当前Promise对象的状态是否允许我们修改(也就是判断当前Promise对象的状态是否为 pending)。

function resolve(data) {
  // 只需要在resolve和reject函数中分别添加下面这个判断
  // 即可保证一个Promise对象的状态只能被修改一次
  if (self.PromiseState !== 'pending') {
    return;
  }
  self.PromiseState = 'fulfilled';
  self.PromiseResult = data;
}

function reject(data) {
  if (self.PromiseState !== 'pending') {
    return;
  }
  self.PromiseState = 'rejected';
  self.PromiseResult = data;
}
3.6、实现 then 函数中的成功回调和失败回调

then 函数的实现是Promise实现异步编程的重要一环,只有给状态转换指定相应的回调函数才能处理异步操作的结果。

首先观察我们用原生的Promise是如何使用 then 方法的:

let p = new Promise((resolve, reject) => {
  resolve('success');
});

p.then(value => {
  console.log(value);
}, reason => {
  console.log(reason);
})

我们需要给 then 函数传入两个函数类型的参数,同时这两个函数参数都会接收一个参数,且该参数的值是异步操作的结果值(也就是 PromiseResult 属性的值)。而且最关键的是,我们应该根据Promise对象的状态判断应该调用两个函数参数中的哪一个参数。

下方的 onResolved 是Promise对象状态为失败时的回调,而 onRejected 是成功时的回调。注意下面的 then 函数中使用了 this ,那为什么这里不需要像上面实现 resolvereject 函数时那样用 self 变量保存 this 的指向呢?仔细观察上面我们调用 then 时,是通过 p.then(...) 的形式,所以,then 函数中的 this 自然而然就会指向当前Promise实例对象。这点要好好理解。

Promise.prototype.then = function (onResolved, onRejected) {
  if(this.PromiseState === 'fulfilled') {
    onResolved(this.PromiseResult);
  }
  if(this.PromiseState === 'rejected') {
    onRejected(this.PromiseResult);
  }
}
3.7、在 executor 中执行异步操作

上面的实现中,我们自定义的Promise都能完成预期中的功能,但是一旦我们在 executor 执行器中执行异步操作,就无法获取预期中的结果。

比如在下面的代码中,我们在 executor 执行器中用一个2秒的定时器包裹 resolve() 函数,那么预期中,经过2秒后,我们在控制台中就可以看到输出结果为 success。但是使用我们自定义的Promise则不会出现预期结果,这是为什么呢?回顾我们在 【七-3-3.3成功或失败的回调被执行且得到数据的时机(重点理解)(按住Ctrl键单击即可跳转)】中所讨论的。那么下面的代码就是【先指定回调,再改变状态】的情况。

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success');
  }, 2000);
});

p.then(value => {
  console.log(value);
}, reason => {
  console.log(reason);
})

当我们的同步代码执行到了 p.then(...) 时,定时器中的代码还没有被执行,所以此时 p 的状态还没有被改为 fulfilled,而是仍然保持为初始的 pending。而在我们自定义的 then 函数中没有对 pending 状态的判断,自然就无法处理当前这个状态下的 p。

如果使用我们目前自定义的Promise,那么上面的代码运行流程大概就如下图所示:

image-20210822210648436

使用目前自定义的Promise的代码流程示意

于是乎,我们就应该考虑到底该在什么时候调用 then 函数中失败或成功的回调?根据下面的两条原则:

① 如果先指定的回调,那当状态发生改变后,回调函数就会马上调用并得到数据。

② 如果先改变的状态,那在指定回调的同时,回调函数就会马上调用并得到数据。

我们应该在状态改变时调用 then 中的回调,也就是在我们自定义的Promise构造函数中的 resolvereject 函数里调用。但是, then 中的回调是定义在 Promise.prototype 身上的,而 resolvereject 是直接定义在 Promise 函数对象上的,两者的作用域不同,所以在 resolvereject 函数里就无法调用 then 中的回调函数。此时需要then 中增加对 pending 状态的判断,并将 then 中的两个回调函数变量保存到Promise实例对象上。具体新增的实现如下:

function Promise(executor) {
  /* this.PromiseState = 'pending';
  this.PromiseResult = null;
	let self = this; */
  
  this.callback = null;
  
  function resolve(data) {
    /* if (self.PromiseState !== 'pending') {
      return;
    }
    self.PromiseState = 'fulfilled';
    self.PromiseResult = data; */
    
    // 注意这里也不能用this,因为这里的this指向的是window,
    // 在当前Promise对象的状态被改为了fulfilled时,
    // 应当调用当初指定的那个成功的回调:onResolved,这是为了应对异步任务
    if (self.callback) {
      self.callback.onResolved(data);
    }
  }

  function reject(data) {
    /* if (self.PromiseState !== 'pending') {
      return;
    }
    self.PromiseState = 'rejected';
    self.PromiseResult = data; */
    
    // 注意这里也不能用this,因为这里的this指向的是window,
    // 在当前Promise对象的状态被改为了rejected时,
    // 应当调用当初指定的那个成功的回调:onRejected,这是为了应对异步任务
    if (self.callback) {
      self.callback.onRejected(data);
    }
  }

  /* try {
    executor(resolve, reject);
  } catch (error) {
    reject(error);
  } */
}

Promise.prototype.then = function (onResolved, onRejected) {
  /* if (this.PromiseState === 'fulfilled') {
    onResolved(this.PromiseResult);
  }
  if (this.PromiseState === 'rejected') {
    onRejected(this.PromiseResult);
  } */
  if (this.PromiseState === 'pending') {
    this.callback = { onResolved, onRejected };
  }
}

所以,针对上面提到的那个例子,此时程序的执行流程就是:

在这里插入图片描述

上述例子的执行流程

3.8、为一个Promise对象指定多个回调时的情况

在 【七-2、为一个Promise对象指定多个回调(按住Ctrl键单击即可跳转)】中,我们知道:

当我们为一个Promise对象指定多个成功或失败的回调时,这些回调函数在Promise对象改为相应的状态时都会被调用。

而在上面的 3.7 中,我们只能保证最后的那个 then 函数中的回调会被执行,所以我们应该改进 3.7 中的代码(注意,有些相关性不大的代码我省略了,原来的写法被注释在改进代码的上方,注意对比):

function Promise(executor) {
  ......
  
  // this.callback = null;
  this.callbacks = [];
  
  function resolve(data) {
    ......
    // if (self.callback) {
    //   self.callback.onResolved(data);
    // }
    self.callbacks.forEach(item => {
      item.onResolved(data);
    })
  }

  function reject(data) {
    ......
    // if (self.callback) {
    //   self.callback.onRejected(data);
    // }
    self.callbacks.forEach(item => {
      item.onRejected(data);
    })
  }

  ......
}

Promise.prototype.then = function (onResolved, onRejected) {
  ......
  
  if (this.PromiseState === 'pending') {
    // this.callback = { onResolved, onRejected };
    this.callbacks.push({ onResolved, onRejected });
  }
}
3.9、处理 then 的返回结果

在 【七-4、then() 方法的返回结果(按住Ctrl键单击即可跳转)】中,我们讨论了 then 函数的返回结果的三种情况:①返回一个Promise类型的结果。②返回一个非Promise类型的结果。③用 throw 语句抛出一个错误。

首先就要在 then 函数中的最外层包裹一个 return new Promise(() => {...}); ,因为无论是上面提到的①②③哪种情况,then 函数返回的都是一个Promise类型的实例对象。大概就像这样:

// 这样就能保证then函数返回的一定是一个Promise类型的实例对象了。
Promise.prototype.then = function (onResolved, onRejected) {
  return new Promise((resolve, reject) => {
    ......
  });
}

然后把原先写在 then 中的代码放在上面的【…】处,稍后就是对这部分代码在原来的基础上进行修改。

3.9.1、executor 中执行的是同步代码
// executor 中执行的是同步代码
let p = new Promise((resolve, reject) => {
  resolve('success');
});

接下来就是分别判断 then 函数中的两个回调函数的返回结果的类型是不是Promise类型(也就是判断上面的①和②两种情况)。

首先用一个变量 result 分别接收 then 函数中的两个回调函数(onResolvedonRejected)的返回结果。看下方代码的【tag1】

Promise.prototype.then = function (onResolved, onRejected) {
  return new Promise((resolve, reject) => {
    if (this.PromiseState === 'fulfilled') {
      let result = onResolved(this.PromiseResult);	// 【tag1】
      ......	// 【tag2】
    }

    if (this.PromiseState === 'rejected') {
      let result = onRejected(this.PromiseResult);	// 【tag1】
      ......	// 【tag2】
    }

    if (this.PromiseState === 'pending') {
      this.callbacks.push({ onResolved, onRejected });
    }
  });
}

然后在上面的【tag2】处添加如下判断:

// 如果回调(onResolved或onRejected)的返回结果是Promise类型的,也就是情况①
if (result instanceof Promise) {
  // if (result.PromiseState === 'pending') return;	// 终止调用链
  // result.PromiseState === 'fulfilled' ? resolve(result.PromiseResult) : reject(result.PromiseResult);
  
  // 下面的这个then函数其实也可以用上面注释掉的两行代替,上面的写法更加语义化,下面的写法更巧妙
	// 因为在这个if判断里,result是一个Promise类型的对象,所以可以调用then
  result.then(value => {
    // 若result是一个成功的Promise,则调用最外层的那个Promise构造函数的resolve函数,
    // 使then函数最后返回的那个Promise对象的状态变为fulfilled,
    // 并将result的结果值传入,以作为then函数最后返回的那个Promise对象的结果值
    resolve(value);
  }, reason => {
    // 若result是一个失败的Promise,则调用最外层的那个Promise构造函数的reject函数,
    // 使then函数最后返回的那个Promise对象的状态变为rejected,
    // 并将result的结果值传入,以作为then函数最后返回的那个Promise对象的结果值
    reject(reason);
  });
} else {
  // 如果回调(onResolved或onRejected)的返回结果是非Promise类型的,也就是情况②
  // 此时直接调用最外层的那个Promise构造函数的resolve函数,
  // 使then函数最后返回的那个Promise对象的状态变为fulfilled,
  // 并将result的结果值传入,以作为then函数最后返回的那个Promise对象的结果值
  resolve(result);
}

注意,上面使用 result.then(...) 来设置最后返回的那个Promise对象的状态的写法中没有特意提到终止调用链的情况。

最后就是处理情况③了,其实只需要用一个 try...catch 语句包裹上面提到的【tag2】处的代码即可:

try {
  /* if (result instanceof Promise) {
    ......
  } else {
    ......
  } */
} catch (error) {
  // 如果回调(onResolved或onRejected)中用throw语句抛出了错误,也就是情况③
  // 那就直接调用最外层的那个Promise构造函数的reject函数,
  // 使then函数最后返回的那个Promise对象的状态变为rejected,
  // 并将捕获到的错误error传入,以作为then函数最后返回的那个Promise对象的结果值
  reject(error);
}
3.9.2、executor 中执行的是异步代码
// executor 中执行的是异步代码,比如设置一个定时器
let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success');
  }, 2000);
});

在 【八-3-3.7、在 executor 中执行异步操作(按住Ctrl键单击即可跳转)】中,我们讨论了当 executor 中执行的是异步操作时,相关的异步回调函数的执行流程所受到的影响,我们是通过【在 then 函数中添加对 pending 状态的判断并将 then 中的两个回调函数变量保存到Promise实例对象上】来解决的,但是对于 then 函数的返回结果,我们却没有做相应处理。具体可看下方图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MlEvSwzb-1629742203001)(https://i.loli.net/2021/08/24/4v18BYUQnPLASI9.png)]

使用目前自定义的Promise的代码流程示意,未对then函数的返回结果做相应处理

所以接下来我们就要针对 if (this.PromiseState === 'pending') {...} 这个判断里的代码做改进。

下方代码中【tag3】处的代码,和上面 3.9.2 中【tag2】处的代码是一样的。也是判断传入 then 中的两个回调函数的返回结果做是否为Promise对象的操作。

if (this.PromiseState === 'pending') {
  // this.callbacks.push({ onResolved, onRejected });
  // 因为要考虑到 then 的返回结果的状态和结果值,
  // 所以不能像上面那样直接将两个回调保存在当前Promise对象实例上,
  // 而要判断当前调用的回调函数的返回结果,从而决定 then 的返回结果的状态和结果值
  this.callbacks.push({
    onResolved: function () {
      try {
        // 注意这里不能用this,因为这里的this指向的是window,
        // 应当特意用一个self变量保存当前Promise实例对象的this的指向
        let result = onResolved(self.PromiseResult);
        ......	// 【tag3】
      } catch (error) {
        reject(error);
      }
    },
    onRejected: function () {
      try {
        // 注意这里不能用this,因为这里的this指向的是window,
        // 应当特意用一个self变量保存当前Promise实例对象的this的指向
        let result = onRejected(self.PromiseResult);
        ......	// 【tag3】
      } catch (error) {
        reject(error);
      }
    }
  });
}

这样的话,当我们设置的异步任务中的 resolvereject 函数被调用时,上面指定的相应回调也会被调用,从而给 then 返回的Promise对象设置相应的状态和结果值。

3.9.3、封装 then 中的重复性代码

写到这里,我们会发现上面 3.9.1 和 3.9.2 中重复使用了多次下面这样的代码结构:

try {
  // 只有接收回调函数的结果这一行有点不一样,xxxx代表onResolved或onRejected
  let result = xxxx(this.PromiseResult);
  
  if (result instanceof Promise) {
    result.then(value => {
      resolve(value);
    }, reason => {
      reject(reason);
    });
  } else {
    resolve(result);
  }
} catch (error) {
  reject(error);
}

then 中的三个 if 判断( if (this.PromiseState === 'fulfilled') {...}if (this.PromiseState === 'rejected') {...}if (this.PromiseState === 'pending') {...} )里总共有四处都用到了这样的结构,唯一不同的就是接收回调函数的结果的那一行,于是我们完全可以把上面的代码封装成一个函数,将其中需要灵活变化的部分抽象成一个函数参数。这样可以让代码变得更加简洁。

Promise.prototype.then = function (onResolved, onRejected) {
  let self = this;

  return new Promise((resolve, reject) => {
    // 封装重复性代码,函数的功能为设置 then 函数的返回值
    function setReturnValOfThen(callbackType) {
      try {
        // 注意这里不能用this,因为这里的this指向的是window,
        // 应当特意用一个self变量保存当前Promise实例对象的this的指向
        let result = callbackType(self.PromiseResult);
        // console.log('回调返回的结果', result);
        if (result instanceof Promise) {
          // if (result.PromiseState === 'pending') return;
          // result.PromiseState === 'fulfilled' ? resolve(result.PromiseResult) : reject(result.PromiseResult);
          // console.log('回调返回的是Promise对象');
          result.then(value => {
            resolve(value);
          }, reason => {
            reject(reason);
          });
        } else {
          // console.log('回调返回的不是Promise对象');
          resolve(result);
        }
      } catch (error) {
        reject(error);
      }
    }

    if (this.PromiseState === 'fulfilled') {
      setReturnValOfThen(onResolved);
    }

    if (this.PromiseState === 'rejected') {
      setReturnValOfThen(onRejected);
    }

    if (this.PromiseState === 'pending') {
      // this.callback = { onResolved, onRejected };
      // this.callbacks.push({ onResolved, onRejected });
      this.callbacks.push({
        onResolved: function () {
          setReturnValOfThen(onResolved);
        },
        onRejected: function () {
          setReturnValOfThen(onRejected);
        }
      });
    }
  });
}
3.10、实现 catch 函数和异常穿透

有关 catch 函数的内容可以到【六-3】中回顾;有关异常穿透的内容可到【七-6】中回顾。

3.10.1、实现 catch 函数

首先,catch 函数和 then 函数一样,都是Promise原型上的一个方法。

另外,此前,我们提到 catch 函数的底层其实也是用 then 函数封装的,只要给 then 函数的第一个参数传入 undefined ;然后把传入 catch 函数中的回调函数当做 then 函数的第二个参数即可。

Promise.prototype.catch = function (onRejected) {
  return this.then(undefined, onRejected);
}
3.10.2、实现异常穿透

目前我们的代码还不能实现异常穿透的功能。

let p = new Promise((resolve, reject) => {
    resolve('OK');
});

let result1 = p.then(value => {
  // console.log(111);
  throw '失败啦!';
})
let result2 = result1.then(value => {
  console.log(222);
})
let result3 = result2.then(value => {
  console.log(333);
})
let result4 = result3.catch(reason => {
  console.warn(reason);
});

// 根据异常穿透的特性,预期结果应该是:在控制台输出 失败啦
// 但实际上却是报出错误:TypeError: callbackType is not a function

使用原生Promise的话,上面各个结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pgw9uEO0-1629742203004)(https://i.loli.net/2021/08/24/yP9LmUwJrixn3jI.png)]

原生Promise的结果

而使用我们自定义的Promise的话,结果如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-08NgJTLZ-1629742203008)(https://i.loli.net/2021/08/24/kZgNnVr8tyGbKjS.png)]

自定义Promise的结果

观察可知,其实上面的链式调用中,在 result1.then(...) 处就有问题了,因为我们没有给这个 then 传入失败时的回调。因为 result1 是一个失败状态的Promise对象,所以当我们调用 result1.then(...) 时,其实是会调用失败的回调,也就是调用 result1.then(...) 中的第二个参数,但是我们没有传入,所以相当于是默认传入了一个 undefined,所以当我们调用 result1.then(...) 时,就相当于是 undefined(...) ,很明显,undefined 不是一个函数类型的数据,所以会报类型错误:【TypeError: callbackType is not a function】。

要解决这个问题,就要求我们处理 then 函数的默认参数问题,也就是处理当我们给 then 传入的 onResovledonRejected 参数不是函数类型时的情况。

Promise.prototype.then = function (onResolved, onRejected) {
  // let self = this;

  // 添加下面两句就可以保证尽管我们给then函数传入的两个参数不是函数类型
  // 但仍然可以保证后续需要调用相关回调时让返回的Promise对象有正确的状态和结果值
  onResolved = typeof onResolved === 'function' ? onResolved : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

  /* return new Promise((resolve, reject) => {
    // 封装重复性代码,函数的功能为设置 then 函数的返回值
    function setReturnValOfThen(callbackType) {
      ......
    }

    if (this.PromiseState === 'fulfilled') {
      setReturnValOfThen(onResolved);
    }

    if (this.PromiseState === 'rejected') {
      setReturnValOfThen(onRejected);
    }

    if (this.PromiseState === 'pending') {
      ......
    }
  }); */
}
3.11、实现 resolve 函数

此前,在 【六-4、resolve() 方法(按住Ctrl键单击即可跳转)】中我们讨论了Promise函数对象身上的 resolve 方法。

首先,我们知道 resolve() 是在Promise函数对象上(Promise.resolve)的一个方法,且其返回值一定是一个Promise对象。

其次,我们知道如下规则:

①如果传入的参数为非Promise类型的对象, 则函数返回的结果为成功的Promise对象。

②如果传入的参数为Promise对象(假设为p),若p的状态为成功,则函数返回的Promise对象状态也为成功,且其结果为p的结果值;若p的状态为失败,则函数返回的Promise对象状态也为失败,且其结果为p的结果值。

Promise.resolve = function (value) {
  return new Promise((resovle, reject) => {
    // 如果传入resolve的值为Promise类型的对象,
    // 则根据这个传入的对象的状态和结果值决定resolve函数所返回的Promise对象的状态和结果值
    if (value instanceof Promise) {
      value.then(val => {
        resovle(val);
      }, reason => {
        reject(reason);
      });
    } else {
      // 如果传入resolve的值为Promise类型的对象,
      // 则直接将resolve函数所返回的Promise对象的状态设为fulfilled,且结果值即为传入的值
      resovle(value);
    }
  });
}
3.12、实现 reject 函数

此前,在 【六-5、reject() 方法(按住Ctrl键单击即可跳转)】中我们讨论了Promise函数对象身上的 reject 方法。

首先,我们知道 reject() 是在Promise函数对象上(Promise.reject)的一个方法,且其返回值一定是一个Promise对象。

其次,我们知道如下规则:

①无论传入的参数是否为Promise类型的对象,,reject 函数返回的结果均为失败的Promise对象。

②reject 函数返回的结果均为失败的Promise对象的值就是我们传入的那个参数的值。

Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason);
  });
}
3.13、实现 all 函数

此前,在 【六-6、all() 方法(按住Ctrl键单击即可跳转)】中我们讨论了Promise函数对象身上的 all 方法。

首先,我们知道 all() 是在Promise函数对象上(Promise.all)的一个方法,且其返回值一定是一个Promise对象。

其次,我们知道如下规则:

①传入的参数是一个包含若干Promise对象的数组,只有传入的数组中的所有Promise对象都成功才成功, 只要有一个失败了就直接失败。

②如果所返回的Promise对象是成功的,则其结果值是数组中的所有Promise对象的结果值所组成的数组。

③如果所返回的Promise对象是失败的,则其结果值是数组中的第一个状态为失败的Promise对象的结果值。

Promise.all = function (promiseArr) {
  // 计数器,用来记录promiseArr中的成功的Promise的个数
  let count = 0;
  // promiseResult用于存储最终返回的Promise对象的结果值
  let promiseResult = [];
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i].then(value => {
        // 如果能进这个回调函数,说明当前遍历的Promise对象是成功的
        count++;
        promiseResult[i] = value;
        // 如果count累加到了promiseArr数组的长度,
        // 说明promiseArr中的所有Promise对象都是成功的
        if (count === promiseArr.length) {
          resolve(promiseResult);
        }
      }, reason => {
        // 如果当前遍历的Promise对象状态为失败,则立即把返回值的状态改为rejected
        reject(reason);
      });
    }
  });
}
3.14、实现 race 函数

此前,在 【六-7、race() 方法(按住Ctrl键单击即可跳转)】中我们讨论了Promise函数对象身上的 race 方法。

首先,我们知道 race() 是在Promise函数对象上(Promise.race)的一个方法,且其返回值一定是一个Promise对象。

其次,我们知道如下规则:

①传入的参数是一个包含若干Promise对象的数组,只有传入的数组中最先完成状态改变的Promise对象的状态和结果就作为 race 函数返回的Promise的状态和结果。

Promise.race = function (promiseArr) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promiseArr.length; i++) {
      // 如果下面的then被调用了,说明当前遍历的Promise对象是最先完成状态转变的
      // 所以可以立马把返回值的状态改为相应的值并同时设置结果值
      promiseArr[i].then(value => {
        resolve(value);
      }, reason => {
        reject(reason);
      });
    }
  });
}
3.15、让 then 中的回调函数是异步执行的

观察下列代码:

let p1 = new Promise((resolve, reject) => {
  resolve('OK')
  // reject('ERROR');
  console.log(111);
});

p1.then(value => {
  console.log(222);
}, reason => {
  console.log(444);
});

console.log(333);
// 原生Promise的输出结果
111
333
222

// 我们目前自定义的Promise的输出结果
111
222
333

出现上面的差异的原因就是原生Promise是让 then 中的回调函数异步执行的,而我们自定的Promise是让其同步执行的。要达到让回调函数异步执行的效果,可以用一个定时器包裹 then 中所要执行的回调函数(也就是让相关的回调函数进入事件循环中的队列等待同步任务执行完毕后再执行)。

共有四处需要修改,都是真正调用 then 中的回调函数的时刻:

// then 函数中的两处
Promise.prototype.then = function (onResolved, onRejected) {
  /* let self = this;
  ......
  return new Promise((resolve, reject) => {
    function setReturnValOfThen(callbackType) {
      ......
    } */

    if (this.PromiseState === 'fulfilled') {
      setTimeout(() => {
        setReturnValOfThen(onResolved);
      });
    }

    if (this.PromiseState === 'rejected') {
      setTimeout(() => {
        setReturnValOfThen(onRejected);
      });
    }

    /* if (this.PromiseState === 'pending') {
      ......
    } */
  });
}
// executor 中的 resolve 和 reject 函数
function resolve(data) {
  /* if (self.PromiseState !== 'pending') {
    return;
  }
  self.PromiseState = 'fulfilled';
  self.PromiseResult = data; */
  setTimeout(() => {
    self.callbacks.forEach(item => {
      item.onResolved(data);
    })
  });
}

function reject(data) {
  /* if (self.PromiseState !== 'pending') {
    return;
  }
  self.PromiseState = 'rejected';
  self.PromiseResult = data; */
  setTimeout(() => {
    self.callbacks.forEach(item => {
      item.onRejected(data);
    })
  });
}

九、async 和 await

1、async 函数

官方MDN文档参考:async函数 - JavaScript | MDN

async 关键字修饰的函数有以下特点:

  1. 该函数的返回值为Promise对象

  2. 返回的Promise对象的结果由 async 函数执行的返回值决定,其中决定的规则和 then 函数中的回调函数的决定规则一样:

    ①如果 async 函数中返回的是非Promise类型的任意值,那么调用该函数时所生成的Promise对象的状态 PromiseState 变为 fulfilled/resolved, 其结果值 PromiseResult 则为 async 函数中所返回的值。

    ②如果 async 函数中返回的是另一个新的Promise(假设为p),则 p 的状态和结果就会成为调用该函数时所生成的那个Promise对象的状态和结果。

    ③如果在 async 函数中用 throw 语句手动抛出一个异常,那么调用该函数时所生成的Promise对象的状态 PromiseState 将变为 rejected, 其结果值 PromiseResult 则为抛出的异常字符串或是异常对象。

2、await 表达式

官方MDN文档参考:await - JavaScript | MDN

await 关键字修饰的表达式有以下特点:

  1. await 关键字必须写在 async 函数中,否则会报错:【Uncaught SyntaxError: await is only valid in async function】;而 async 函数中不一定要有 await 表达式。

  2. await 关键字右侧的表达式有三种情况:

    ①如果表达式是成功的Promise对象,await 返回的是该Promise对象的结果值。

    ②如果表达式是失败的Promise对象,await 会报错:【Uncaught (in promise) XXXX】,可以用 try...catch 语句包裹该 await 表达式,则 catch 语句中所捕获到的错误即为那个失败的Promise对象的结果值。

    ③如果表达式是非Promise对象的任意值,await 返回的就是该任意值。

3、async 和 await 的配合

asyncawait 的配合下,我们可以在不使用回调函数的情况下就完成某些异步操作(比如读文件或是发送AJAX请求等等),完成这些异步任务时就像是在写同步的代码(实际上的异步操作是在 await 内部完成的),比较简洁易读。

有关参考

更新:2021年8月21日18:33:46

参考:尚硅谷Web前端Promise教程从入门到精通(2021抢先版)_哔哩哔哩_bilibili

【Promise相关在线文档】

参考:使用 Promise - JavaScript | MDN

参考:Promise - JavaScript | MDN

参考:Promise 对象 - ES6 教程 - 网道

参考:AJAX学习笔记_赖念安的博客-CSDN博客

参考:浅谈js中的回调地狱问题_良子的博客-CSDN博客_回调地狱

参考:util.promisify() 实用功能

更新:2021年8月22日14:27:59

参考:测试分析promise异常穿透原理 - 17135131xjt - 博客园

更新:2021年8月24日01:09:38

【async和await】

参考:async函数 - JavaScript | MDN

参考:await - JavaScript | MDN

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值