Promise 详解
- 示例
- 一个抽奖小游戏
- 原生 JavaScript 实现
- Promise 实现
- 读取文件
- 原生 JavaScript 实现
- Promise 实现
- Promise 对象
- Promise 对象的状态
- Promise 对象的创建
- Promise 对象的状态的改变
- pending 转换为 fulfilled
- pending 转换为 rejected
- 与 Promise 对象相关的 API
- Promise.then()
- Promise.catch()
- Promise.resolve()
- 验证
- Promise.reject()
- 验证
- 转换为 Promise 对象的规则
- Promise 对象转换为 Promise 对象
- 非 Promise 数据转换为 Promise 对象
- 验证
- 抛出错误
- 验证
- Promise 中的链式调用
- 链式调用
- 猜猜看
- 分析
- 异常穿透
- 中断
示例
在进行 Promise 的讲解前,请先通过两个小示例来感受一下 Promise。
一个抽奖小游戏
原生 JavaScript 实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>抽奖</title>
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<button id="start">Start</button>
<script src="./index.js"></script>
</body>
</html>
*{
/* 消除浏览器样式默认存在的内外边距 */
margin: 0px;
padding: 0px;
box-sizing: border-box;
}
body{
/*
设置 body 元素的最低高度为浏览器可视区域的高度,
并为该标签设置了 flex 布局。
*/
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
/* 为按钮设置样式 */
#start{
width: 130px;
height: 40px;
border: 1px solid yellow;
background-color: red;
color: #fff;
font-size: 600;
font-weight: 700;
border-radius: 3px;
/* 为按钮设置过渡动画效果,过渡时间为 1s */
transition: all 1s;
}
/* 为按钮添加鼠标悬停效果 */
#start:hover{
cursor: pointer;
width: 200px;
}
function rand(min, max) {
// 随机数生成函数
// 该函数将返回一个位于区间 [min, max) 的随机数(包括 min 但不包括 max)
return Math.floor(Math.random() * (max - min) + min);
}
// 获取 DOM 元素
const btn = document.querySelector('#start');
// 为 DOM 元素添加点击事件,为模拟抽奖过程中存在的延迟我们的事件调用函数
// 是一个 setTimeout() 函数,我们为这个定时器函数设置了 1s 的延迟。
btn.addEventListener('click', () => {
setTimeout(() => {
if(rand(1, 100) > 30){
alert('恭喜你中奖了!')
}else{
alert('很遗憾,请再接再厉!')
}
}, 1000)
}
)
Promise 实现
Promise 实现与原生 JavaScript 实现的不同点仅位于 JavaScript 代码部分。
function rand(min, max) {
// 随机数生成函数
// 该函数将返回一个位于区间 [min, max) 的随机数(包括 min 但不包括 max)
return Math.floor(Math.random() * (max - min) + min);
}
// 获取 DOM 元素
const btn = document.querySelector('#start');
// 为 DOM 元素添加点击事件,为模拟抽奖过程中存在的延迟我们的事件调用函数
// 是一个 setTimeout() 函数,我们为这个定时器函数设置了 1s 的延迟。
btn.addEventListener('click', () => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
if(rand(1, 100) > 30){
resolve() // 成功执行后需要执行的函数,该函数执行后将 Promise 对象的状态设置为 【成功】
}else{
reject() // 成功执行后需要执行的函数,该函数执行后将 Promise 对象的状态设置为 【失败】
}
}, 1000)
})
p.then(() => {
alert('恭喜你中奖了!')
},
() => {
alert('很遗憾,请再接再厉!')
})
// then() 方法中的第一个参数为成功执行时需要调用的函数,第二个参数为失败执行时需要调用的函数
})
读取文件
原生 JavaScript 实现
由于读取文件需要使用 fs 模块,我们需要通过 Node.js 来运行该示例文件。如果你在浏览器中导入该示例文件很可能会在控制台中看到如下错误:
让我们看看被读取文件中的内容:
《也是微云》
胡适
也是微云,也是微云过后月光明。
只不见去年的游伴,只没有当日的心情。
不愿勾起相思,不敢出门望月。
偏偏月进窗来,害我相思一夜。
// 导入 fs 模块
const fs = require('fs');
fs.readFile('./content.txt', (err, data) => {
if(err) throw(err);
// 由于读取的结果为 buffer 对象,所以我们需要通过
// toString() 函数来将该 buffer 对象转换为字符串。
console.log(data.toString())
})
在终端中 cd 进入该示例文件后,使用如下命令运行该文件(需先安装 Node.js):
node index.js
Promise 实现
const fs = require('fs');
fs.readFile('../content.txt', (err, data) => {
const p = new Promise((resolve, reject) => {
if(err) reject(err);
resolve(data);
})
p.then((value) => {
console.log(value.toString())
},
(reason) => {
throw(reason)
}
)
})
同样,执行该文件需要在终端使用 cd 命令切换到到文件所在的路径后使用如下命令(需要先安装并配置 Node.js):
node index.js
Promise 对象
Promise 对象的状态
Promise 对象代表一个异步操作,该对象存在三种状态,对象的状态不受外界影响。对象的状态仅被异步操作的结果影响,任何其他操作都无法改变这个状态,这也是 Promise 这个名字的由来。
Promise 对象的状态由其 PromiseState 属性进行记录。
状态值 | 描述 |
---|---|
pending | 初始状态,执行的操作既没有被拒绝也没有被同意。 |
rejected | 已拒绝,执行操作失败。 |
fulfilled | 已同意,执行操作成功。 |
状态的变换仅存在两种可能:
- pending 变为 rejected
- pending 变为 fulfilled
注:
- 状态的转换仅能发生一次。状态由 pending 转换为 rejected 或 fulfilled 后将无法再发生改变。
- 对象的状态将决定后续调用的回调函数具体为哪一个。
- 若 Promise 对象的状态由 pending 转换为 rejected 而你没有对此进行捕获(设置 rejected 状态下需要执行的回调函数),则浏览器将抛出错误。类似于这样:
可以通过使用 Promise.catch() 方法来对该状态进行捕获。
Promise 对象的创建
Promise 对象可以通过 new 来调用 Promise() 构造函数来进行创建。
const p = new Promise((resolve, reject) => {
if(1==1):
resolve()
else:
reject()
})
其中:
- 构造函数 Promise() 接收一个参数,你可以通过使用该参数提供一个函数来指定需要执行的 执行器函数。
执行器函数的第一个参数所对应的函数用于将 Promise 对象的状态由 pending 转换为 fulfilled。而第二个参数所对应的函数用于将 Promise 对象由 pending 转换为 rejected。 - resolve() 与 reject() 函数的调用仅改变创建的 Promise 的 PromiseState 属性的值,并不会中止执行器函数的运行。
在下面的这个例子中,Win 将被成功打印:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
if(1==1){
resolve()
console.log('Win')
}
else{
reject()
}
})
</script>
</body>
</html>
- Promise 对象创建的过程中会调用作为参数传递给 Promise 构造函数的函数,所以应该在合适的地方(比如某个事件调用函数中)创建 Promise 对象。
Promise 对象的状态的改变
pending 转换为 fulfilled
通过调用提供给 Promise 构造函数的函数的第一个参数对应的函数 resolve() 即可将创建的 Promise 对象的状态由 pending 转换为 fulfilled。
pending 转换为 rejected
- 通过调用提供给 Promise 构造函数的函数的第二个参数对应的函数 reject() 即可将创建的 Promise 对象的状态由 pending 转换为 rejected。
- 通过在提供给 Promise 构造函数的函数中使用关键字 throw 抛出错误也可以将 Promise 对象的状态属性 PromiseState 由 pending 转换为 rejected。
举个栗子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
if(1==1){
throw 'Error';
}
})
console.log(p)
</script>
</body>
</html>
打印结果:
与 Promise 对象相关的 API
Promise.then()
Promise.then() 接收两个参数,其中第一个参数为 Promise 对象的状态为 fulfilled 时执行的回调函数;第二个参数为 Promise 对象的状态为 rejected 时执行的回调函数。
Promise.catch()
Promise.catch() 与 Promise.then() 类似,Promise.catch() 仅接收一个参数,你可以为该参数提供一个实参(函数),用于指定 Promise 对象的状态为 rejected 时执行的调用函数。
Promise.resolve()
Promise.resolve() 方法用于将基本数据类型或其他引用数据类型转换为 Promise 对象。对于 Promise.resolve() ,存在如下关系:
Promise.resolve('RedHeart');
// 等价于
new Promise((resolve) => {resolve('RedHeart')});
验证
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p1 = Promise.resolve('RedHeart');
const p2 = new Promise((resolve) => {resolve('RedHeart')});
console.log(p1);
console.log(p2);
</script>
</body>
</html>
打印结果:
Promise.reject()
Promise.reject() 方法用于将基本数据类型或其他引用数据类型转换为 Promise 对象。对于 Promise.reject(), 存在如下关系:
Promise.reject('RedHeart');
// 等价于
new Promise((resolve, reject) => {reject('RedHeart')});
验证
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p1 = Promise.reject('RedHeart');
const p2 =new Promise((resolve, reject) => {reject('RedHeart')});
console.log(p1);
console.log(p2)
</script>
</body>
</html>
打印结果:
转换为 Promise 对象的规则
在使用 Promise.then()、Promise.reject() 等函数时可能会将其他数据转换为 Promise 对象,转换过程中将遵循一定的转换规则。
Promise 对象转换为 Promise 对象
如果被转换的 Promise 对象的状态结果为 rejected ,则转换后的 Promise 对象的状态也将为 rejected;如果被转换的 Promise 对象的状态结果为 fulfilled ,则转换后的 Promise 对象的状态也将为 fulfilled。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p1 = Promise.resolve(new Promise((resolve, reject) => {
resolve('RedHeart')
}));
const p2 = Promise.resolve(new Promise((resolve, reject) => {
reject('RedHeart')
}));
console.log(p1);
console.log(p2)
</script>
</body>
</html>
打印结果:
注:
若 Promise.then() 的调用函数中使用 retrun 返回了一个 Promise 对象,则 Promise.then() 返回的 Promise 对象依据此类情况(Promise 对象转换为 Promise 对象)进行判断。
非 Promise 数据转换为 Promise 对象
非 Promise 对象转换为 Promise 对象后的 Promise 对象的结果存在以下情况:
- 经 Promise.resolve() 转换后得到的 Promise 对象的状态为 fulfilled;而经 Promise.reject() 转换后得到的 Promise 对象的状态为 rejected。Promise.resolve() 及 Promise.reject() 得到的 Promise 对象的 PromiseResult 属性值均为提供给他们各自的参数。
- Promise.then() 及 Promise.catch() 的转换结果均由其调用的回调函数来决定。但对于非 Promise 数据转换为 Promise 对象这种情况,得到的 Promise 对象的状态都将为 fulfilled,其 PromiseResult 属性的值则由其调用的回调函数的返回值决定。
验证
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p1 = Promise.resolve('RedHeart');
const p2 = Promise.reject('RedHeart');
var p3 = new Promise((resolve, reject) => {
resolve('RedHeart')
});
var p4 = new Promise((resolve, reject) => {
reject('RedHeart')
});
p3 = p3.then((value) => {
return 'p3'
});
p4 = p4.catch((reason) => {
return 'p4'
});
console.log(p1);
console.log(p2);
console.log(p3);
console.log(p4);
</script>
</body>
</html>
打印结果:
抛出错误
- 经 Promise.resolve() 转换后得到的 Promise 对象的状态为 fulfilled;而经 Promise.reject() 转换后得到的 Promise 对象的状态为 rejected。Promise.resolve() 及 Promise.reject() 得到的 Promise 对象的 PromiseResult 属性值均为提供给他们各自的参数。
- 若回调函数中抛出错误,则 Promise.then() 及 Promise.catch() 产生的 Promise 对象的状态均为 rejected。而他们的 PromiseState 属性值为调用函数中抛出错误时提供的信息。
验证
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p1 = Promise.resolve(() => {
throw 'Error'
});
const p2 = Promise.reject(() => {
throw 'Error'
});
var p3 = new Promise((resolve, reject) => {
resolve()
});
var p4 = new Promise((resolve, reject) => {
reject()
});
p3 = p3.then(() => {
throw 'Error'
});
p4 = p4.catch(() => {
throw 'Error'
});
console.log(p1);
console.log(p2);
console.log(p3);
console.log(p4);
</script>
</body>
</html>
打印结果:
Promise 中的链式调用
链式调用
由于 Promise.then() 及 Promise.catch() 等函数的返回结果仍旧为 Promise 对象,所以我们可以像这样去使用 Promise.then() 的返回结果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
resolve()
});
p.then(() => {
console.log('1')
}).then(() => {
console.log('2')
}).then(() => {
console.log('3')
})
</script>
</body>
</html>
打印结果:
z
注:
- 链式调用过程中的调用结果(由得到的 Promise 对象的 PromiseState 及 PromiseResult 属性决定)需要结合前面讲到的 转换为 Promise 对象的规则 来进行判断。
猜猜看
请观察如下代码并猜测最后两个打印语句的打印结果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK')
}, 1000)
});
p.then(value => {
return new Promise((resolve, reject) => {
resolve('Win')
})
}).then(value => {
console.log(value)
}).then(value => {
console.log(value)
})
</script>
</body>
</html>
打印结果:
Win
undefined
分析
- 第一个 Promise.then()
由于第一个 Promise.then() 的返回值为一个 Promise 对象,所以我们需要先判断这个 Promise 对象的 PromiseState 及 PromiseResult 的值,这两个属性对应的值分别为 fulfilled 及 Win。因此第一个 Promise.then() 返回的 Promise 对象的 PromiseState 及 PromiseResult 的值分别为 fulfilled 及 Win。 - 第二个 Promise.then()
由于第一个 Promise.then() 返回的 Promise 对象的 PromiseState 及 PromiseResult 的值分别为 fulfilled 及 Win。因此传递给第二个 Promise.then() 的 value 对应的值为 Win。fulfilled 决定能不能调用第二个 Promise.then() 函数,如果第一个 Promise.then() 的 PromiseState 对应的值为 rejected,则第二个 Promise.then() 将不会被调用。 - 第三个 Promise.then()
由于第二个 Promise.then() 发生了 非 Promise 数据转换为 Promise 对象,所以第二个 Promise.then() 的结果为 Promise 对象,该对象的PromiseState 及 PromiseResult 的值分别为 fulfilled 及 undefined。
异常穿透
我们可以在链式调用的最后一环添加一个 Promise.catch() 函数以应对可能产生的 rejected 状态的 Promise 对象。就像这样:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK')
}, 1000)
});
p.then(value => {
return new Promise((resolve, reject) => {
reject()
})
}).then(value => {
console.log(value)
}).then(value => {
console.log(value)
}).catch(reason => {
console.log('Lose')
})
</script>
</body>
</html>
打印结果:
Lose
中断
对于下面的代码,如果你希望打印的结果为:
1
2
而不是:
1
2
3
你该怎么做?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
resolve()
});
p.then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
})
</script>
</body>
</html>
我们可以尝试在第二个 Promise.then() 中的调用函数中返回一个特定状态(在这个例子中需要的 Promise 对象的状态应该为 pending 或 rejected)的 Promise 对象来中断链式调用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
resolve()
});
p.then(() => {
console.log(1)
}).then(() => {
console.log(2)
return new Promise((resolve, reject) => {
reject()
})
}).then(() => {
console.log(3)
})
</script>
</body>
</html>
打印结果:
使用如下代码也可以在适当的时机中断链式调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
resolve()
});
p.then(() => {
console.log(1)
}).then(() => {
console.log(2)
return new Promise(() => {})
}).then(() => {
console.log(3)
})
</script>
</body>
</html>
打印结果:
1
2