1. Promise 是什么
还记得第一次听到 JS 里有个这个东西,本能的中式英语翻译了一下,许诺?
我好好的搬个代码,我许什么诺?花里胡哨的名字
现在回头看看,它状态不可更改的特性也蛮符合这个名字,难道... 莫非这就是程序员的浪漫!!
(狗头)(我不信,阿里 pX 什么事的,果然男人有钱会变坏。我不会,我缺的就是 money)
好了,说回正题,让我们相信爱情的美好,看看这个 Promise 到底是什么
Promise 是异步编程的一种解决方案,比传统的回调函数和事件相比更加的合理,强大
1.1 Promise 产生的原因,解决的痛点
在实际项目中,如果遇到这样一个情况:
我们需要根据第一个网络请求的结果,再去执行第二个网络请求,拿着第二个请求的结果再去执行第三个请求...
不使用 Promise 的代码大概是下面这样子:
请求1 (function (请求结果1) {
请求2 (function (请求结果2) {
请求3 (function (请求结果3) {
...
})
})
})
这样看其实还好,没有很恐怖!
但是如果业务需求再复杂一些,这个请求就要一直叠加下去。
而且更糟糕的是,实际应用中,每一个请求都会对请求数据进行处理,这样代码就会变得十分难看臃肿,而且基本上这段代码无法复用。
这就是大名鼎鼎的 回调地狱
JS 实现异步是通过回调函数实现的。就是把任务的第二段单独写在一个函数里,等到第一段有了结果需要执行第二段时,直接调用这个回调函数。
Promise 就是为了解决回调地狱
问题提出的,它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套改变成链式调用。
21世纪了,打代码一定要有所追求,一定要优雅。所以很多大佬就想办法解决这个问题,要用一个更加优雅的代码组织方式解决异步嵌套的问题。想到了类似下面这种同步的写法,于是 Promise 规范就诞生了。
let 请求结果1 = 请求1();
let 请求结果2 = 请求2(请求结果1);
let 请求结果3 = 请求3(请求结果2);
...
// 还可以复用某一个请求
let 请求结果4 = 请求3(请求结果1);
let 请求结果5 = 请求2(请求结果3);
当然 Promise 也有它的不足,Promise 最大的问题是会代码冗余,一大堆then,原来的语义不清晰,这个之后我们再说。
那下面我们来看看什么是 Promise 规范
1.2 Promise 规范
Promise 构造函数:
Promise 对象是一个构造函数,用来生成 Promise 实例
// 创建一个 promise 实例
const promise = new Promise(function (resolve, reject) {
// ... some code
if (/* 异步操作成功 */) {
resolve(value)
} else {
reject(error)
}
})
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject.。它们又是两个函数。
resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
promise 常规写法:
new Promise (请求1)
.then(请求2(请求结果1))
.then(请求3(请求结果2))
.then(请求4(请求结果3))
.then(请求5(请求结果4))
.catch(...// 处理异常)
比较一下这种写法和上面的回调式的写法。我们不难发现,Promise 的写法更为直观,并且能够在外层捕获异步函数的异常信息。
Promise 常用的方法有哪些?
类方法:
1. Promise.resolve
Promise 对象必须 reslove
一个值才可以被之后的 then
接收。而 then
中的函数要 return
一个结果或者一个新的 Promise 对象 ( then 本身就会返回一个新 promise,如果没有 return 数据,下一个 then 接收的值就是 undefined
),才可以让之后的 then
回调接收
let p = new Promise((reslove) => {
reslove(2)
// return 2 无法传递给下面的then
})
p.then(v => v).then(v => console.log(v)) // 2
2. Promise.reject
3. Promise.race
多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败
4. Promise.all
将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3])
多个 Promise 任务同时执行。如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。
当这个数组里的所有 promise 对象全部变为 resolve 或者有一个 rejected 状态出现的时候,它才会去调用 .then 方法,它们是并发执行的。
实现 Promise.all
先看下 promise.all 的用法
- 接收一个 Promise 实例的数组或具有 Iterator 接口的对象。
- 如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
- 如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调
- 只要有一个失败,状态就变为 rejected,返回值将直接传递给回调。
- all() 的返回值也是新的 Promise 对象。
function promiseAll(promises) {
return new Promise(function (resolve, reject) {
if (!Array.isArray(promises)) {
return reject(new TypeError('arguments must be an array'));
}
let promiseLength = promises.length;
let resolveCounter = 0;
let resolveValues = new Array(promiseLength);
for (let i = 0; i < promiseLength; i++) {
Promise.resolve(promises[i]).then(function (value) {
resolveCounter++;
resolveValues[i] = value;
if (resolveCounter == promiseLength) {
return resolve(resolveValues);
}
}, function (err) {
return reject(err);
})
}
})
}
这里参考了大佬的文章 实现 promise.all
如何实现即使有失败状态,依然返回所有数据? 对每个传入 all 的 Promise 对象都加上 .then
Promise.all([
Promise.resolve(1).then((res) => ({ status: 'suc', res }), (err) => ({ status: 'err', err })),
Promise.reject(2).then((res) => ({ status: 'suc', res }), (err) => ({ status: 'err', err })),
Promise.resolve(3).then((res) => ({ status: 'suc', res }), (err) => ({ status: 'err', err }))
]).then(res => console.log(res), err => console.log(err))
// [{status: "suc", res: 1},{status: "err", err: 2},{status: "ok", res: 3}]
// 函数封装
function handlePromise(promises) {
return promises.map(promise =>
promise.then(res => ({ status: 'suc', res }), err => ({ status: 'err', err }))
)
}
Promise.all(handlePromise([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)]))
.then(res => console.log(res),err=>console.log(err))
这里就不一一展开每个类方法的具体使用方法了,这些可以看阮一峰大神的 ES6 学习。请点击这里
实例方法:
- Promise.prototype.then
作用是为 Promise 实例添加状态改变时的回调函数。
第一个参数是reslove
状态的回调函数
第二个参数(可选)是rejected
状态的回调函数
then
方法返回的是一个新的 Promise 实例,因此可以采用链式写法
then
这两个参数的返回值可以是一下三种情况中的一种
return
一个同步的值,或者undefined
(没有返回一个有效值时,默认返回undefined
);
返回一个 resolved 状态的 Promise 对象,值为同步的值
或undefined
return
另一个 Promise,then
方法将根据这个Promise的状态和值创建一个新的Promise对象返回throw
一个同步异常,then
方法将返回一个rejected
状态的Promise,值是该异常
.then(() => {
...
return 2;
return Promise.resolve(2); // 与上面一样
})
- Promise.prototype.catch
是
.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数
1.3 Promise 优缺点
优点
-
统一异步 API。
-
解决了回调地狱的问题,将异步操作以同步的流程表达出来。
-
链式调用是 Promise 的一大优点,事件处理不可以链式处理。
-
带来了较好的错误处理方式。
缺点
-
无法取消 Promise,一旦新建就会立即执行。并且调用
resolve
或reject
并不会终结 Promise 的参数函数的执行。new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); }); // 2 1 console.log(2)还是会执行 最好是 return resolve(1)
-
不设置回调,Promise 内部抛出的错误不会反应到外部。
-
当处于 Pending 状态时,无法得知进展,是刚开始还是快结束。
-
Promise 真正执行回调时,定义 Promise 的代码部分实际已经运行完了,所以 Promise 的报错堆栈上下文不友好。
1.4 Promise 的实现
简单实现几个重要属性
function myPromise(constructor) {
let self = this;
self.status = 'pending';
self.value = undefined;
self.reason = undefined;
function resolve(value) {
if (self.status === 'pending') {
self.value = value;
self.status = 'resolved';
}
}
function reject(reason) {
if (self.status === 'pending') {
self.reason = reason;
self.status = 'rejected';
}
}
// 捕获构造异常
try {
constructor(resolve, reject)
} catch (e) {
reject(e)
}
}
myPromise.prototype.then = function (onFullfilled, onRejected) {
let self = this;
switch (self.status) {
case 'resolved':
onFullfilled(self.value);
break;
case 'rejected':
onRejected(self.reason);
break;
default:
}
}
这里只是简单模仿下 Promise , 完整的实现机制很复杂,需要钻进去。网上文章很多
2. promise 应用
2.1 Promise 特性
- 立即执行性
- 内部状态机,三种状态
- 状态的不可逆性
- 链式调用
- then 回调异步性,微任务,then 回调是交替执行的
- Promise.resolve() 返回值
- resolve 拆箱, reject 不拆箱
通过 8 题看下 Promise 特性 Click Here
2.2 async / await
详细内容看这里 async 函数
与 promise 区别
- 定义上
-
Promise 是对象,用于表示一个异步操作最终完成或失败,及其结果值
-
async function 是声明语句,定义一个返回 AsyncFunction 对象的异步函数,它会通过一个隐式的 Promise 返回其结果。
-
await 是表达式,用于暂停当前异步函数的执行,等待 await 那个Promise 对象执行完成。
- 中断机制上
-
Promise 内部是状态机,一旦运行无法中止
-
async function 使用 await 中断程序
与 promise 关系
async / await 目的是简化使用多个 promise 时的同步行为,并对一组 Promises 执行某些操作。
Promises 类似结构化回调,async / await 更像结合了 Generators 和 Promise
使用场景
一般在异步操作时使用,搭配使用
-
Promise 提供的工具函数对应的场景,如 Promise.all 并执行一组 Promises
-
async / await 避免了繁杂的 Promise 链式调用,并且更加语义化。
举个栗子:假如有这么个需求:有三个独立的请求,请求成功后改变数据,我们需要再请求成功后拿到最新数据,然后再去执行另一个函数。
function 1 () { return ...new Promise }
function 2 () { return ...new Promise }
function 3 () { return ...new Promise }
funciton 4 () { ... // 在1,2,3之后处理 }
// 在此触发上述函数
async function click () {
await 1();
await 2();
await 3();
4()
}
2.3 Promise 与事件循环
Promise 内部的代码是立即执行的。但是 Promise.then 是微任务,复杂的链式调用非常考验对事件循环的分析。还要注意 return 影响的范围。更多分析看下面的题目
1. Promise 链式调用的执行顺序
我们通过一道妈妈做饭的题目来看 Promise 的链式调用 按照书写习惯,从上到下 log 妈妈做饭的过程
new Promise((resolve, reject) => {
console.log('妈妈要做饭');
resolve();
}).then(() => {
console.log('要买菜');
new Promise((resolve, reject) => {
console.log('去菜市场');
resolve();
}).then(() => {
console.log('买食物');
}).then(() => {
console.log('回家');
})
}).then(() => {
console.log('做菜');
})
先思考下这题输出什么?
来,上菜!
妈妈要做饭
要买菜
去菜市场
买食物
做菜
回家
what!!!难道妈妈不爱我了,在外面做了饭也不给我吃了是吗?
不可能呀!!!
第一次看到这个题目就错了,然后快速看了一遍答案,感觉自己会了,感觉自己又学到了新知识,又变强了!!
后来又看到了这题,果不其然,又错了。
这里就牵扯到了更多 JS 不为人知的小秘密了,恍然大悟,不了解这些秘密,那我永远不理解这道题。
详细分析可以看这篇文章 深度揭秘 Promise 微任务注册和执行过程
文章的第一题扩展一下
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
return new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
})
.then(() => {
console.log("内部第三个then");
})
.then(() => {
console.log("内部第四个then");
})
})
.then(() => {
console.log("外部第二个then");
});
输出结果: 偷懒... 按照书写顺序打印出来
原因:在第一个 then
里 return
了一个 new Promise
,最重要的是,内部的四个 then
是一起被返回的。外层代码下一个 then
必须等到 return
代码执行完成,Promise 状态变更后, 后面的 then
才能执行。return
未完成,第一个 then
就未完成,外部第二个 then
就要等着。
在此题就是要等到 内部第四个 then
执行完成。
证明:给 new Promise
和每个 then
添加返回值,看外部第二个 then
接受谁的返回值就可判断。
会发现,接收的是 内部第四个 then
的返回值,其他的都是 undefined
。感兴趣的同学可以自己打开注释测试下。
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
return new Promise((resolve, reject) => {
console.log("内部promise");
resolve('ss');
})
.then(() => {
console.log("内部第一个then");
// return 1;
})
.then(() => {
console.log("内部第二个then");
// return 2;
})
.then(() => {
console.log("内部第三个then");
// return 3;
})
.then(() => {
console.log("内部第四个then");
return 4;
})
})
.then((r) => {
console.log(r);
});
2. Promise 链式调用的交替执行
- 一个 Promise
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
})
.then(() => {
console.log("内部第三个then");
})
.then(() => {
console.log("内部第四个then");
})
})
.then(() => {
new Promise((resolve, reject) => {
resolve();
})
console.log("外部第二个then");
});
输出
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
内部第三个then
内部第四个then
- 多个 Promise
new Promise(resolve => {
resolve()
}).then(() => {
return new Promise(r => {
console.log('1-1');
r()
}).then(() => {
console.log('1-1 p1')
})
}).then(() => {
console.log('1-2')
})
new Promise(resolve => {
resolve(2);
}).then(() => {
console.log('2-1');
}).then(() => {
console.log('2-2');
}).then(() => {
console.log('2-3');
})
输出
1-1
2-1
1-1 p1
2-2
2-3
1-2
从上面两个案例可以看出 Promise.then 是交替执行的。
有个问题,因为前面的 return Promise,为什么 1-2 最后才输出?
3. return Promise.resolved
相当于占几个微任务
new Promise(resolve => {
resolve()
}).then(() => {
return new Promise(r => {
console.log('promise');
r(5)
})
}).then(r => {
console.log(r)
})
new Promise(resolve => {
resolve(2);
}).then(() => {
console.log('1');
}).then(() => {
console.log('2');
}).then(() => {
console.log('3');
}).then(() => {
console.log('4');
})
// promise 1 2 3 5 4
由之前的学习可知,没有 return
的话,r
的输出在 2 之前。因为 return
延后了两个微任务才输出。
具体分析还是这个文章最后有介绍 深度揭秘 Promise 微任务注册和执行过程
很感谢这篇文章,就不抄写了。引流吧
2.4 Promise 其他题型
之后更新...