目录
- 一. Promise 简介
- Promise 是什么?
- 我们为什么需要 Promise?
- Promise 能解决什么?
- Promise 的特点
- Promise 的缺点
- 二. Promise API 解析
- Promise 的构造方法
- Promise 的实例方法
- Promise 类级别方法
- 三. Promise 深度剖析
- Promise 与 setTimeout
- 多层级 .then()
一. Promise 简介
1. Promise 是什么?
ES6 出现的目标是为了使 JavaScript 语言可以编写大型的复杂应用程序,使之成为企业级的开发语言,Promise 也是其中的一环。
从语法上讲 Promise 是个内置对象,抽象来看,它像是一个容器,里面保存着一个异步操作的执行结果,Promise 提供了一套 API 以保证所有异步操作的统一处理方法。
2. 我们为什么需要 Promise?
我们先来举一个简单的 “栗子”,某人需要做三件事(A,B,C),并且要按照这个顺序依次执行,现在将这个转换为传统的代码方式:
// 首先我们要先定义这三件事(A,B,C)分别为三个函数
// 这三个函数都需要提供一个回调来表示事情的结束
function A(callback) {
console.log('Do A.');
callback();
}
function B(callback) {
console.log('Do B.');
callback();
}
function C(callback) {
console.log('Do C.');
callback();
}
// 现在,如果我想要依次做这三件事我需要这样。。。
A(function() {
// 其它的一些代码
B(function() {
// 其它的一些代码
C(function() {
// 其它的一些代码
});
});
});
可以看出,当涉及到异步操作时,曾经的大部分方式都是靠回调的嵌套来实现的,然而这样的方式造成了几个十分严重的问题:
- 出现了多层回调,当加上业务代码后,将会使整体显得臃肿且凌乱。
- 回调的出现导致逻辑的流程不清晰,不具有可读性和维护性。
- 当嵌套层级过多时会产生大量无用数据的滞留以及数据的混杂。
- 使用回调函数便完全浪费了 return 和 throw 关键字的能力。
综上所述,我们需要一种更好的解决办法在某些(并不是全部)方面来取代回调(callback)方式的异步操作。
3. Promise 能解决什么?
- 编写大型应用时,一种高级、实用且能解决实际问题的高大上语法。
- 避免层层嵌套的回调函数,将异步操作以同步操作的流程表达出来,使逻辑更加清晰。
- Promise 提供统一的接口来进行异步操作。
4. Promise 的特点
Promise 是基于状态的,一个 Promise 对象表示了一个异步操作,而这个操作会有三种状态:Pending(进行中)、Resolved(已成功)和 Rejected(已失败)。只有异步操作的执行结果可以决定是哪一种状态,任何其它操作都无法改变。而且,一旦状态改变,就不会再变化。
Promise 的异步操作不需要像回调一样执行异步操作后立即就会调用回调,Promise 允许任何时候都可以得到这个异步操作的结果。也就是说它其实和事件的机制是完全不同的,事件触发时,如果你没有处于监听状态,那么错过了就再也得不到此次事件的结果,而 Promise 则是将结果状态会凝固,等待你去观察这个异步操作的结果。
5. Promise 的缺点
一旦构造了 Promise 实例就代表执行了一个异步操作,也就是说它会立即执行,并且中途无法取消。
如果不设置回调,Promise 内部抛出的错误,不会反应到外部。(有利有弊,具体看怎么用)
Promise 其实只适合单一的异步程序,并不适合不断发生的事件处理,所以使用时,要找好最适合使用的场景。
二. Promise API 解析
1. Promise 构造方法
Promise 本身就是个构造函数,用来生成 Promise 实例。
Promise 构造函数接受一个函数作为参数,而该函数的两个参数分别为 resolve 和 reject,它们分别是两个函数,由 JavaScript 引擎提供。
resolve 函数的作用是将 Promise 对象的状态从 Pending 变为 Resolved,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。
reject 函数的作用是将 Promise 对象的状态从 Pending 变为 Rejected,在异步操作失败时调用,并将异步操作失败所抛出的错误,作为参数传递出去。
const promise = new Promise((resolve, reject) => {
// 模拟一个异步操作
setTimeout(function() {
if ( /* 异步操作成功 */ ) {
// 异步操作成功,携带载荷将状态变为 resolved
resolve(payload);
} else {
// 异步操作失败,携带错误将状态变为 rejected
reject(error);
}
}, 1000);
});
Promise 实例生成后,异步操作就已经开始执行了。
要注意,resolve 和 reject 的调用是对 Promise 对象状态的变更和数据的传递,并不会影响函数的执行,所以 resolve 和 reject 后面如果有可以正常执行的流程代码,它们仍然会被正常执行。如果不想这样,可以使用 return 强制函数执行的结束。
2. Promise 实例方法
(1) Promise.prototype.then()
Promise 实例生成以后,可以用 then 方法来指定 Resolved 状态和 Rejected 状态的回调函数
promise.then((payload) => {
// Resolved 时执行
}, (error) => {
// Rejected 时执行
});
Promise.prototype.then() 的两个参数都需要传递函数,代表着两个状态转变所要执行的回调,每个回调都可以接受 Promise 对象状态转变时传出的值作为参数。其中,then() 的第二个函数是可选的。
回到一开始我们举的 “栗子”,现在,我想通过 Promise 的方式来实现多个异步程序的依次执行,我们可以在 then() 的调用中显式的返回一个新的 Promise 实例,然后就可以链式的且依次的执行 then(),看下面的代码:
const promise = new Promise((resolve, reject) => {
// 模拟异步程序 1:睡觉 1s
setTimeout(() => {
resolve('睡完觉了');
}, 1000);
});
promise.then((val) => {
console.log(val);
return (new Promise((resolve, reject) => {
// 模拟异步程序 2:吃饭 1s
setTimeout(() => {
resolve('吃完饭了');
}, 1000);
}));
}).then((val) => {
console.log(val);
return (new Promise((resolve, reject) => {
// 模拟异步程序 3:喝水 1s
setTimeout(() => {
resolve('喝完水了');
}, 1000);
}));
}).then((val) => {
console.log(val);
});
通过在 then() 的第一个回调函数中,返回新的 Promise 实例,我们可以用同步的流程将异步的操作表示出,相比使用 callback 更加的逻辑清晰。
(2) Promise.prototype.catch()
Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数,也可以捕获 then() 运行中所抛出的错误。
一般来说,好的方式是不再 then() 里面指定 Rejected 的回调,而是使用 catch() 来对所有错误的捕获(包括 then() 里面抛出的错误)
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
promise.then(() => {
return temp + 2;
}).catch((error) => {
console.error(error);
});
// ReferenceError: temp is not defined
3. Promise 类级别方法
(1)Promise.all()
Promise.all() 方法用于将多个 Promise 实例包装成一个新的 Promise 实例。
Promise.all() 接受一个类数组(具有 Iterator)作为参数,每个成员应为 Promise 实例,如果不是,会调用 Promise.resolve() 方法将其转换为 Promise 实例。
Promise.all() 返回的新的 Promise 实例的状态由传递的类数组成员的共同状态决定:
- 当所有成员的状态都变为 Resolved,Promise 实例的状态才会变为 Resolved,此时所有成员的返回值组成一个数组,传递给 Promise 实例的回调函数
- 只要有一个成员的状态变为 Rejected,Promise 实例的状态就变成 Rejected,此时第一个被 Rejected 的实例的返回值会传递给 Promise 实例的回调函数
如果作为 Promise.all() 参数的 Promise 实例自己定义了 catch 方法,那么它的状态变为 Rejected 时,只会触发它自己的 catch(),不会触发 Promise.all() 的 catch()
(2)Promise.race()
Promise.race() 方法和 Promise.all() 几乎是一样的,只是对状态的处理存在差别。
Promise.race() 返回的新的 Promise 实例的状态由传递的类数组成员中最先改变状态的成员的状态决定。
(3)Promise.resolve()
Promise.resolve() 将现有对象转换为 Promise 对象。
Promise.resolve() 的参数分成四种情况:
- Promise 实例:直接返回这个实例。
- thenable 对象
- thenable 对象指的是具有 then 方法的对象。
- Promise.resolve() 会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then 方法,相当于将这个对象的 then 方法作为 Promise 构造函数的参数,返回一个新的 Promise 实例。
- 其余情况
- Promise.resolve() 返回一个新的 Promise 对象,状态为 Resolved,使用 then() 时,会将 Promise.resolve() 的参数传递给 then() 的回调函数中。
- 这个立即 Resolved 的 Promise 对象,实在本次 “事件循环” 的结束时才开始执行。
(4)Promise.reject()
Promise.reject() 会返回一个新的 Promise 实例,状态为 Rejected。
Promise.reject() 等价于下面的写法:
Promise.reject(obj);
// 等价于
new Promise((resolve, reject) => reject(obj));
Promise.reject() 会将参数原封不动的作为 reject() 的参数。
三. Promise 深度剖析
1. Promise 与 setTimeout
我们来看这样的一段代码:
setTimeout(() => {
console.log(1);
}, 0);
(new Promise((resolve, reject) => {
resolve();
})).then(() => {
console.log(2);
});
console.log(3);
这段代码的运行结果的顺序是 3,2,1,原因如下:
- 对于 setTimeout 我们应该都知道,它是放在下一轮 “事件循环” 的开始,所以它一定要在本轮事件结束后才会执行,也就是输出 1 一定要在输出 3 以后
- 接下来就是 Promise 异步执行的问题,虽然 Promise 实例中并没有真正意义上的异步程序,而是直接将状态变更为 Resolved,且立即使用 then 进行状态的观察,但是实质上,立即 Resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务,所以,输出 2 一定要在输出 3 以后
- 最后,就是 Promise 和 setTimeout 之间的问题,上面也说到了,setTimeout 是在下一轮事件的开始,而 Promise 又实在本一轮事件的结束,所以,很明显,输出 1 要在输出 2 以后
2. 多层级 .then()
一个 Promise 实例可以连续使用 .then() 来绑定回调函数:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
promise.then(() => {
console.log(1);
}).then(() => {
console.log(2);
});
// 1
// 2
如果在 .then() 的回调中显式的指定一个返回值(非 Promise 实例),这个值会被作为下一个链式 .then() 回调函数中的参数:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
promise.then(() => {
console.log(1);
return 'Promise';
}).then((val) => {
console.log(val);
});
// 1
// Promise
如果在 .then() 的回调中返回的是一个 Promise 实例,那么下一个链式 .then() 会在这个 Promise 实例的状态变更时会被调用,并且 resolve 所传递的值会被作为下一个链式 .then() 回调函数中的参数:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
promise.then(() => {
console.log(1);
return (new Promise((resolve, reject) => {
setTimeout(function() {
resolve('Promise');
}, 1000);
}));
}).then((val) => {
console.log(val);
});
// 1
// Promise