Promise学习笔记

学习来源:尚硅谷

笔记参考:努力学习的汪

0 补充知识

0.1 回调函数的例子

关于回调的基础知识,可在ajax学习笔记中查看。

回调函数定义:

1
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
  • 异步请求的回调函数
1
2
3
4
5
6
7
$.get('test.html', function(data) {
alert('Load was performed');
});
// 箭头函数形式:
$.get('test.html', (data) => {
alert('Load was performed');
});
  • 点击事件的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
btn.addEventListener('click', function() {
// 定时器
setTimeout(() => {
let n = rand(1, 100);
if(n <= 30) {
alert('中奖');
} else {
alert('没中奖,再接再厉');
}
}, 1000);
});
// 箭头函数形式
btn.addEventListener('click', () => {
// 定时器
setTimeout(() => {
let n = rand(1, 100);
if(n <= 30) {
alert('中奖');
} else {
alert('没中奖,再接再厉');
}
}, 1000);
});

回调与同步、异步并没有直接的联系,回调只是一种实现方式,既可以有同步回调,也可以有异步回调,还可以有事件处理回调和延迟函数回调,这些在我们工作中有很多的使用场景

所以其实并不是我们不认识回调函数,而是我们都萦绕在了这个“callback“ 这个词上,当你在一个函数中看到它就会困惑,其实它只是一个形参名字而已。

0.2 js中的同步任务和异步任务

0.2.1 回调函数和异步机制

我们都知道js是单线程的,这种设计模式给我们带来了很多的方便之处,我们不需要考虑各个线程之间的通信,也不需要写很多烧脑的代码,也就是说js的引擎只能一件一件事的去完成和执行相关的操作,所以所有需要执行的事情都像排队一样,等待着被触发和执行,可是如果这样的话,如果在队列中有一件事情需要花费很多的时间,那么后面的任务都将处于一种等待状态,有时甚至会出现浏览器假死现象,例如其中有一件正在执行的一个任务是一个死循环,那么会导致后续其他的任务无法正常执行,所以js在同步机制的缺陷下设计出了异步模式。

在异步执行的模式下,每一个异步的任务都有其自己一个或着多个回调函数,这样当前在执行的异步任务执行完之后,不会马上执行事件队列中的下一项任务,而是执行它的回调函数,而下一项任务也不会等当前这个回调函数执行完,因为它也不能确定当前的回调合适执行完毕,只要引它被触发就会执行。

js的单线程浏览器内核的多线程

image-20220209112924355

浏览器常驻三大线程: js引擎线程,GUI渲染线程,浏览器事件触发线程

浏览器是一个多线程的执行环境,在浏览器的内核中分配了多个线程,最主要的线程之一即是js引擎的线程,同时js事件队列中的异步请求,交互事件触发,定时器等事件都是由浏览器的事件触发线程进行监听的,浏览器的事件触发线程被触发后会把任务加入到js引擎的任务队列中,当js引擎空闲时候就会开始执行该任务。

0.2.2 js的任务

1) 概述

所有的任务分为两种,一种是同步任务,一种是异步任务。

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

  • 异步任务指的是,不进入主线程,而进入”任务队列“(task queue)的任务,只有等主线程任务执行完毕,”任务队列”开始通知主线程,请求执行任务,该任务才会进入主线程执行。

主要的异步任务有

  • Events:javascript各种事件的执行都是异步任务
  • setTimeout、setInterval 定时器 特别的如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数
  • queueMicrotask 执行微任务
  • XMLHttpRequest(也就是 Ajax)
  • requestAnimationFrame 类似于定时器,但是是以每一帧为单位
  • fetch Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分
  • MutationObserver 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用。
  • Promise
  • async function

这里说到了一个“队列”(即任务队列),该队列放的是什么呢,放的就是setTimeout中的function,这些function依次加入该队列,即该队列中所有function中的程序将会在该队列以外的所有代码执行完毕之后再以此执行,这是为什么呢?因为在执行程序的时候,浏览器会默认认为setTimeout以及ajax请求这一类的方法都是耗时程序(尽管可能不耗时),将其加入一个队列中,该队列是一个存储耗时程序的队列,在所有不耗时程序执行过后,再来依次执行该队列中的程序。

具体来说,异步运行机制如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件

(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

例如,setTimeOut函数为异步任务,for循环为同步任务,setTimeOut里的函数为回调函数。执行顺序为:同步优先,异步靠边,回调垫底。所以即使setTimeOut的时间参数是0依然会放到任务队列里,而不是主线程。主线程执行完for循环以后才执行异步任务setTimeOut。另外setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function a() {
console.log('执行a函数');
setTimeout(() => {
console.log('执行a函数的延迟函数');
}, 0);
}

function b() {
console.log('执行b函数');
}

a();
b();

// 执行结果
执行a函数
执行b函数
执行a函数的延迟函数

这里a和b为同步任务,会按照顺序优先执行,setTimeout为异步任务,只有等待执行栈中的任务全部执行完毕后,js线程才会执行该任务。

2) js单线程

javascript是单线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。于是就有一个概念——任务队列。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。于是JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成这门语言的核心特征,将来也不会改变。

注:所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。

  • js是同步的?是的,单线程,那肯定只能同步(排队)执行咯

  • js为什么需要异步?如果JS中不存在异步,只能自上而下执行,万一上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着”卡死”,这样就导致了很差的用户体验

1 promise理解和使用

1.1 概念和特点

Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

通俗讲,Promise是一个许诺、承诺,是对未来事情的承诺,承诺不一定能完成,但是无论是否能完成都会有一个结果。

  • Pending 正在做。。。
  • Resolved 完成这个承诺
  • Rejected 这个承诺没有完成,失败了

Promise 用来预定一个不一定能完成的任务,要么成功,要么失败。在具体的程序中具体的体现,通常用来封装一个异步任务,提供承诺结果。

Promise 是异步编程的一种解决方案,主要用来解决回调地狱的问题,可以有效的减少回调嵌套。真正解决需要配合async/await

特点

  1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。

优点

  1. 指定回调函数的方式更加灵活
    1. 旧的方法:必须在启动异步任务前指定
    2. promise:启动异步任务 => 返回promise对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定多个)
  2. 支持链式调用,可以解决回调地狱问题

缺点

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。和一般的对象不一样,无需调用。
  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3. 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

总结

  1. Promise 是一门新的技术(ES6 规范)
  2. Promise 是 JS 中进行异步编程的新解决方案 备注:旧方案是单纯使用回调函数
  3. 从语法上来说: Promise 是一个构造函数
  4. 从功能上来说: promise 对象用来封装一个异步操作并可以获取其成功/ 失败的结果值

1.2 promise初体验

1.2.1 封装setTimeout异步任务

需求:点击按钮,1s后显示是否中奖(30%概率中奖),弹出相应提示框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>promise初体验</title>
<link crossorigin="anonymous" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h2 class="page-header">promise初体验</h2>
<button class="btn btn-primary" id="btn">点击抽奖</button>
</div>
<script>
// 生成随机数
function rand(m, n) {
return Math.ceil(Math.random() * (n-m+1)) + m -1;
}
// 获取元素对象
const btn = document.getElementById('btn');
// 绑定点击事件
btn.addEventListener('click', () => {
// 定时器
setTimeout(() => {
let n = rand(1, 100);
if(n <= 30) {
alert('中奖');
} else {
alert('没中奖,再接再厉');
}
}, 1000);
});

</script>
</body>
</html>
  • Promise实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
btn.addEventListener('click', () => {
// promise形式实现
// resolve 解决 函数类型的数据
// reject 拒绝 函数类型的数据
const p = new Promise((resolve, reject) => {
// 包裹异步操作setTimeout
// 定时器
setTimeout(() => {
let n = rand(1, 100);
if(n <= 30) {
resolve(); // 将promise状态设置为 成功
} else {
reject(); // 将promise状态设置为 失败
}
}, 1000);
});

// 调用then方法
p.then(() => {
alert('中奖');
}, () => {
alert('没中奖,再接再厉');
});
});

执行结果:

image-20220209120149566

image-20220209120159438

添加需求

显示提示时,输出选中的号码。

无论变为成功还是失败,都会有一个结果数据。成功的结果数据一般称为 value, 失败的结果数据一般称为 reason,分别作为resolve和reject的形参。

解决:将n作为参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 绑定点击事件
btn.addEventListener('click', () => {
// promise形式实现
// resolve 解决 函数类型的数据
// reject 拒绝 函数类型的数据
const p = new Promise((resolve, reject) => {
// 包裹异步操作
// 定时器
setTimeout(() => {
let n = rand(1, 100);
if(n <= 30) {
resolve(n); // 将promise状态设置为 成功
} else {
reject(n); // 将promise状态设置为 失败
}
}, 1000);
});

// 调用then方法
p.then((value) => {
alert('中奖,您的中奖号码为' + value);
}, (reason) => {
alert('没中奖,再接再厉,您的号码为' + reason);
});
});

执行结果:

image-20220209120417833

1.2.2 封装fs读取文件异步任务

需求:读取项目文件夹下的test.txt的文件内容。

  • 回调函数形式
1
2
3
4
5
6
7
8
const fs = require('fs');

fs.readFile('./test.txt', (err, data) => {
if(err) {
throw err;
}
console.log(data.toString());
});

image-20220209121030447

  • promise形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');
// promise形式
const p = new Promise((resolve, reject) => {
fs.readFile('./test.txt', (err, data) => {
if(err) {
reject(err);
}
resolve(data);
});
});

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

执行结果:

image-20220209121344765

1.2.3 封装ajax异步任务

需求:向某个地址发送ajax请求,获取响应内容

  • 回调函数形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>promise 封装 ajax请求</title>
<link crossorigin="anonymous" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h2 class="page-header">promise初体验</h2>
<button class="btn btn-primary" id="btn">发送ajax请求</button>
</div>
<script>
// 接口地址 https://api.apiopen.top/getJoke
// 获取元素对象
const btn = document.getElementById('btn');
// 绑定点击事件
btn.addEventListener('click', () => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.apiopen.top/getJoke');
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300) {
// 控制台输出响应体
console.log(xhr.response);
} else {
// 控制台输出响应状态码
console.log(xhr.status);
}
}
}
});
</script>
</body>
</html>
  • promise形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 绑定点击事件
btn.addEventListener('click', () => {
// 创建promise对象
const p =new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.apiopen.top/getJoke');
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300) {
// 控制台输出响应体
resolve(xhr.response);
} else {
// 控制台输出响应状态码
reject(xhr.status);
}
}
}
});
p.then(value => {
console.log(value)
}, reason => {
console.log(reason)
});
});

执行结果:

image-20220209122408990

1.2.4 封装自定义异步操作

1) fs封装

需求:封装一个函数 myReadFile 读取文件内容

  • 参数: path 文件路径
  • 返回: promise 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myReadFile(path) {
return new Promise((resolve, reject) => {
require('fs').readFile(path, (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}

myReadFile('test.txt').then(value => {
console.log(value.toString());
}, reason => {
console.log(reason);
});

image-20220209123019664

2) ajax封装

需求:封装一个函数 sendAjax 发送GET请求

  • 参数: URL
  • 返回结果: Promise对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function sendAjax(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300) {
// 控制台输出响应体
resolve(xhr.response);
} else {
// 控制台输出响应状态码
reject(xhr.status);
}
}
}
});
}

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

1.3 util.promisify风格转化

util.promisify传入一个遵循常见的错误优先的回调风格的函数(即以(err, value) => ...)回调作为最后一个参数,并返回一个promise的版本。

  • 作用:将回调函数风格的异步函数,转化为promise风格的异步函数。可以将函数直接变成promise的封装方式,不用再去手动封装。

例如将fs.readFile函数转化:

1
2
3
4
5
6
7
8
9
10
11
12
// 引入util模块
const util = require('util');
// 引入fs模块
const fs = require('fs');
// 返回一个新的函数
let myReadFile = util.promisify(fs.readFile);

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

1.4 promise的状态和结果

image-20220209125642120

promise实例对象中的一个属性PromiseState,表示该实例对象的状态。

PromiseState有三种取值:

  • pending 未决定的
  • resolved / fullfilled 成功
  • rejected 失败

promise 的状态改变只有两种:

  1. pending => resolved
  2. pending => rejected

说明: 只有这2种状态改变,且一个 promise 对象只能改变一次


promise实例对象中的一个属性PromiseState,表示该实例对象成功或失败的结果。

只有resolvereject这两个函数才能修改该属性。

1.5 promise的工作流程

image-20220209125931941

1.6 promise常用API

1.6.1 构造函数

Promise 构造函数: Promise (excutor) {}

  • executor 函数: 也称为执行器,函数形式:(resolve, reject) => {},其中:

    • resolve 函数: 内部定义,成功时我们调用的函数 value => {}

    • reject 函数: 内部定义,失败时我们调用的函数 reason => {}

说明: executor 会在 Promise 内部立即同步调用,是作为同步任务,而不是作为回调函数的形式(异步任务),异步操作在执行器中执行,换句话说Promise支持同步也支持异步操作。即:执行器本身是作为同步操作,执行器内部的操作作为异步操作。

示例

1
2
3
4
5
6
let p =new Promise((resolve, reject) => {
// 同步调用
console.log('111');
});

console.log('222');

执行结果:

image-20220209130945662

1.6.2 then方法

Promise.prototype.then 方法:原型 (onResolved, onRejected) => {},其中:

  • onResolved 函数: 成功的回调函数 (value) => {}

  • onRejected 函数: 失败的回调函数 (reason) => {}

说明: 指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调。返回一个新的 promise 对象

1.6.3 catch方法

Promise.prototype.catch 方法: 原型(onRejected) => {}

  • onRejected 函数: 失败的回调函数 (reason) => {}

    • 说明: then()的语法糖, 相当于: then(undefined, onRejected)
  • 异常穿透使用:当运行到最后,没被处理的所有异常错误都会进入这个方法的回调函数中

示例

1
2
3
4
5
6
7
8
let p =new Promise((resolve, reject) => {
// 修改promise对象状态
reject('error');
});

p.catch(reason => {
console.log(reason);
});

image-20220209131754985

1.6.4 resolve方法

Promise.resolve 方法: (value) => {},属于函数对象,而不是实例对象

  • value: 成功的数据或 promise 对象

  • 返回一个成功/失败的 promise 对象,直接改变promise状态

  • 传入参数:
    • 如果为非promise类对象,则返回的结果为成功的promise对象
    • 如果为promise类对象,则参数的结果决定了resolve的结果

示例

1
2
3
4
5
6
7
8
// 注意创建形式
let p1 = Promise.resolve(521);
let p2 = Promise.resolve(new Promise((resolve, reject) => {
// 设置参数的状态为失败
reject('error');
}));
console.log(p1);
console.log(p2);

执行结果:

image-20220209132953756

reject方法

Promise.reject 方法: (reason) => {}

  • reason: 失败的原因

说明: 返回一个失败的 promise 对象,直接改变promise状态,代码示例同上

1.6.5 all方法

Promise.all 方法: (promises) => {}

  • promises: 包含 n 个 promise 的数组

  • 返回一个新的 promise, 只有所有的 promise 都成功才成功, 只要有一个失败了就直接失败

示例

1
2
3
4
5
let p1 = new Promise((resolve, reject) => { resolve('success');  });
let p2 = Promise.reject('error');
let p3 = Promise.resolve('success');
const result = Promise.all([p1, p2, p3]);
console.log(result);

image-20220209133855099

1.6.6 race方法

Promise.race 方法: (promises) => {}

  • promises: 包含 n 个 promise 的数组
  • 返回一个新的 promise, 第一个完成的 promise 的结果状态就是最终的结果状态

示例

如p1延时,开启了异步,内部正常是同步进行,所以p2>p3>p1,结果是P2

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
}, 1000);
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');
//调用
const result = Promise.race([p1, p2, p3]);
console.log(result);

image-20220209142028433

1.7 promise的几个关键问题

①如何改变 promise 的状态?

  1. resolve(value): 如果当前是 pending 就会变为 resolved
  2. reject(reason): 如果当前是 pending 就会变为 rejected
  3. 抛出异常: 如果当前是 pending 就会变为 rejected

②一个 promise 指定多个成功/失败回调函数, 都会调用吗?

当 promise 改变为对应状态时都会调用,改变状态后,多个回调函数都会调用,并不会自动停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p = new Promise((resolve, reject) => {
// 改变状态为成功
resolve('OK');
});

// 指定回调 - 1
p.then(value => {
console.log(value);
});

// 指定回调 - 2
p.then(value => {
alert(value);
});

执行结果:

image-20220211192407278

③改变 promise 状态和指定回调函数谁先谁后?

  • 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调

    • 先指定回调再改变状态(异步):先指定回调—> 再改变状态 —>改变状态后才进入异步队列执行回调函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      let p = new Promise((resolve, reject) => {
      //异步写法,这样写会先指定回调,再改变状态
      setTimeout(() => {
      resolve('OK');
      }, 1000);
      });

      p.then(value => {
      console.log(value);
      });
    • 先改状态再指定回调(同步):改变状态 —>指定回调 并马上执行回调

      1
      2
      3
      4
      5
      6
      7
      8
      let p = new Promise((resolve, reject) => {
      //这是同步写法,这样写会先改变状态,再指定回调
      resolve('OK');
      });

      p.then(value => {
      console.log(value);
      });
  • 如何先改状态再指定回调? 注意:指定并不是执行

    • 在执行器中直接调用 resolve()/reject() —>即,不使用定时器等方法,执行器内直接同步操作
    • 延迟更长时间才调用 then() —>即,在.then()这个方法外再包一层例如延时器这种方法
  • 什么时候才能得到数据?

    • 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据
    • 如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据

注:源码中,promise的状态是通过一个默认为padding的变量进行判断,所以当你resolve/reject延时(异步导致当then加载时,状态还未修改)后,这时直接进行p.then()会发现,目前状态还是进行中,所以只是这样导致只有同步操作才能成功。

所以promise将传入的回调函数拷贝到promise对象实例上,然后在resolve/reject的执行过程中再进行调用,达到异步的目的。

④promise.then()返回的新 promise 的结果状态由什么决定?

  • 简单表达: 由 then()指定的回调函数执行的结果决定

  • 详细表达:

    • 如果抛出异常,新 promise 变为 rejected,reason 为抛出的异常

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      let p = new Promise((resolve, reject) => {
      resolve('ok')
      });

      let res = p.then(value => {
      throw 'error';
      }, reason => {
      console.warn(reason);
      });
      console.log(res);

      image-20220211200817044

    • 如果返回的是非 promise 的任意值,新 promise 变为 resolved,value 为返回的值

      1
      2
      3
      4
      5
      let res = p.then(value => {
      return 1;
      }, reason => {
      console.warn(reason);
      });

      image-20220211200858171

    • 如果返回的是另一个新 promise,此 promise 的结果就会成为新 promise 的结果

      1
      2
      3
      4
      5
      6
      7
      let res = p.then(value => {
      return new Promise((resolve, reject) => {
      reject('error!!!');
      });
      }, reason => {
      console.warn(reason);
      });

      image-20220211200955952

⑤promise 如何串连多个操作任务?

  • promise 的 then()返回一个新的 promise,可以看成 then()的链式调用
  • 通过 then 的链式调用串连多个同步/异步任务,这样就能用then()将多个同步或异步操作串联成一个同步队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok')
}, 1000);
});

p.then(value => {
console.log(value);
return new Promise((resolve, reject) => {
resolve('success!')
});
}).then(value => {
console.log(value)
});

第一个回调函数then与p实例对象进行绑定;p返回一个新的promise对象,它(没有名字)与第二个回调then进行绑定,故输出value值为success

执行结果:

image-20220211201826131

如果再添加一个then回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok')
}, 1000);
});

p.then(value => {
console.log(value)
return new Promise((resolve, reject) => {
resolve('success!')
});
}).then(value => {
console.log(value)
}).then(value => {
console.log(value);
});

执行结果:

image-20220211202034588

注意,第二个回调then也返回一个新的promise对象,对于第二个回调then本身,返回的是非 promise 的值undefined(js函数没有指定return时,默认返回undefined),因此,新的promise对象与第三个then绑定,其value为返回的undefined

⑥promise 异常传透?

  • 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调
  • 前面任何操作出了异常,都会传到最后失败的回调中处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok')
}, 1000);
});

p.then(value => {
console.log(111);
throw 'Error!!!';
}).then(value => {
console.log(222)
}).then(value => {
console.log(333);
}).catch(reason => {
console.log(reason);
});

执行结果:

image-20220211202733163

注:可以在每个then()的第二个回调函数中进行err处理,也可以利用异常穿透特性,到最后用catch去承接统一处理,两者一起用时,前者会生效(因为err已经将其处理,就不会再往下穿透)而走不到后面的catch。

⑦中断 promise 链?

关键问题2中,可以得知,当promise状态改变时,他的链式调用都会生效,那如果我们有这个一个实际需求:我们有3个then(),但其中有条件判断,如当我符合或者不符合第三个then条件时,要直接中断链式调用,不再走下面的then,该如何?

办法:在回调函数中返回一个 pendding 状态的promise 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok')
}, 1000);
});

p.then(value => {
console.log(111);
// 中断promise链
// 只有一种方式,即返回pending状态的promise对象
return new Promise(() => {});
}).then(value => {
console.log(222)
}).then(value => {
console.log(333);
}).catch(reason => {
console.log(reason);
});

执行结果:

image-20220211203244358

原因:由关键问题4可知,第一个then返回的新promise对象为then函数的返回结果(即新new出来的promise对象),且与第二个then回调进行绑定,而这个promise对象状态为pending,因此无法执行第二个then回调,所以链条就中断了。

2 自定义Promise

2.1 Promise的实例方法实现

2.1.1 初始结构搭建

  • 新建html页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>promise 封装</title>
<script src="./promise.js"></script>
</head>
<body>
<script>
let p = new Promise((resolve, reject) => {
resolve('ok');
});

p.then(value => {
console.log(value);
}, reason => {
console.warn(reason);
});
</script>
</body>
</html>

注意,这里的Promise通过引入我们自己定义的promise.js来全局覆盖原来内置的Promise。

  • 新建promise.js
1
2
3
4
5
6
7
8
9
// 声明构造函数Promise
function Promise(executor) {

}

// 添加then方法
Promise.prototype.then = function(onResolved, onRejectd) {

}

这样基本框架就搭建好了,运行没有报错。

2.1.2 resolve和reject实现

  • 使用const self = this;保存this执行,使function中可以取得当前实例

ps:可以不使用该方法保存,但是下方function需要改为箭头函数,否则function默认指向是window

之后代码默认使用self保存this,箭头函数方式将在最后改为class写法时使用

  • 默认设置 PromiseState = 'pending'以及 PromiseResult = 'null',这就是promise状态基础
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 声明构造函数
function Promise(executor) {
// 添加属性
this.PromiseState = 'pending';
this.PromiseResult = null;
// 保存实例对象的this值
const self = this;
function resolve(data) {
// 1.修改对象的状态(promiseState)
self.PromiseState = 'fulfilled';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
}

function reject(data) {
// 1.修改对象的状态(promiseState)
self.PromiseState = 'rejected';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
}

// 同步调用执行器函数
executor(resolve, reject);
}

测试结果:成功时

image-20220211211542751

2.1.3 throw 抛出异常改变状态

  1. 在2.1.2的基础上进行修改:将执行器放入try-catch()
  2. 在catch中使用reject()修改 promise 对象状态为『失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 声明构造函数
function Promise(executor) {
// 添加属性
this.PromiseState = 'pending';
this.PromiseResult = null;
// 保存实例对象的this值
const self = this;
function resolve(data) {
// 1.修改对象的状态(promiseState)
self.PromiseState = 'fulfilled';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
}

function reject(data) {
// 1.修改对象的状态(promiseState)
self.PromiseState = 'rejected';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
}
// 捕获异常
try {
// 同步调用执行器函数
executor(resolve, reject);
} catch(e) {
// 修改promise状态为失败
reject(e);
}

}

测试代码:

1
2
3
4
let p = new Promise((resolve, reject) => {
throw 'error';
});
console.log(p);

执行结果:

image-20220211212006718

2.1.4 状态只能修改一次

  1. 基于2、3代码中resolve和reject方法进修改
  2. 在成功与失败函数中添加判断if(self.PromiseState !== 'pending') return;,如果进入函数时状态不为pending直接退出,这样就能做到状态只能从pending改至其他状态且做到只能改一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function resolve(data) {
// 判断状态
if(self.PromiseState !== 'pending') return;
// 1.修改对象的状态(promiseState)
self.PromiseState = 'fulfilled';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
}

function reject(data) {
// 判断状态
if(self.PromiseState !== 'pending') return;
// 1.修改对象的状态(promiseState)
self.PromiseState = 'rejected';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
}

测试代码:

1
2
3
4
5
6
let p = new Promise((resolve, reject) => {
resolve('success');
reject('error');
});
console.log(p);
// 最后的状态应该为成功

image-20220211212428789

2.1.5 then方法执行基础回调

  1. 修改Promise.prototype.then方法
  2. 传入then(成功回调,失败回调),当调用then后,会判断当前this.PromiseState的状态,当其为成功时调用成功回调,失败时调用失败回调
1
2
3
4
5
6
7
8
9
let p = new Promise((resolve, reject) => {
resolve('success');
reject('error');
});
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);
});
1
2
3
4
5
6
7
8
9
10
11
12
// 添加then方法
Promise.prototype.then = function(onResolved, onRejectd) {
// 调用回调函数
// 这里的then是被p对象调用,因此this指向p
if(this.PromiseState === "fulfilled") {
onResolved(this.PromiseResult);
}

if(this.PromiseState === "rejected") {
onRejectd(this.PromiseResult);
}
}

执行结果:

image-20220214113238115

2.1.6 异步任务 then 方法实现

问题:当第五小节的then运行异步代码后,执行器内部代码还未返回(因为用了定时器,里面的代码进入了异步队列),所以当下面的.then()运行时:ppending状态,所以根本不会执行resolve与reject方法。

解决:在then中添加判断pending状态,将当前回调函数保存到实例对象(存到实例上是为了更方便)中,这样后续改变状态时候才调用得到。

测试代码:

1
2
3
4
5
6
7
8
9
10
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success');
}, 1000);
});
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);
});

promise.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 声明构造函数
function Promise(executor) {
// 添加属性
this.PromiseState = 'pending';
this.PromiseResult = null;
// 用于保存回调函数
this.callback = {};
// 保存实例对象的this值
const self = this;
function resolve(data) {
// 判断状态
if(self.PromiseState !== 'pending') return;
// 1.修改对象的状态(promiseState)
self.PromiseState = 'fulfilled';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
// 当状态改变时,调用成功的回调函数
if(self.callback.onResolved) {
self.callback.onResolved(data);
}
}

function reject(data) {
// 判断状态
if(self.PromiseState !== 'pending') return;
// 1.修改对象的状态(promiseState)
self.PromiseState = 'rejected';
// 2.设置对象结果值(promiseResult)
self.PromiseResult = data;
// 当状态改变时,调用失败的回调函数
if(self.callback.onRejectd) {
self.callback.onRejectd(data);
}
}
// 捕获异常
try {
// 同步调用执行器函数
executor(resolve, reject);
} catch(e) {
// 修改promise状态为失败
reject(e);
}

}

// 添加then方法
Promise.prototype.then = function(onResolved, onRejectd) {
// 调用回调函数
if(this.PromiseState === "fulfilled") {
onResolved(this.PromiseResult);
}

if(this.PromiseState === "rejected") {
onRejectd(this.PromiseResult);
}

// 判断pending的状态(异步时:如果状态还未改变)
if(this.PromiseState === "pending") {
// 保存回调函数 重点
this.callback = {
onResolved: onResolved,
onResolved: onRejectd
}
}
}

2.1.7 指定多个回调

第六小节中保存回调函数的方式有BUG,如果有多个.then(),后面加载的回调函数会覆盖之前的回调函数,导致最后回调函数有且只有最后一个。

例如测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success');
}, 1000);
});
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);
});

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

执行结果:

image-20220214122010465

解决:使用数组的方式进行存储回调函数,调用时也是用数组循环取出

构造函数中:

1
2
3
4
5
6
this.callbacks = [];
// ...
// 当状态改变时,调用失败的回调函数
self.callbacks.forEach(item => {
item.onRejectd(data);
});

then方法中:

1
2
3
4
5
6
7
8
// 判断pending的状态
if(this.PromiseState === "pending") {
// 保存回调函数
this.callbacks.push({
onResolved: onResolved,
onRejectd: onRejectd
});
}

执行结果:

image-20220214122158517

2.1.8 同步任务then返回结果

  • 在之前的then运行结果中得知,我们使用 [ then ] 后的返回结果是其回调函数的返回结果,而我们需要的返回结果是一个新的promise对象

    • 解:所以我们在then中return new Promise(),使其得到的是一个新的promise对象
  • 在为解决问题1后产生一个新问题:新的promise对象因为没有用rejerect与resolve方法,导致返回的状态一直是pending

    • 解:在新的promise中判断运行回调函数后的返回值是什么,然后根据其不同类型给其赋予不同状态
    • 即 回调函数中的promise 赋值给then返回值,所以 最终返回状态==回调函数中的新promise状态
    • 如果返回值是一个非promise对象,返回状态设置为成功
    • 如果返回值是一个异常,返回状态设置为失败
  • 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let p = new Promise((resolve, reject) => {
resolve("OK");
});

const res = p.then(value => {
// 同步任务

// 1.返回undefined
// console.log(value);

// 2.返回字符串
// return "Hello World";

// 3.返回新的promise对象,由res接收
// return new Promise((resolve, reject) => {
// resolve("new Promise OK");
// });

// 4.抛出异常
throw "FAIL";
}, reason => {
console.log(reason);
});

console.log(res);
  • promise.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 添加then方法
Promise.prototype.then = function(onResolved, onRejectd) {
// 返回一个新的promise对象
return new Promise((resolve, reject) => {
// 调用回调函数
if(this.PromiseState === "fulfilled") {
try{
// 获取回调函数的执行结果
let result = onResolved(this.PromiseResult);
if(result instanceof Promise) {
// 如果是promise对象
result.then(v => {
resolve(v);
}, r => {
reject(r);
});
} else {
// 结果的对象状态为 成功
resolve(result);
}
} catch(e) {
reject(e);
}
}

if(this.PromiseState === "rejected") {
try{
// 获取回调函数的执行结果
let result = onRejectd(this.PromiseResult);
if(result instanceof Promise) {
// 如果是promise对象
result.then(v => {
resolve(v);
}, r => {
reject(r);
});
} else {
// 结果的对象状态为 事变
reject(result);
}
} catch(e) {
reject(e);
}
}

// 判断pending的状态
if(this.PromiseState === "pending") {
// 保存回调函数
this.callbacks.push({
onResolved: onResolved,
onRejectd: onRejectd
});
}
});
}

2.1.9 异步任务then返回结果

2.1.10 then方法代码优化

2.1.11 catch方法与异常穿透与值传递

2.2 Promise的静态方法实现

2.2.1 Promise.resolve 封装

2.2.2 Promise.reject 封装

2.2.3 Promise.all 封装

2.2.4 Promise.race封装

2.3 其他优化

2.3.1 回调函数异步执行

2.3.2 class改写promise

3 async和await

  • promise —> 异步
  • await —> 异步转同步

    • await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。
    • await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果等的是正常的表达式则立即执行
  • async —> 同步转异步

    • 方法体内部的某个表达式使用await修饰,那么这个方法体所属方法必须要用async修饰所以使用awit方法会自动升级为异步方法

3.1 async函数

  1. 函数的返回值为 promise 对象
  2. promise 对象的结果由 async 函数执行的返回值决定,和then返回的结果相同
1
2
3
4
5
6
7
8
9
async function main() {
return new Promise((resolve, reject) => {
resolve("success");
});
};

let result = main();

console.log(result);

执行结果:

image-20220214124838330

3.2 await表达式

  1. await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
  2. 如果表达式是 promise 对象, await 返回的是 promise 成功的值
  3. 如果表达式是其它值, 直接将此值作为 await 的返回值

注意:

  1. await 必须写在 async 函数中, 但 async 函数中可以没有 await
  2. 如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try…catch 捕获处理

示例

  • await 右侧的表达式一般为 promise 对象,但也可以是其它的值,如果表达式是 promise 对象,await 返回的是 promise 成功的值
1
2
3
4
5
6
7
8
9
10
async function main() {
let p = new Promise((resolve, reject) => {
resolve("success");
});

let res = await p;
console.log(res); // success
};

main();
  • 如果表达式是其它值,直接将此值作为 await 的返回值
1
2
3
4
5
6
async function main() {
let res = await 20;
console.log(res); // 20
};

main();
  • 如果 await 的 promise 失败了,就会抛出异常,需要通过 try…catch 捕获处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function main() {
let p = new Promise((resolve, reject) => {
reject("error");
});

try{
let res = await p;
} catch(e) {
console.log(e); // error
}

};

main();

3.3 两者的结合案例

  • 需求:读取三个文件

原始的回调函数形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require("fs");

// 使用回调函数的方式
fs.readFile("./1.html", (err,data1) => {
if(err) throw err;
fs.readFile("./3.html", (err,data2) => {
if(err) throw err;
fs.readFile("./6.html", (err,data3) => {
if(err) throw err;
console.log(data1 + data2 + data3);
});
});
});

async + await形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const myReadFile = require("util").promisify(fs.readFile);
// 使用async和await
async function main() {
try{
let data1 = await myReadFile("./1.html");
let data2 = await myReadFile("./3.html");
let data3 = await myReadFile("./6.html");
console.log(data1 + data2 + data3);
}catch(e){
console.log(e);
}

};

main();
  • 发送ajax请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function sendAjax(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300) {
// 控制台输出响应体
resolve(xhr.response);
} else {
// 控制台输出响应状态码
reject(xhr.status);
}
}
}
});
}

let btn = document.getElementById("btn");
btn.addEventListener("click",async function(){
// 获取段子信息
let joke = await sendAjax('https://api.apiopen.top/getJoke');
console.log(joke);
});