最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。
前言
Promise是ES6带来的重量级新特性。就算很多开发者在日常编码中,不习惯使用es6的某些新语法,但相信大部分都会请不自禁地使用Promise。因为他不仅是优化了我们的代码复杂度,甚至改变了我们的编码风格。
概念
Promise是es6的一个新的内置类,用来管理异步任务(当然不一定是异步)。它是异步编程风格的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大,解决了如回调地狱等问题。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。
用法
new Promise(【executor】)。函数参数不能为空,否则报错。
let p1 = new Promise();
// Uncaught TypeError: Promise resolver undefined is not a function
// executor函数应该有2个参数resolve, reject
let p = new Promise(function(resolve, reject){
// do something...
});
Promise实例
执行new之后我们会得到一个promise实例。
属性
他有2个内置属性,一个promise的状态,一个是promise的result。
- [PromiseState]]: 实例状态,pending准备状态 fulfilled/resolved成功态 rejected失败态。通过resolve, reject方法修改。注意:状态一旦设置了就不能再修改了。
- [[PromiseResult]] : 实例的值,executor执行后的结果。通过resolve, reject方法修改。
公共属性方法 Promise.prototype
- then
- catch
- finally
- Symbol(Symbol.toStringTag)
then
then中填2个函数参数,onfulfilledCallback和onrejectedCallback。
p.then(onfulfilledCallback,onrejectedCallback);
onfulfilledCallback和onrejectedCallback分别是Promise executor的执行结果的后续处理。如果Promise执行了resolve就会触发onfulfilledCallback并把结果传递,同理onrejectedCallback就是reject的后续。
p1.then(result => {
//Promise执行了resolve,result是resolve的结果
console.log('成功-->', result);
}, reason => {
//Promise执行了reject,reason是reject的结果
console.log('失败-->', reason);
});
值得注意的是,p.then有返回值的。他会返回一个新的Promise实例。而这个实例的状态和结构遵循以下:
- 无执行的是基于p1.then存放的onfulfilledCallback/onrejectedCallback两个方法中的哪一个。只要方法执行不报错且没有返回值,就会触发resolve。返回值是undefined。
var p1 = new Promise((resolve,reject)=>{
resolve('p ins')
});
// 执行onfulfilledCallback
var p2 = p1.then((res)=>{
console.log(res);
})
console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: undefined
//执行onrejectedCallback
var p3 = new Promise((res,rej)=>{
rej('wrong!');
}).then((res)=>{},reason=>{
console.log(reason);
})
console.log(p3);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: undefined
- 如果方法执行报错,p2的 [[PromiseState]]设为rejected 。 [[PromiseResult]]就是报错原因。
var p2 = p1.then((res)=>{
throw '出错了!';
})
console.log(p2);
//[[PromiseState]]: "rejected"
//[[PromiseResult]]: "出错了!"
- 如果方法中返回一个全新的Promise实例,则“全新的Promise实例”的成功和失败决定then实例的成功和失败。
var p2 = p1.then((res)=>{
return new Promise((resolve,reject)=>{
resolve(10);
});
})
console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: 10
var p2 = p1.then((res)=>{
return new Promise((resolve,reject)=>{
reject(-10);
});
})
console.log(p2);
//[[PromiseState]]: "rejected"
//[[PromiseResult]]: -10
- 如果主动返回非promise,则 [[PromiseState]]设为fulfiled。 [[PromiseResult]]是主动返回的值。
var p2 = p1.then((res)=>{
return 100
})
console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: 100
- 如果传入的不是函数,会直接返回p1的结果
var p2 = p1.then(111)
console.log(p2);
//[[PromiseState]]: "fulfilled"
//[[PromiseResult]]: 'p ins'
顺延/穿透
既然then会返回一个新的promise实例,因此我们就可以利用这个机制,不断地then下去。实现同步逻辑写法。这种现象就是【顺延】或者【穿透】。
new Promise((resolve, reject) => {
// resolve('OK');
reject('NO');
}).then(result=>resul ,reason=>Promise.reject(reason))
.then(result => {
console.log('成功-->', result);
}, reason => {
console.log('失败-->', reason);
}).then(result => {
console.log('成功-->', result);
}, reason => {
console.log('失败-->', reason);
});
catch
正常情况下,executor的执行,如果触发的是reject。promise是会往程序抛错的。
new Promise((resolve,reject)=>{
reject(100)
});
// Uncaught (in promise) 100
因此,promise实例很贴心的为我们提供了catch方法。它是promise用来捕获错误的公共方法,我们不需要自己另外try,catch。
new Promise((resolve,reject)=>{
reject(100)
}).catch(err=>{
console.log(100);
})
// 100
当然它还可以配合穿透机制使用,在then的队列中只要其中一个抛错了,就会触发catch。并不是按顺序执行。
new Promise((resolve, reject) => {
resolve('OK');
}).then(result => {
// 抛错直接执行catch
return Promise.reject('xx');
}).then(result => {
// 由于前面已经抛错,这里不会再执行了
console.log('成功-->', result);
}).catch(reason => {
// 执行输出xx
console.log('失败-->', reason);
});
Promise.all
Promise.all是一个挂在Promise上的方法。入参是一个promise实例数组。返回值是一个新的promise实例A,这个实例A是成功还是失败,取决于数组中的每一个promise实例是成功还是失败,只要有一个是失败,A就是失败的,只有都成功A才是成功的。
function fn(interval) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(interval);
}, interval);
});
}
let p1 = fn(3000);
let p2 = fn(1000);
let p3 = fn(2000);
Promise.all([p1, p2, p3]).then(results => {
// 不论谁先知道状态,最后结果的顺序和传递数组的顺序要保持一致
console.log(results);
// 输出[3000,1000,2000]
}).catch(reason => {
console.log(reason);
});
let p1 = fn(3000);
let p2 = fn(1000);
let p3 = Promise.reject(0);
Promise.all([p1, p2, p3]).then(results => {
console.log(results);
}).catch(reason => {
// 处理过程中,遇到一个失败,则All立即为失败,结果就是当前实例失败的原因
console.log(reason);
// 输出0
});
Promise.race
Promise.race同样是挂在Promise上的方法,同样传入promise实例数组。返回一个新的promise实例R。实例R的结果取决于,最先数组中最先完成的promise实例的结果。
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
// p2先完成,直接作为结果
console.log(value);
// 输出 'two'
});
重点:promise的同步与异步
现在我们已经了解了Promise的基础使用了,下面我们来看看Promise的难点——在Promise中的同步与异步。这部分的知识点常见于面试题的笔记部分。
那些是异步?
我们先来区分promise中究竟哪些是同步执行的,哪些是异步执行的。很多人也许不知道,exector本身是同步执行的。只是他的结果实例的then和catch等结果回调才是异步的。
举个例子:
console.log(1);
var p1 = new Promise((resolve,reject)=>{
console.log(2);
resolve('hi');
console.log(3);
})
p1.then((res)=>{
console.log(res);
})
console.log(4);
因为executor的内容是同步执行的,所以2,3会在4之前输出,但then的内容是异步的,会最后输出。
// 1
// 2
// 3
// 4
// hi
回调方法必须在实例的状态改变之后才会执行
then的内容是否执行,得看实例本身的状态是否已经改变了。只要改变了之后,函数才会进入执行队列。
new Promise(resolve => {
console.log(1);
resolve();
}).then(() => {
console.log(2);
new Promise(resolve => {
console.log(3);
resolve();
}).then(() => {
console.log(4);
}).then(() => {
console.log(5);
});
}).then(() => {
console.log(6);
});
在上述的代码中,执行流程应该是:
- 第一次程序执行时,同步内容输出1,并且修改实例为成功状态。
- 后面接的2个then会进入待执行的状态。但执行时机取决于实例的状态。
- 这里因为resolve已经修改了状态。所以会执行第一个then。
- 输出2,同步执行第二个executor,输出3,修改成功状态。并把4,5进入待执行状态。
- 注意这是我们待执行的队列中有6,4,5。
- 由于4的状态已经被3改变,所以输出4。修改了5的状态为成功。
- 同时由于2中的代码已经执行完了,并肯定没有抛错,所以6的状态转为成功。
- 此时待执行队列中6和5都是成功状态了,可是因为6是先进入队列的,所以先输出6,再输出5。
// 答案是
// 1
// 2
// 3
// 4
// 6
// 5
难点,这里6其实是先进入待执行队列的,但根据我们上面学习的内容知道,then返回的promise实例的结果假如没有返回,就得看then内代码有没有报错。所以6会等then内容的promise全部执行完,确保没抛错了之后,修改状态为成功。又由于6比5先进入队列,所以即使两者状态同为成功,6会比5先输出。
手写Promise
手写Promise在近年来也成为了一道面试题出现,既然我们已经掌握了promise的核心机制。接下来我们就试着手写Promise吧。
Promise类
class MyPromise{
constructor(executor){
// 设置实例初始状态
this.PromiseState = 'pedding';
this.PromiseResult = undefined;
this.onFulfilled = [];//成功的回调
this.onRejected = []; //失败的回调
// 定义resolve,reject
const resolve = (result)=>{
if(this.PromiseState==='padding'){
this.PromiseState = 'fulfilled';
this.PromiseResult = result;
this.onFulfilled.forEach(item=>item());
}
};
const reject = (reason)=>{
if(this.PromiseState==='padding'){
this.PromiseState = 'rejected';
this.PromiseResult = result;
this.onRejected.forEach(item=>item());
}
};
// 如果exector执行抛错,直接当reject处理
try{
executor(resolve,reject);
}catch(error){
reject(error);
}
}
// 定义then方法
then(onfulfilledCallback,onrejectedCallback){
// 处理入参,防止不是函数或者undefined
const fulfilledCallback = typeof onfulfilledCallback === 'function'? onfulfilledCallback: (result)=>result;
const rejectedCallback = typeof onrejectedCallback === 'function'? onrejectedCallback: (reason)=>{ throw reason};
// 返回一个新的promise实例
const subPromiseInst = new MyPromise((resolve,reject)=>{
// 1. 因为要有异步的效果,这里有setTimeout制作异步,但大家要留意setTimeout是宏任务,promise应该是微任务。
// 2. 要try catch是因为如果then的回调本身执行抛错,统一当reject处理
// 3. resolvePromise核心函数负责处理then结果和新promise实例的关系
if(this.PromiseState = 'fulfilled'){
// 状态已经成功可以直接执行
setTimeout(()=>{
try{
const thenResult = fulfilledCallback(this.PromiseResult);
resolvePromise(subPromiseInst,thenResult,resolve,reject);
}catch(error){
reject(error)
}
},0)
}else if(this.PromiseState = 'rejected'){
// 状态已经失败可以直接执行
setTimeout(()=>{
try{
const thenResult = rejectedCallback(this.PromiseResult);
resolvePromise(subPromiseInst,thenResult,resolve,reject);
}catch(error){
reject(error)
}
},0)
}else if(this.PromiseState = 'padding'){
// 状态还没设置,先加到队列中
this.onFulfilled.push(()=>{
setTimeout(()=>{
try{
const thenResult = fulfilledCallback(this.PromiseResult);
resolvePromise(subPromiseInst,thenResult,resolve,reject);
}catch(error){
reject(error)
}
},0)
});
this.onRejected.push(()=>{
setTimeout(()=>{
try{
const thenResult = rejectedCallback(this.PromiseResult);
resolvePromise(subPromiseInst,thenResult,resolve,reject);
}catch(error){
reject(error)
}
},0)
});
}
});
return subPromiseInst;
}
}
核心
resolvePromise核心函数是处理then和返回的新promise实例的关系的。每次执行then都会返回一个新的promise实例,而返回的内容是根据then的内容执行决定的。thenResult的可能性有:
- 返回原始数据类型或者undefined
- 返回普通引用数据类型
- 返回一个新的promise实例
同时我们需要一个标识(used),来确保成功回调和失败回调只会执行其中一个。
所谓then结果和新实例的关系,其实就是我们要知道究竟要执行新实例的resolve方法还是reject方法!
// 处理核心
function resolvePromise(promiseIns, thenResult, resolve, reject) {
let self = this;
// 避免死循环
if (promiseIns === thenResult) {
reject(new TypeError('死循环'));
}
// thenResult返回的是引用类型
if (thenResult && typeof thenResult === 'object' || typeof thenResult === 'function') {
let used;
try {
let then = thenResult.then;
// 判断then中返回是是不是又是promise实例
if (typeof then === 'function') {
// 如果返回的是新的promise实例,得根据实例的结果来决定返回的内容
then.call(thenResult, (newPromiseInsThenResult) => {
// 如果新实例then的成功回调被执行了,说明新实例的状态是成功
if (used) return;
used = true;
// 递归新promise实例
// 虽然我们知道新promise实例是成功的,但不知道要resolve传递什么内容,所以还得再进行一次。
resolvePromise(promiseIns, newPromiseInsThenResult, resolve, reject);
}, (newPromiseInsThenReason) => {
// 如果新实例的then的失败回调被执行了,说明新实例的状态是失败
if (used) return;
used = true;
reject(newPromiseInsThenReason);
});
}else{
// 如果then中返回是普通数据,可以直接执行resolve并传递值
if (used) return;
used = true;
resolve(thenResult);
}
} catch (error) {
// 确保resolve和reject只执行一个
if (used) return;
used = true;
reject(error);
}
//如果then返回的是原始数据类型或者undefined,直接执行resolve,并传递结果
} else {
resolve(thenResult);
}
}
总结
今天我们了解了promise的核心规律,这部分的内容可以帮助我们解决几乎所有的promise相关的笔试。同时我们也深入尝试了手写promise的实现,需要注意的是虽然我们的异步实现用的是setTimeout, 但promise的异步事实上是微任务。这个要记得。