Promise的使用与实现
[TOC]
引言:
Promise是ES6引入进行异步编程的新的解决方案,从语法上来说,就是一个构造函数,可以封装异步的任务,并且可以对结果进行处理。
Promise最大的好处在于可以解决回调地狱的问题,并且在指定回调和错误处理方面,更加的灵活与方便。
课程大纲
- 1.
Promise
介绍和基本使用 - 2.
Promise API
- 3.
Promise
关键问题 - 4.
Promise
自定义封装 - 5.
async
与await
0.前置知识
0.1.区别实例对象和函数对象
实例对象
new
函数产生的对象,称为实例对象, 简称对象
函数对象
- 将函数作为对象使用时,简称函数对象(都是
Object
函数的实例对象)
- 将函数作为对象使用时,简称函数对象(都是
先看懂语法,再看懂功能
- 每个变量是什么数据类型
- 返回的是什么数据类型
- 括号的左边必然是函数,点的左边必然是对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
function Fn() { // Fn是函数
}
const fn = new Fn() // Fn是构造函数,fn是实例对象(简称对象)
console.log(Fn.prototype) // Fn是函数对象
Fn.bind({}) // Fn是函数对象
$('#test') // JQuery函数
$.get('/test') // JQuery函数对象
</script>
</body>
</html>
0.2.两种类型的回调
同步回调
立即执行,完全执行完了才结束,不会放入回调队列中
例子:
数组遍历相关的回调函数
1
2
3
4
5const arr = [1, 3, 5]
arr.forEach(item => {
console.log(item) // 遍历的回调函数,是同步回调,不会放入队列,一上来就会执行
})
console.log('forEach()之后')Promise
的executor
函数
异步回调
不会立即执行,会放入回调队列中将来执行
例子
1
2
3
4setTimeout(() => {
console.log('timeout callback()') // 异步回调函数,会放入队列中将来执行
})
console.log('setTimeout()之后')
如何判断是异步还是同步的呢?
- 打印看一下输出顺序
拓展:10张图让你彻底理解回调函数 - 知乎 (zhihu.com)
0.3.JS
中error
的处理
Error - JavaScript | MDN (mozilla.org)
错误类型
Error
:所有错误的父类型ReferenceError
:引用的变量不存在1
2console.log(a) // 如果没有捕获异常,下面的代码是不会执行的
console.log('-----')TypeError
:数据类型不正确的错误1
2
3
4let b = null
console.log(b.xxx) // 想要读属性,点的左边应该是一个对象
b.xxx() // 也是类型错误RangeError
:数据值不在其允许的范围内1
2
3
4
5
6function fn() {
fn()
}
fn()SyntaxError
:语法错误1
const c = """"
错误处理(如果不处理,程序就无法向下执行)
捕获错误:
try...catch
1
2
3
4
5
6
7
8
9try {
let d
console.log(d.xxx)
} catch(error) {
console.log(error)
console.log(error.message)
console.log(error.stack)
}
console.log('出错之后')打断点查看
error
对象抛出错误:
throw error
1
2
3
4
5
6
7
8
9
10
11
12
13function doSomething() {
if(Date.now() % 2 === 1) {
console.log('当前日期为奇数,可以执行任务')
} else {
throw new Error('当前日期为偶数,无法执行')
}
}
try {
doSomething()
} catch (error) {
alert(error.message)
}
错误对象
message
属性:错误相关信息stack
属性:函数调用记录栈记录信息
1.Promise
初体验
index.html
1 |
|
1.1.Promise
封装异步操作
1 |
|
如下图框选的[[PromiseState]]
,表示的就是Promise
对象p
的状态
,该状态会传递给then
方法,不同的状态会传递给then
方法不同的形参
1.2.获取成功/失败的结果值
新需求:打印输出中奖的数字
Promise
对象用来封装一个异步操作,并可以获取其成功/失败的结果值
在实例化Promise时,封装的异步操作里面,任何定义的变量,都可以当作时结果值,传递给resolve
和reject
我们可以将n
看作是结果值,传递给resolve
和reject
1 | if(n <= 30) { // 将 n <= 30 认为是成功的状态 |
对象p
可以拿到处理成功/失败的结果,作为参数传递给then
方法对应的成功/失败的回调函数,默认then
方法两个回调函数的参数分别是value
和reason
,写成a
和b
也行
1 | p.then((value) => { |
如下图框选的[[PromiseResult]]
,表示的就是Promise
对象p
获取到异步操作的结果值
,该结果值根据状态
的不同,传递给then
方法不同的形参
1.3.Promise
练习
1.3.1.fs
读取文件
回调函数形式
1 | const fs = require('fs') |
Promise形式
1 | const fs = require('fs') |
1.3.2.AJAX
请求
原生AJAX请求
1 |
|
Promise形式
1 |
|
1.4.Promise
封装
1.4.1.Promise
封装fs
读取文件操作
1 | // 封装一个函数myReadFile,读取文件内容 |
当这样封装后,不需要在readFile
后写回调函数,可以在then
方法里写回调函数
1.4.2.使用util.promisify
方法进行promise
风格转化
上一小节中,我们是手动封装了require('fs').readFile
方法
node
中可以使用util.prmisify
来达到自动
封装的效果
官网:util 实用工具 | Node.js API 文档 (nodejs.cn)
util.promisify(original)
新增于: v8.0.0
original
<Function>
- 返回:
<Function>
采用遵循常见的错误优先的回调风格的函数(也就是将
(err, value) => ...
回调作为最后一个参数),并返回一个返回 promise 的版本。
1 | // 引入util模块 |
作用:我们不需要每个函数都手动封装成Promise
风格的函数
1.4.3.Promise
封装AJAX
1 |
|
2.Promise
的理解和使用
2.1.Promise
是什么?
2.1.1.理解
抽象表达:
Promise
是一门新的技术(ES6规范)Promise
是JS
中进行异步编程的新解决方案
旧解决方案是是使用回调函数
- 什么是回调函数:10张图让你彻底理解回调函数 - 知乎 (zhihu.com)
异步操作包括哪些呢
fs
文件操作1
require('fs').readFile('./index.html',(err,data) => {})
数据库操作
网络请求(AJAX)
1
$.get('/server', (data) => {})
定时器(setTImeout)
1
setTimeout(() => {},2000)
具体表达:
从
语法
上来说,Promise
是一个构造函数,可以通过new
来实例化实例化时需要一个
函数
类型的值作为参数,并且这个函数有两个形参resolve
、reject
(写成a
和b
也可以)1
2
3const p = new Promise((resolve, reject) => {
// 包裹异步操作
})resolve
和reject
都是函数
类型的数据- 当
异步任务成功
时,调用resolve
函数,并将Promise
对象p
的状态
,设置为成功
- 当
异步任务失败
时,调用reject
函数,并将Promise
对象p
的状态
,设置为失败
- 异步任务是成功还是失败,取决于实际业务逻辑,具体可见
2.3.1
的案例
- 当
p.then()
Promise
的实例对象有一个then
方法,这个方法执行时接收两个函数
类型的参数第一个函数,是对象
p
状态
为成功
的回调函数第二个函数,是对象
p
状态
为失败
的回调函数1
2
3
4
5p.then(() => {
// 成功状态后的操作
}, () => {
// 失败状态后的操作
})
从功能上来说,
Promise
对象用来封装一个异步操作,并可以获取其成功/失败的结果值- 详见
2.3.2
的案例
- 详见
2.1.2.Promise
对象的状态改变
状态
是Promise
实例对象的一个属性
:[[PromiseState]]
该属性的可能值
pending
:初始化的默认值,表示未决定
resolved
/fulfilled
:表示(异步操作的结果)成功
rejected
:表示(异步操作的结果)失败
状态的改变
- 异步操作
成功
,Promise
对象状态,由pending
变成resolved
- 异步操作
失败
,Promise
对象状态,由pending
变成rejected
- 说明:
Promise
对象状态只有这两种,且一个Promise
对象的状态,只能改变一次- 无论
Promise
对象状态变为成功
还是失败
,都会获取到异步操作的结果数据
(前提是给resovle
和reject
函数传参了,否则结果数据
为undefined
) 成功
状态获取到结果数据
一般称为value
,失败
状态获取到的结果数据
一般称为reason
- 异步操作
2.1.3.Promise
对象的结果值
结果值
是Promise
实例对象的一个属性:[[PromiseResult]]
,保存着Promise
对象成功/失败
的结果值
谁可以修改这个
结果值
呢?resolve/reject
这两个函数,是可以对实例对象中的结果值
进行修改的
在后续
then
方法的回调函数中,就可以把实例对象的结果值
取出来,然后进行相关的操作
2.1.4.Promise
执行的基本流程
2.2.为什么要使用Promise
?
2.2.1.指定回调函数的方式更加灵活
- 旧的:
- 必须在异步任务前指定
Promise
:- 启动异步任务 => 返回
Promise
对象 => 给Promise
对象绑定回调函数(甚至可以在异步任务结束后指定/多个)
- 启动异步任务 => 返回
2.2.2.支持链式调用,可以解决回调地狱的问题
什么是回调地狱?
回调函数的嵌套调用,外部回调函数异步执行的结果,是嵌套的回调执行的条件
1
2
3
4
5
6
7
8
9asyncFunc1(opt, (...args1) => {
asyncFunc2(opt, (...args2) => {
asyncFunc3(opt, (...args3) => {
asyncFunc4(opt, (...args4) => {
// some operation
})
})
})
})
回调地狱的缺点
- 语法上的一层层嵌套,只是最基础的
- 不便于阅读
- 不便于异常处理
- 可能会写很多重复性代码
- 控制反转问题(重点)
解决方案
2.3.如何使用Promise
2.3.1.API
Promise
构造函数
Promise
构造函数:Promise(executor) {}
Promise
构造函数实例化时,需要接收一个函数类型的参数,可以是箭头函数或匿名函数,该参数被称为executor
函数executor
函数:表示执行器函数,就是(resolve, reject) => {}
执行器函数特点:会在
Promise
内部立即同步调用,异步操作在执行器函数执行1
2
3
4
5const p = new Promise((resolve, reject) =>{
console.log('aa')
})
console.log('bb')打印结果如下:
先输出
aa
,再输出bb
,就说明执行器函数内部的代码,和第5行的代码,是同步调用的,也就是说,执行器函数的代码,不会进入到队列中,会立即执行(后面进行封装时要注意这一点)
resolve
函数:内部定义成功时,我们调用的函数,并且会修改Promise
对象的结果值
:value => {}
reject
函数:内部定义失败时,我们调用的函数。并且会修改Promise
对象的结果值
:reason => {}
Promise.prototype.then
方法
Promise.prototype.then
方法:(onResolved, onRejected) =>{}
then
方法有两个参数,也都是函数类型的参数,指定用于得到成功value
的成功回调,和用于得到失败reason
失败的回调,返回一个新的Promise
对象onResolved
函数:成功的回调函数,并获取到Promise
对象的结果值
:value => {}
onRejected
函数:失败的回调函数,并获取到Promise
对象的结果值
:reason => {}
then
方法的返回值:为一个新的Promise
对象如果
then
方法的回调函数中,抛出异常,新Promise
状态变为rejected
,结果值reason
为抛出的异常1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let p = new Promise((resolve, reject) => {
resolve('ok')
})
// 执行then方法
let result = p.then(value => {
throw 'Error'
}, reason => {
console.log(reason)
})
console.log(result)
result.catch(reason => {
return reason
})如果
then
方法的回调函数中,返回的是非Promise
的任意值,新Promise
状态变为resolved/fulfilled
,结果值value
为返回的值1
2
3
4
5
6
7
8
9
10
11
12
13let p = new Promise((resolve, reject) => {
resolve('ok')
})
// 执行then方法
let result = p.then(value => {
return 521
}, reason => {
console.log('reason为:' + reason)
})
console.log(result)如果
then
方法的回调函数中,返回的是另一个新的Promise
,此Promise
的结果就会成为新的Promise
的结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let p = new Promise((resolve, reject) => {
resolve('ok')
})
// 执行then方法
let result = p.then(value => {
return new Promise((resolve, reject) => {
resolve('success')
})
}, reason => {
console.log('reason为:' + reason)
})
console.log(result)如果
then
方法的回调函数中,即没有抛出异常,也没有return
,则新的Promise
对象的结果值为undefined
Promise.prototype.catch
方法
Promise.prototype.catch
方法:(onRejected) => {}
catch
方法也是用来指定回调的,不过只能用来指定失败
的回调catch
只是做了一个单独的封装,内部也是用then
方法来实现的
onRejected
函数:失败的回调函数,并获取到Promise
对象的结果值
:reason => {}
1
2
3
4
5
6
7
8const p = new Promise((resovle, reject) => {
reject('error')
})
p.catch(reason => {
console.log(reason)
return reason
})打印结果如下:
catch
处理过失败的Promise
对象后,返回的是一个成功的Promise
对象,该对象的结果值,取决于你在实际代码中返回的值。
Promise.resolve
方法
Promise.resolve
方法:(value) => {}
resolve
是在Promise
实例对象上的,而不是在Promise
的原型对象上resolve
参数:value
可以是成功状态
的结果
值或Promise
对象如果传入的参数为非
Promise
类型的对象(可以是undefined
、null
等等),则返回的结果为成功的Promise
对象1
2const p = Promise.resolve('value')
console.log(p)如果传入的参数为
Promsie
对象,则参数的结果,决定了resolve
的结果当参数返回成功时,得到的就是
成功
的Promise
:1
2
3
4const p = Promise.resolve(new Promise((resolve, reject) => {
resolve(true)
}))
console.log(p)当参数返回失败时,得到的就是
失败
的Promise
:1
2
3
4
5
6
7const p = Promise.resolve(new Promise((resolve, reject) => {
reject('Error')
}))
console.log(p)
p.catch(reason =>{
console.log(reason)
})
返回:返回一个
成功/失败
的Promise
对象作用:为了快速得到一个
Promise
对象,并且还能封装一个值,将这个值转化为Promise
对象
Promise.reject
方法
Promise.reject
方法:(reason) => {}
reject
是在Promise
实例对象上的,而不是在Promise
的原型对象上reject参数
:reason
表示失败的原因如果传入的参数为非
Promise
类型的对象,返回的是失败
的Promise
对象1
2
3
4
5
6const p = Promise.reject('value')
console.log(p)
p.catch(reason => {
console.log('reason为:', reason)
})如果传入的参数为
失败
的Promise
对象,返回的是失败
的Promise
对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 定义失败的Promise对象
let p1 = new Promise((resolve, reject) => {
reject('Error')
})
// 处理失败的Promise对象
p1.catch(reason => {
return reason
})
const p2 = Promise.reject(p1) // 将失败的Promise对象传入reject
console.log(p2) // 返回的仍是失败的Promise对象,其结果值是传入的那个Promise对象
p2.catch(reason => {
console.log('reason为:', reason)
})如果传入的参数为
成功
的Promise
对象,返回的仍然是失败
的Promise
对象,只不过reason
是你传的那个成功
的Promise
对象1
2
3
4
5
6
7
8const p = Promise.reject(new Promise((resolve, reject) => {
resolve('OK')
}))
console.log(p)
p.catch(reason => {
console.log('reason为:', reason)
})
返回:始终返回一个
失败
的Promise
对象作用:为了快速的将一个值,转换成一个失败的
Promise
对象。无论这是值是非Promise
类型,还是成功/失败
的Promise
类型。
Promise.all
方法
Promise.all
方法:(promises) => {}
promises
参数:包含n
个promise
的数组返回:返回一个新的
promise
,只有所有的promise
都成功才成功,只要有一个失败了就直接失败如果数组中的每个
Promise
对象状态都是成功,all
方法返回的是一个成功的Promise
对象,该Promise
对象成功的结果值
,是每个promise
的成功结果值
组成的数组1
2
3
4
5
6
7
8let p1 = new Promise((resolve, reject) => {
resolve('OK')
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Oh Yeah')
const result = Promise.all([p1, p2, p3])
console.log(result)如果数组中有一个
Promise
对象状态是失败,all
方法返回的是一个失败的Promise
对象,该Promise
对象失败的结果值
,是那个失败的Promise
对象的结果值1
2
3
4
5
6
7
8let p1 = new Promise((resolve, reject) => {
resolve('OK')
})
let p2 = Promise.reject('Error') // 失败的Promise对象
let p3 = Promise.resolve('Oh Yeah')
const result = Promise.all([p1, p2, p3])
console.log(result)返回的是一个失败的
Promise
对象注意:如果我们用链式调用的方式,使用
catch
指定了失败Promise
对象的回调,则all
方法返回的仍是成功的Promise
对象1
2
3
4
5
6
7
8
9
10let p1 = new Promise((resolve, reject) => {
resolve('OK')
})
let p2 = Promise.reject('Error').catch(reason => {
return reason
}) // 使用catch处理失败的Promise对象,返回的是一个成功的Promise对象
let p3 = Promise.resolve('Oh Yeah')
const result = Promise.all([p1, p2, p3])
console.log(result)因为失败的
Promise
一旦用catch
指定失败的回调
,其返回值是一个成功的Promise
对象所以在处理失败的
Promise
对象时,不能用链式调用的写法,要分开来写才行,这样p2
就是一个失败的Promise
对象了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let p1 = new Promise((resolve, reject) => {
resolve('OK')
})
let p2 = Promise.reject('Error')
p2.catch(reason => {
return reason
}) // 分开来写
let p3 = Promise.resolve('Oh Yeah')
const result = Promise.all([p1, p2, p3])
console.log(result)
result.catch(reason => {
return reason
})备注:如果有两个失败的
Promise
对象,则返回的结果值
是优先失败的那个Promise
对象的结果值,这里返回的是p3
的结果值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20let p1 = new Promise((resolve, reject) => {
resolve('OK')
})
let p2 = Promise.reject('Error') // 失败的Promise
p2.catch(reason => {
return reason
})
let p3 = Promise.reject('Oh Yeah') // 失败的Promise
p3.catch(reason => {
return reason
})
const result = Promise.all([p1, p3, p2]) // 将p3放在前面
console.log(result)
result.catch(reason => {
return reason
})
Promise.race
方法
Promise.race
方法:(promises) => {}
promises
参数:包含n
个promise
的数组返回:返回一个新的
Promise
,第一个完成的Promise
的结果状态,就是最终的结果状态1
2
3
4
5
6
7
8
9
10let p1 = new Promise((resolve, reject) => {
resolve('OK')
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Oh Yeah')
const result = Promise.race([p1, p2, p3])
console.log(result)p1
先完成,race
返回的就是p1
的状态结果我们给
p1
添加一个定时器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK')
}, 2000)
})
let p2 = Promise.reject('Error')
p2.catch(reason => {
return reason
})
let p3 = Promise.resolve('Oh Yeah')
const result = Promise.race([p1, p2, p3])
console.log(result)
result.catch(reason => {
return reason
})p2
先完成,race
返回的就是p2
的结果
2.3.2Promise
的几个关键问题
如何改变Promise
的状态
resolve(value)
:如果当前是pending
,就会变成resolved
1
2
3
4let p = new Promise((resolve, reject) => {
// 1.resolve函数
resolve('OK') // pending ==> fulfilled
})reject(reason)
:如果当前是pending
,就会变成rejected
1
2
3
4let p = new Promise((resolve, reject) => {
// 2.reject函数
resolve('Error') // pending ==> rejected
})通过
throw
抛出了异常:如果当前是pending
,就会变成rejected
1
2
3
4let p = new Promise((resolve, reject) => {
// 3.throw抛出异常
throw 'Error' // pending ==> rejected
})
能否执行多个回调
如果使用
then
方法,为一个Promise
对象指定多个成功/失败回调函数,都会调用吗?当
Promise
改变为对应状态时都会调用1
2
3
4
5
6
7
8
9
10
11
12let p = new Promise((resolve, reject) => {
resolve('OK') // pending => fulfilled
})
// 指定回调1
p.then(value => {
console.log(value + '1')
})
// 指定回调2
p.then(value => {
console.log(value + '2')
})结果如下:
如果不调用
resolve
,则状态一直是pending
,则回调函数也不会被调用
改变Promise
状态和指定回调函数谁先谁后
1 | let p = new Promise((resolve, reject) => { |
问:resolve('OK')
改变状态,和value => {}
指定回调,谁先执行?
都有可能,正常情况下是先指定回调再改变状态,但也可以先改变状态再指定回调
如何改变
Promise
状态:resolve()
reject()
throw
如何指定回调:
then()
catch()
什么时候先改变状态,再指定回调?
在执行器函数中,直接调用
resolve()/reject()
当执行器函数里面的任务,是同步任务的时候
1
2
3
4
5
6
7
8
9
10let p = new Promise((resolve, reject) => {
resolve('OK') // 先改变状态
})
p.then(value => {
// 后指定回调
console.log(value)
}, reason => {
})延迟更长时间才调用
then()
,这个例子中,我们可以延迟2秒再执行then()
方法
什么时候先指定回调,后改变状态?
当执行器函数里面的任务,是异步任务,需要进入队列的时候
1
2
3
4
5
6
7
8
9
10
11
12
13
14let p = new Promise((resolve, reject) => {
// 后改变状态
setTimeout(() => {
resolve('OK')
}, 2000)
})
p.then(value => {
// 先指定回调
// 等1秒,输出value
console.log(value)
}, reason => {
})进行文件操作、数据库操作、
AJAX
操作时,执行器函数里面的,都是异步任务
什么时候才能得到数据?(回调函数什么时候执行)
- 执行器函数中,可以是同步任务,也可以是异步任务
- 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据(异步任务)
- 如果先改变状态,那当指定回调时,回调函数就会调用,得到数据(同步任务)
Promise.then()
返回的新的Promise
的结果状态,由什么决定?
简单表达:由
then()
指定的回调函数执行的结果决定详细表达:
then
方法的返回值:为一个新的Promise
对象如果
then
方法的回调函数中,抛出异常,新Promise
状态变为rejected
,结果值reason
为抛出的异常1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let p = new Promise((resolve, reject) => {
resolve('ok')
})
// 执行then方法
let result = p.then(value => {
throw 'Error'
}, reason => {
console.log(reason)
})
console.log(result)
result.catch(reason => {
return reason
})如果
then
方法的回调函数中,返回的是非Promise
的任意值,新Promise
状态变为resolved/fulfilled
,结果值value
为返回的值1
2
3
4
5
6
7
8
9
10
11
12
13let p = new Promise((resolve, reject) => {
resolve('ok')
})
// 执行then方法
let result = p.then(value => {
return 521
}, reason => {
console.log('reason为:' + reason)
})
console.log(result)如果
then
方法的回调函数中,返回的是另一个新的Promise
,此Promise
的结果就会成为新的Promise
的结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let p = new Promise((resolve, reject) => {
resolve('ok')
})
// 执行then方法
let result = p.then(value => {
return new Promise((resolve, reject) => {
resolve('success')
})
}, reason => {
console.log('reason为:' + reason)
})
console.log(result)如果
then
方法的回调函数中,即没有抛出异常,也没有return
,则新的Promise
对象的结果值为undefined
Promise
如何串联多个操作任务
Promise
的then()
返回一个新的Promise
,可以写成then()
的链式调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK')
}, 1000)
})
p.then(value => {
console.log(value) // 延迟一秒,OK
return new Promise((resolve, reject) => {
resolve('success')
})
}).then(value => {
console.log(value) // success
}).then(value => {
console.log(value) // undefined
})通过
then()
的链式调用串联多个同步/异步任务
Promise
异常穿透
当使用
Promise
的then
链式调用时,可以在最后指定失败的回调1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('Error')
}, 1000)
})
p.then(value => {
console.log(value)
return new Promise((resolve, reject) => {
resolve('success')
})
}).then(value => {
console.log(value)
}).then(value => {
console.log(value)
}).catch(reason => {
console.log(reason) // 1秒后 Error
})前面任何操作出现了异常,都会传到最后失败的回调中处理
- 如果出现了多个异常,只捕获第一个异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('Error')
}, 1000)
})
p.then(value => {
console.log(value)
return new Promise((resolve, reject) => {
reject('Error02')
})
}).then(value => {
console.log(value)
}).then(value => {
console.log(value)
}).catch(reason => {
console.log(reason) // 1秒后 Error,不会输入Error02
})
如何中断Promise
链
当使用
Promise
的链式调用时,在中间中断,不再调用后面的回调函数唯一办法:
在回调函数中返回一个
pendding
状态的Promise
对象pendding
状态,意味着状态没有发生改变,也就不会执行回调函数了代码:
return new Promise((resolve, reject) => {})
1
2
3
4
5
6
7
8
9
10
11
12
13
14let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK')
}, 1000)
})
p.then(value => {
console.log(111) // 只会输出111
return new Promise((resolve, reject) => {})
}).then(value => {
console.log(222) // 不会被调用
}).then(value => {
console.log(333) // 不会被调用
})
3.手写Promise
3.1.定义整体结构
1 | |----index.html |
index.html
1 |
|
promise.js
1 | function Promise() { |
报错信息如下,因为我们没有在构造函数Promise
的显示原型属性上定义then
方法,所以实例对象p
的隐式原型属性上自然就没有该then
方法
构造函数Promise
的显示原型属性上,添加then
方法,该then
方法有两个形参:onResolved
、onRejected
同时也给Promise
构造函数,指定一个形参:executor
promise.js
1 | function Promise(executor) { |
至此,基本结构搭建完毕
3.2.resolve
和reject
结构搭建
promise.js
1 | function Promise(executor) { |
注意:在当前作用域当中,并没有resolve
和reject
变量
所以需要在函数内部声明,这两个变量都是函数类型的变量
promise.js
1 | function Promise(executor) { |
3.3.resolve
和reject
代码实现
resolve
函数执行完之后,有什么效果?Promise
的状态会发生改变,有pending
变为fulfilled
- 可以设置
Promise
对象的结果值
- 我们需要添加
promiseState
和promiseResult
这两个属性
promise.js
1 | function Promise(executor) { |
在index.html
中测试看下效果
1 |
|
结果如下:
按道理来说,PromiseResult
应该是OK
,PromiseState
应该是fulfilled
为什么没有变化呢?
是因为resolve
函数中,this
指向window
1 | function Promise(executor) { |
结果:
此时,调用resolve
,Promise
对象的状态和结果都发生了改变
完善一下reject
1 | function Promise(executor) { |
3.4.throw
抛出异常改变状态
如果直接抛出异常,则会报错
1 | <script> |
正常来说,应该打印一个失败的Promise
对象
我们使用try catch
来处理throw
,加在执行器函数执行的地方
1 | function Promise(executor) { |
打印结果:
3.5.Promise
状态只能修改一次
我们现在封装的代码,如果先调用了resolve
,再调用reject
,Promise
对象的状态会改变两次
1 | <script> |
最终的结果(这是错误的):
那么应该怎么做,可以确保状态值更改一次呢?
应该在改变状态之前,加一层判断,判断是否已经更改过了
1 | function Promise(executor) { |
结果如下(这个是对的):
3.6.then
方法执行回调
此时我们封装的then
方法,是不能执行回调的
index.html
1 |
|
结果是没有打印输出的
为啥呢?因为我们还没写调用的代码!
应该在then
方法中调用回调函数
1 | function Promise(executor) { |
打印结果:
3.7.异步任务回调的执行
我们在执行器函数中定义异步任务
1 |
|
此时并没有打印输出结果
代码从上往下走,resovle
函数并没有立即执行
再往下走,p
的状态并没有发生变化,依旧是pending
状态,但我们并没有对pending
状态做任何处理
所以,在then
方法中我们需要对pending
状态进行判断
1 | // 判断pending状态 |
那么,我们需要做什么操作呢?
对于异步任务来说,什么时候调用回调函数呢?应该是在状态发生改变的时候,那么什么时候状态发生改变呢?
很明显,是在调用resovle/reject
函数的时候,
所以我们应该在resolve/reject
函数内,调用回调函数
1 | function resolve(data) { |
那么,怎么样才可以在resovle
内,调用到then
方法的回调函数呢?这两个函数是在不同的作用域内的
所以,我们在判断pending
状态后,应该将回调函数保存下来
那么,应该保存在哪儿呢?在then
方法外部定义一个变量吗?不!我们直接给Promise
构造函数声明一个属性,实例化时保存在实例对象上
1 | function Promise(executor) { |
运行完then
方法后,打印输出p
:
1 |
|
打印结果:
对象p
已经有了回调函数
之后,我们就可以在resolve/reject
函数中,执行对应的回调函数了
1 | function Promise(executor) { |
此时再看下打印效果,一秒后,输出结果:
3.8.指定多个回调的实现
index.html
1 |
|
目前我们的代码是不能够执行多个回调的,最后的then
方法中的回调,会将之前的覆盖掉,只会执行最后一次的回调
所以我们之前那种保存回调函数的方式,需要修改一下:
将callback
定义成数组,每次保存回调时push
进去一个对象即可
1 | this.callbacks = [] // 回调函数保存在实例对象的callback属性上 |
在then
方法后面,打印一下对象p
此时两个回调函数都被保存到了实例对象上
在resovle/reject
函数中,重新调用下相应的回调函数
1 | function Promise(executor) { |
打印结果如下:
两次回调函数都被执行了
3.9.同步任务下的then
方法返回结果实现
index.html
1 |
|
目前res
是undefined
,因为我们在then
方法中,并没有返回任何的值
所以我们在then
方法中,返回一个Promise
对象
1 |
|
此时res
的结果如下:
有个问题,就是then
方法返回的Promise
对象的状态,一直是pending
我们需要根据then
方法回调函数的执行结果,来改变then
方法返回的Promise
对象的状态
所以要定义一个变量result
保存回调函数执行的结果
1 | let result = onResolved(this.PromiseResult) |
对结果进行类型判断,并对状态进行设置
1 | function Promise(executor) { |
打印结果如下:
如果then
方法的回调函数,返回的是Promise
类型的对象呢?
1 | function Promise(executor) { |
index.html
1 |
|
打印结果如下:
如果then
方法的回调函数,抛出了异常呢?
使用try catch
进行包裹处理
1 | function Promise(executor) { |
index.html
1 |
|
打印结果如下:
3.10.异步任务下的then
方法返回结果实现
index.html
1 |
|
打印结果是pending
状态的Promise
:
由于是异步任务,对象p
在调用then
方法时,是一个pending
状态,目前我们自定义的代码,对于pending
状态 操作,仅做了一个保存回调函数的操作
1 | if (this.PromiseState === 'pending') { |
我们需要修改一下pending
状态下回调的代码
1 | if (this.PromiseState === 'pending') { |
验证下onResoved
函数在回调执行的时候,的确是被调用了:
我们真正要做的,是要执行成功的回调函数(注意this
的指向问题)
1 | Promise.prototype.then = function (onResolved, onRejected) { |
但此时返回的Promise
对象的状态仍然是pending
和上一小节类似,要根据函数的执行结果来决定状态
1 | function Promise(executor) { |
我们在then
方法成功的回调函数中,返回一个值
1 |
|
打印结果如下:
完善一下onRejected
,同时做一下异常处理
1 | function Promise(executor) { |
3.11.then
方法的完善与优化
对
rejected
状态情况进行完善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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121function Promise(executor) {
this.PromiseState = 'pending'
this.PromiseResult = null
this.callbacks = []
const self = this
function resolve(data) {
if (self.PromiseState !== 'pending') return
self.PromiseState = 'fulfilled'
self.PromiseResult = data
self.callbacks.forEach(item => {
item.onResolved(data)
})
}
function reject(data) {
if (self.PromiseState !== 'pending') return
self.PromiseState = 'rejected'
self.PromiseResult = data
self.callbacks.forEach(item => {
item.onRejected(data)
})
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function (onResolved, onRejected) {
const self = this
return new Promise((resolve, reject) => {
if (this.PromiseState === 'fulfilled') {
let result = onResolved(this.PromiseResult)
// 使用try catch进行包裹处理
try {
if (result instanceof Promise) {
result.then(v => {
resolve(v)
}, r => {
reject(r)
})
} else {
resolve(result)
}
} catch (e) {
reject(e)
}
}
if (this.PromiseState === 'rejected') {
try {
// 对rejected的情况进行完善
let result = onRejected(this.PromiseResult)
if (result instanceof Promise) {
result.then(v => {
resolve(v)
}, r=> {
reject(r)
})
} else {
resolve(result)
}
} catch (e) {
reject(e)
}
}
if (this.PromiseState === 'pending') {
this.callbacks.push({
onResolved: function () {
// 处理抛出的异常
try {
// 异步任务的pendding状态下,先调用成功的回调函数
let result = onResolved(self.PromiseResult)
// 根据回调函数的执行结果来决定状态
if (result instanceof Promise) {
result.then(v => {
resolve(v)
}, r => {
reject(r)
})
} else {
resolve(result)
}
} catch (e) {
reject(e)
}
},
onRejected: function () {
try {
let result = onRejected(self.PromiseResult)
// 根据回调函数的执行结果来决定状态
if (result instanceof Promise) {
result.then(v => {
resolve(v)
}, r => {
reject(r)
})
} else {
resolve(result) // 这里不需要改成reject(result)
}
} catch (e) {
reject(e)
}
}
})
}
})
}重复代码的封装
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79function Promise(executor) {
this.PromiseState = 'pending'
this.PromiseResult = null
this.callbacks = []
const self = this
function resolve(data) {
if (self.PromiseState !== 'pending') return
self.PromiseState = 'fulfilled'
self.PromiseResult = data
self.callbacks.forEach(item => {
item.onResolved(data)
})
}
function reject(data) {
if (self.PromiseState !== 'pending') return
self.PromiseState = 'rejected'
self.PromiseResult = data
self.callbacks.forEach(item => {
item.onRejected(data)
})
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function (onResolved, onRejected) {
const self = this
return new Promise((resolve, reject) => {
// 封装函数,注意内部this的指向问题
function callback(type) {
// 使用try catch进行包裹处理
try {
let result = type(self.PromiseResult) // 封装复制的时候,result的定义要放在try的后面
if (result instanceof Promise) {
result.then(v => {
resolve(v)
}, r => {
reject(r)
})
} else {
resolve(result)
}
} catch (e) {
reject(e)
}
}
if (this.PromiseState === 'fulfilled') {
callback(onResolved)
}
if (this.PromiseState === 'rejected') {
callback(onRejected)
}
if (this.PromiseState === 'pending') {
this.callbacks.push({
onResolved: function () {
callback(onResolved)
},
onRejected: function () {
callback(onRejected)
}
})
}
})
}
3.12.catch
方法异常穿透与传值
index.html
1 |
|
此时会报错:
因为我们自定义的代码中,还没有添加catch
方法
添加catch
:我们在内部调用then
方法即可
1 | function Promise(executor) { |
catch
方法返回的也是Promise
对象
index.html
1 |
|
打印结果如下:
我们还需要实现异常穿透
的功能
index.html
1 |
|
打印结果如下:
原因:
代码执行到then
之前的时候,对象p
是一个pending
的状态,代码中我们把两个回调存起来了:
1 | if (this.PromiseState === 'pending') { |
但此时,成功的回调是有的,失败的回调我们没写,所以是一个undefined
,当状态改变完之后,一定会调用回调:
1 | self.callbacks.forEach(item => { |
而这个item
里面,只有成功的回调,而没有失败的回调。实际调用的是就是undefined
,就有问题了
所以我们在封装时,要允许then
方法不传第二个回调函数,then
方法在return
之前,需要对onRejected
的回调做一下判断
1 | Promise.prototype.then = function (onResolved, onRejected) { |
当没有失败的回调时,我们自定义一个默认的回调,这个默认的回调中,抛出了异常的结果值。
这样当链式调用then
方法,就相当于如下情况
1 | p.then(value => { |
然后在then
方法中一直抛,直到遇到catch
方法,可以将异常处理掉
1 | Promise.prototype.catch = function(onRejected) { |
打印结果如下:
如果异常是发生在链式调用的中间,也是可以实现异常穿透的
index.html
1 |
|
打印效果如下:
除了可以进行异常穿透外,还有一个特性就是值传递
即如下代码,也是可以可以正常的执行的
index.html
1 |
|
但我们自定义的是不行的,打印结果如下:
原因和上面一样,所以我们也要指定默认的成功回调函数
1 | function Promise(executor) { |
打印结果正常:
3.13.封装resolve
方法
index.html
1 |
|
报错:
因为我们自定义的代码还没有添加呢
1 | function Promise(executor) { |
测试一下结果,返回的结果的确是一个Promise
对象
接下来我们根据传入的value
来更改状态
1 | function Promise(executor) { |
此时打印结果的状态,不再是pending
了,并且当传入的value
是一个Promise
对象时,也是正常的
1 |
|
打印结果如下:
resolve
的嵌套调用也是支持的
1 |
|
3.14.封装reject
方法
类似resovle
方法的封装
1 | // ... |
测试效果:
1 |
|
3.15.封装all
方法
1 |
|
此时我们的all
方法还没有定义,定义all
方法:
1 | // ... |
此时看下打印结果:
3.16.封装race
方法
新增race
方法
1 |
|
测试:
1 |
|
结果:
对象p2
先执行完,就先返回p2
3.17.then
方法回调函数异步执行
原生实现:
1 |
|
打印结果:
自定义实现打印结果:
then
方法的回调应该是异步执行的,我们给对应执行的代码添加定时器:
1 | function Promise(executor) { |
此时的结果在表现上,就和原生的一样了:
3.18.Promise
完整实现
1 | function Promise(executor) { |
3.19.class
版本的实现
promise_class.js
1 | class Promise { |
调用测试一下:
1 |
|
结果正常
4.async
与await
4.1.mdn
文档
async function - JavaScript | MDN (mozilla.org)
await - JavaScript | MDN (mozilla.org)
4.2.async
函数
函数的返回值为一个
Promise
对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
}
let result = main()
console.log(result)
</script>
</body>
</html>Promise
对象的结果由async
函数执行的返回值决定如果返回值是一个非
Promise
类型的数据:- 最终的返回结果就是你返回的那个值,
- 最终返回的状态时为成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
return 'OK'
}
let result = main()
console.log(result)
</script>
</body>
</html>如果返回值是一个
Promise
类型的对象,最终async
函数的返回结果和该Promise
对象返回的结果一致1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
return new Promise((resolve, reject) => {
resolve('Success')
})
}
let result = main()
console.log(result)
</script>
</body>
</html>如果抛出了异常,最终返回的结果就是一个失败的
Promise
对象,结果值就是抛出的结果值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
throw 'Oh NO'
}
let result = main()
console.log(result)
</script>
</body>
</html>报错是因为我们没有
catch
处理异常
这个规则,和
then
方法中成功状态回调函数的返回规则是一样的
4.3.await
表达式
await
右侧的表达式一般为Promise
对象,但也可以是其它的值如果表达式是
Promise
对象,await
返回的是Promsie
成功的值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
let p = new Promise((resolve, reject) => {
resolve('OK')
})
// 右侧为promise的情况
let res = await p
console.log(res)
}
main()
</script>
</body>
</html>如果表达式是其它的值,直接将此值作为
await
的返回值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
let p = new Promise((resolve, reject) => {
resolve('OK')
})
// 右侧为其他类型的数据
let res = await 100
console.log(res)
}
main()
</script>
</body>
</html>
4.4.注意
await
必须写在async
函数中,但async
函数中可以没有await
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
await 100
</script>
</body>
</html>如果
await
的Promise
对象失败了,就会抛出异常,需要使用try catch
捕获处理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
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
async function main() {
let p = new Promise((resolve, reject) => {
reject('Error')
})
// 如果Promise状态为失败,需要使用try catch来捕获异常
try {
let res = await p
} catch(e) {
console.log(e)
}
}
main()
</script>
</body>
</html>
4.5.async
和await
结合实践
需求:读取三个文件的内容,拼接后输出
1.html
1 | 行宫 |
2.html
1 | 寥落古行宫,宫花寂寞红。 |
3.html
1 | 白头宫女在,闲坐说玄宗。 |
纯回调函数实现
1
2
3
4
5
6
7
8
9
10
11const fs = require('fs')
fs.readFile('./resource/1.html', (err, data1) => {
if(err) throw err
fs.readFile('./resource/2.html', (err, data2) => {
if(err) throw err
fs.readFile('./resource/3.html', (err, data3) => {
console.log(data1 + data2 + data3)
})
})
})async + await
实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const fs = require('fs')
const util = require('util')
const myReadFile = util.promisify(fs.readFile)
async function main() {
try {
let data1 = await myReadFile('./resource/1.html')
let data2 = await myReadFile('./resource/2.html')
let data3 = await myReadFile('./resource/3.html')
console.log(data1 + data2 + data3)
} catch(e) {
console.log(e)
}
}
main()首先不需要写那么的回调函数,并且也不需要每一层都对异常进行处理
我们故意写错第一个路径,直接用
try catch
处理异常即可,处理异常的方式变得异常灵活我们在
async
和await
的使用中是看不到回调函数的(在Promise
里面是有的),写起来非常的简洁,就像写同步代码一样(内部的执行是异步的)
4.6.async
和await
结合发送ajax
请求
需求:刷新下页面,就发送一次请求
拿到我们之前封装成Promise
的发送ajax
请求的方法,并使用await
在async
函数中发送请求
1 |
|
结果:
封装成Promise
对象后,代码的书写非常的简洁
并且,将网络请求封装成Promise
对象,已经有现成的工具库了Axios
,这是一个基于Promise
的Ajax
封装的包
使用axios
发送请求时
1 | async categoryList() { |
5.JS
异步之宏队列与微队列
5.1.原理图
5.2.队列分类
JS
中用来存储待执行回调函数的队列包含2个不同特定的队列
异步任务会放入队列中执行
- 宏队列
- 用来保存待执行的宏任务(回调)
- 回调函数的种类:
dom
事件回调ajax
回调- 定时器回调
- 放入宏队列中的回调函数称为宏任务
- 微队列
- 用来保存待执行的微任务(回调)
- 回调函数的种类:
Promise
回调mutation
回调- 监视的是标签属性的改变
- 放入微队列中的回调函数称为微任务
5.3.执行顺序
JS
执行时会区别这2个队列JS
引擎必须先执行所有的初始化同步任务代码- 同步代码还没执行完时,队列里是可以先有任务的,只不过不会执行
- 只有将同步代码全部执行完之后,才会执行队列里面的回调函数
再执行队列里的代码
- 每次准备取出第一个宏任务执行前,都要将所有的微任务一个一个取出来执行
微任务相比较于宏任务,优先执行
1 | setTimeout(() => { // 会立即放入宏队列 |
宏任务与微任务同级之间,是按次序执行的
1 | setTimeout(() => { // 会立即放入宏队列 |
前置宏任务内部如果有微任务,后置宏任务会后执行
1 | setTimeout(() => { // 会立即放入宏队列 |
5.4.Promise
面试题
面试题一
1 | setTimeout(() => { |
面试题二
Promise
的构造器函数的执行时同步的,所以先输入2
1 | setTimeout(() => { |
面试题三
Promise
对象状态改变过一次后,后面就不会再改变了,回调函数也不再被调用,所以最后的resovle(6)
不会有任何效果
1 | const first = () => (new Promise((resolve, reject) => { |
面试题四
当Promise
对象的pending
状态发生改变时,其第一个then
方法的回调函数会进入微队列最后,第二个及后面的then
方法依次执行时,其各自的回调函数会被缓存。
被缓存的回调函数,在第一个then
方法的回调对应的微任务被执行完,该then
方法返回的Promise
对象状态发生了改变时,被缓存的第二个then
方法的回调,进入微队列的末端,等待之前的微任务执行完毕后开始执行。
1 | // 1 7 2 3 8 4 6 5 0 |