Promise理解
Promise是javascript中进行异步编程的新解决方案
- 从语法上来说:Promise是一个构造函数
- 从功能上来说:Promise对象用来封装一个异步操作并可以获取其成功/失败的结果值。
异步编程:
- 定时器
- ajax
- fs 文件操作(nodejs环境)
- 数据库操作(nodejs环境)
好处:
- 支持链式调用
- 调用Promise构造函数会返回一个Promise对象,该对象可以调用Promise上相关的方法(then、catch、finally),调用这些方法也会返回一个已经解决的Promise实例,所以支持链式调用。
- 解决回调地狱的问题
- 回调地狱:不便于阅读、不便于异常处理
- 回调地狱的产生主要源于:需要获取到异步操作的结果,并利用该结果进行新的异步操作,以此类推,会产生回调地狱的编码风格。
- 获取异步操作结果更方便
- 在Promise出现之前,对于异步操作,都是利用回调函数来进行操作的,利用回调函数获取异步操作的结果,并利用回调函数对结果进行业务处理,所以回调函数的编写必须存在于当前异步操作中,且异步操作的结果只限于当前回调函数,因此相比来说,不是那么的灵活。而使用Promise,通过返回的Promise实例对象,来对异步操作的结果更加自由的操作,并且回调函数可以在then/catch/finally任意地方进行编码处理结果。
Promise语法
promise的状态改变
promise的状态指的是:promise实例对象中的一个属性:【PromiseState】,一共有三个属性值
- pending 未决定的,初始化时的默认值
- resolved / fullfilled 成功
- rejected 失败
promise的状态只可能发生以下几种情况:
- pending => resolved
- pending => rejected
且状态一旦发生变化,就不可逆。
例子:
let pro = new Promise(function (resolve, reject) {});
console.log(pro);
在浏览器环境中进行查看,node环境只能查看状态,所以无法看到对象的其他属性值
如何更改promise状态呢?
可以通过传给Promise构造函数的初始化函数中的参数resolve/reject来改变状态
- resolve: pengding=>fulfilled/resolved
- reject: pengding=> rejected
- throw错误: pending=>rejected
promise对象的值
promise对象的值指的是:实例对象中的一个属性:【PromiseResult】,该属性存储的是异步任务成功/失败的结果。
能修改该属性的方法只有两个:
- resolve
- reject
通过以上方式修改【PromiseResult】的值,后期可以通过then/catch方法的调用获取到该属性的值。
promise工作流程
graph TD
A[new Promise--pending状态]
A --> C{执行异步操作}
C -->|成功 执行resolve| D[修改promise状态为resolved]
C -->|失败 执行reject| E[修改promise状态为rejected]
D -->F[执行then方法中第一个回调函数]
E -->G[执行then方法中第二个回调函数或者执行catch方法中回调函数]
F -->H[以上方法执行完成之后 返回一个新的promise实例对象]
G-->H
Promise API
- 初始化Promise
let pro = new Promise(excutor)
其中:excutor为执行器函数,执行器会在Promise内部立即 同步调用,而异步操在执行器中执行。
什么意思呢?
当js执行到当前行的时候,excutor函数内部有同步代码,则会立即执行,而如果遇到了异步操作的代码,则会放在任务队列中。
除此之外,执行器默认接收两个参数,一个是resolve函数,一个是reject函数,开发者可以利用这两个参数在异步任务成功/失败的时候进行调用。
- Promise.prototype.then()
该方法会在异步任务执行完成,且异步任务中调用了resolve/reject,同时该任务处于任务队列中最头部位置时,进行调用。
接收两个参数:
- onResolved:异步任务成功时的回调函数
- onRejected:异步任务失败时的回调函数
该方法会返回一个新的Promise实例对象。
那么返回的新的Promise实例对象的状态以及值是如何指定的呢?
以下不管是成功还是失败回调函数都是一样的规则
- 如果回调函数中抛出异常(throw),则返回一个失败的Promise实例对象,值为抛出异常的描述信息。
- 如果回调函数中return一个非Promise对象值,则返回一个成功的Promise实例对象,值为return的值。
- 如果回调函数中return一个Promise对象值,状态和值由Promise对象的状态和值决定。
注意:如果回调函数中既没有throw,也没有return一个值,那么返回的新的Promise实例对象状态依然是resolve,但是其值为undefined
例子:
let p = new Promise((resolve, reject) => {
reject(12);
});
let re = p.then(value => {
console.log(value);
}, reason => {
return Promise.resolve('0')
});
console.log(re); // 【PromiseState】: resolve 【PromiseResult】: 0;
链式调用:
由于then方法是可以指定Promise实例中异步任务成功/失败时的回调函数,那也就是对任何的Promise实例对象都可以调用then方法对这两种状态进行处理。从以上的实例可以看出,then方法会返回一个新的Promise实例对象,所以我们可以通过链式调用来处理需要并联进行的操作。
比如:
let p = new Promise((resolve, reject) => {
reject(12);
});
p.then(value => {
console.log(value);
}, reason => {
return Promise.resolve('0')
}).then(value => {
console.log(value);// 0
}).then(value => {
console.log(value);// undefined
});
- Promise.prototype.catch()
该方法会在异步任务失败时进行调用,属于一个语法糖,其内部实现还是使用了then方法
该方法会返回一个新的已解决的Promise实例对象。
需要注意的是,以上对失败的处理(then的第二个参数/catch),都是异步处理的,不能使用同步的方法try/catch进行处理
- Promise.resolve()
该方法属于Promise构造函数,而不属于Promise实例对象,调用该方法可以立即得到一个Promise的实例对象。该实例对象的状态采用以下规则确定:
- 如果传入的参数为 非Promise类型的对象,则返回的对象为成功状态的Promise实例对象
- 如果传入的参数为 Promise类型的对象,则返回的对象状态根据传入的参数的状态来决定。
比如:
const p = Promise.resolve(new Promise((resolve,reject)=>{
reject('1');
}))
则p的【PromiseState】的值为: Rejected。
- Promise.reject()
类似于Promise.resolve(),也是属于Promise构造函数的方法,但是这个方法,总是获得一个失败的Promise期约,且传递的值作为该方法的返回值,不管传递什么参数都是如此。它不同于Promise.resolve对参数类型不同返回的期约是不同的。
比如:传入一个已经解决的期约,返回的是一个失败的期约,且其值为已解决的期约。
let pro = Promise.reject(new Promise(function (resolve, reject) {
resolve('ok');
}))
console.log(pro);
结果:
- Promise.all()
是对多个期约作用的一个方法。
参数:期约的集合
返回状态(【PromiseState】):当且仅当所有期约都是已解决的期约,才返回已解决的期约。只要有一个期约为失败的期约,那么返回一个失败的期约。
返回的结果值(【PromiseResult】):当且仅当所有期约都是已解决的期约,结果值为所有已解决的结果值的集合。只要有一个期约为失败的期约,结果值为:最开始返回失败期约的结果值。
例子(失败案例):
let p1 = Promise.resolve('1');
let p2 = Promise.reject('ii');
let p3 = Promise.reject('ok');
let p4 = Promise.all([p1,p2,p3]);
console.log(p4);
结果:
例子(成功案例):
let p1 = Promise.resolve('1');
let p2 = Promise.resolve('ii');
let p3 = Promise.resolve('ok');
let p4 = Promise.all([p1,p2,p3]);
console.log(p4);
结果
- Promise.race()
类似于Promise.all(),也是一个对多个期约作用的方法。
参数:期约的集合
返回状态(【PromiseState】):由第一个返回的期约的状态所决定。
返回值(【PromiseResult】):由第一个返回的期约的返回值所决定。
例子:
let p1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(1);
}, 1000);
})
let p2 = Promise.reject(2);
let p3 = Promise.resolve(3);
let p4 = Promise.race([p1,p2,p3]);
console.log(p4);
结果:
由于第一个期约需要等待1s之后,才能返回,而第二个期约能够直接返回,所以期约状态为第二个的状态,期约返回值为第二个的返回值。
常见问题
- 一个promise指定多个成功/失败回调函数,都会调用么?
当promise对象状态发生变化时,对应状态的回调函数都会被调用。
比如:
let p = new Promise((resolve, reject) => {
resolve(12);
});
p.then(value => {
console.log(value);
});
p.then(value => {
console.log(value);
});
查看打印结果:
可以看到,当promise状态改变为成功时(resolve),指定的两个成功回调函数(then的第一个参数),都被调用了。
- 改变promise状态和指定回调函数谁先谁后?
- 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调。这里的指定回调指的是:初始化回调函数(不一定执行)
- 什么情况下哪个事件先发生呢?
-
先改状态再指定回调
- 执行器函数中执行的是同步任务,同步任务直接调用了resolve/reject
- 延迟更长时间才调用then
-
先指定回调再改变状态
执行器函数中执行的异步任务。
从上面我们总结的情况可以看出:由于js执行机制的原因,先执行同步任务,遇到异步任务放到异步队列中,等同步任务执行完毕,再通过事件循环执行异步任务。所以如果是在同步任务中直接调用resolve/reject,状态就会立即改变,接着再初始化回调函数。但是如果是在异步任务中调用resolve/reject,那么异步任务会在同步任务之后执行,此时就会先初始化回调函数,再改变状态。
- 那什么时候获取的数据呢?
- 如果先指定回调,当状态改变时,回调函数就会调用,获取到数据
- 如果先改变状态,当指定回调函数的同时,回调函数而会被调用,得到数据。
- promise异常传透
当使用promise的then链式调用时,可以在最后指定失败的回调。前面任何操作出了异常,都会传到最后失败的回调中处理。但是这里有个前提,如果一旦该异常出现后,在最后失败回调之前调用了失败回调,则该异常就会被捕获。
比如:
let p = new Promise((resolve, reject) => {
reject(12);
});
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);// 异常被捕获了
}).then(value => {
console.log(value);
}).catch(reason => {
console.log(1);
console.log(reason); // 无法到达这里。
});
我们也可以统一处理异常,也即在最后添加失败回调。
let p = new Promise((resolve, reject) => {
reject(12);
});
p.then(value => {
console.log(value);
}).then(value => {
console.log(value);
}).catch(reason => { // 这里也可以使用then的第二个参数
console.log(reason); // 捕获到了异常
});
- 中断Promise链
Promise链:利用Promise实例对象可以链式调用then/catch的特性所形成的链。
需求:当仅仅想执行当且回调函数,不想再执行之后的链式调用。
做法:返回一个pending状态的Promise实例对象。
let p = new Promise((resolve, reject) => {
resolve(12);
});
p.then(value => {
console.log(value);
return new Promise(() => {});
}).then(value => {
console.log(value);
}).catch(reason => {
console.log(reason);
});
为什么返回pending状态的Promise实例对象可以实现呢?原因:Promise实例一旦发生变化(pending->resolve/reject),都会去调用与之对应的回调函数,也即总会去执行then/catch方法。而pending状态不会发生以上状况。
使用Promise封装异步操作
promise封装:fs模块
const fs = require('fs');
let promise = new Promise(((resolve, reject) => {
fs.readFile('../html/test.html', (err, data) => {
if (err) reject(err);
resolve(data);
})
}))
promise.then(data => {
console.log(data.toString());
}, reason => {
console.log(reason);
})
node中util模块里面的函数:promisify
该函数的作用:将异步函数自动进行Promise封装,并且返回一个Promise实例,这样就不需要以上方式手动封装。
使用方法:
- 引入util模块
const util = require('util');
- 将待封装的异步操作传入到promisify函数中
let mineReadFile = util.promisify(fs.readFile);
注意:此处不需要传入异步操作的参数
- 使用已封装的函数,使用方法和原来异步操作函数一致(只是对其进行了封装而已)
mineReadFile('../html/test.html')
.then(data => {
console.log(data.toString());
}, reason => {
console.log(reason)
})
- 完整的代码
const fs = require('fs');
const util = require('util');
let mineReadFile = util.promisify(fs.readFile);
mineReadFile('../html/test.html')
.then(data => {
console.log(data.toString());
}, reason => {
console.log(reason)
})
仿写PromiseAPI
class Promise {
constructor(executor) {
this.PromiseState = 'pending';
this.PromiseResult = null;
// 保存异步任务的回调函数
this.callbacks = [];
// 保存当前实例的上下文
const self = this;
function resolve(data) {
// 判断状态,保证状态只会被修改一次
if (self.PromiseState !== 'pending') return;
// 1. 改变状态
self.PromiseState = 'fulfilled';
// 2. 改变值
self.PromiseResult = data;
// 处理异步任务的结果的回调
// 异步处理结果
setTimeout(() => {
self.callbacks.forEach(item => {
item.onResolved(data);
})
})
}
function reject(data) {
// 判断状态,保证状态只会被修改一次
if (self.PromiseState !== 'pending') return;
// 1. 改变状态
self.PromiseState = 'rejected';
// 2. 改变值
self.PromiseResult = data;
// 处理异步任务的结果的回调
// 异步处理结果
setTimeout(() => {
self.callbacks.forEach(item => {
item.onRejected(data);
})
})
}
// 处理throw的方法
try {
executor(resolve, reject);
} catch (e) {
// 改为失败的状态
reject(e);
}
}
// 添加then方法
then(onResolved, onRejected) {
// 保存上下文
const self = this;
// 判断回调函数参数,如果不是函数,则给其一个默认值,实现异常穿透,等效于帮用户将异常传递到最后的异常捕获的回调函数中进行处理
// 如果没有这一步,当该实例reject/throw的时候,且失败处理放在最后,那么之前的then方法都会因为onRejected为undefined而抛出异常
// 虽然最后还是会被catch捕获,但是失败值会是:onRejected is not a function,而不是期待输出的失败值。
if (typeof onRejected !== 'function') {
onRejected = reason => {
throw reason;
}
}
// 处理如果then方法中什么参数都没有传递的情况,防止报错。注意:错误的创建都需要用户去定义,而不是源码本身去创建用户不知道的错误。
if (typeof onResolved !== 'function') {
onResolved = value => value;
}
// 返回一个新的Promise实例
return new Promise((resolve, reject) => {
// 封装函数
function callback(type) {
// 处理throw的情况
try {
// 获取回调的执行结果
let result = type(self.PromiseResult);
// 如果是Promise实例,新的Promise实例对象则需要根据当前Promise实例状态来指定
if (result instanceof Promise) {
// 由于result是Promise实例对象,所以也可以调用then方法,根据result实例对象的状态会调用对应的回调函数
result.then(value=> {
resolve(value);
}, reason => {
reject(reason);
})
} else {
// 不是Promise实例对象,则直接将新的Promise实例对象状态设为成功
resolve(result);
}
} catch (e) {
// 如果抛出异常,则将状态改为失败【rejected】
reject(e);
}
}
// 以下只针对同步方式
// 成功执行回调,并把成功结果传入
if (this.PromiseState === 'fulfilled') {
// then方法是异步任务的特性
setTimeout(() => {
callback(onResolved);
})
}
// 失败执行回调,并把失败结果传入
if (this.PromiseState === 'rejected') {
setTimeout(() => {
callback(onRejected);
})
}
// 针对异步任务,保存回调函数,待状态发生变化的时候去执行
if (this.PromiseState === 'pending') {
// 针对给该Promise实例调用多个then的情况,将其放在数组里面,之后状态改变的时候,逐个去执行
this.callbacks.push({
onResolved: function () {
callback(onResolved);
},
onRejected: function () {
callback(onRejected);
}
})
}
})
}
// 添加catch方法
catch(onRejected) {
return this.then(undefined, onRejected);
}
// 添加resolve方法
static resolve(type) {
return new Promise((resolve, reject) => {
// Promise实例对象,则根据实例对象状态决定
if (type instanceof Promise) {
type.then(value => {
resolve(value);
}, reason => {
reject(reason);
})
} else {
// 非Promise实例对象,则返回成功的Promise实例对象
resolve(type);
}
})
}
// 添加reject方法
static reject(value) {
return new Promise((resolve, reject) => {
reject(value);
});
}
// 添加all方法
static all(promises) {
if (!Array.isArray(promises)) {
// 如果不是数组,则抛出异常
throw new Error(`${typeof promises} is not iterable (cannot read property Symbol(Symbol.iterator))`);
}
return new Promise((resolve, reject) => {
// 声明一个标识为,表示实例成功的个数,当前仅当count === promises.length的时候,才会将返回的Promise实例对象状态改为成功
// 只要promises中有一个对象的状态改为失败,则立即改变当前的Promise实例对象为失败
let count = 0;
// 保存成功实例对象的返回的结果
let arr = [];
for (let i = 0; i < promises.length; i++) {
// 每个Promise实例都有一个then方法,可以通过then方法获取到当前实例的状态变化
promises[i].then(value => {
// 当前实例变为成功,必然会走这
// 只要成功,则增加1 表明成功的个数
count++;
// 保存成功实例对象的返回结果,注意顺序问题
arr[i] = value;
if (count === promises.length) {
// 只要实例对象全部都成功,才改变实例对象状态,且把保存的所有实例结果的数组作为当且实例对象的值传入
resolve(arr);
}
}, reason => {
// 只要失败,直接改变当且对象的状态为失败
reject(reason);
})
}
})
}
// 添加race方法
static race(promises) {
if (!Array.isArray(promises)) {
// 如果不是数组,则抛出异常
throw new Error(`${typeof promises} is not iterable (cannot read property Symbol(Symbol.iterator))`);
}
return new Promise((resolve, reject) => {
// 只要实例对象中有一个状态发生改变,则直接改变当前实例对象的状态
promises.forEach(item => {
item.then(value => {
resolve(value);
}, reason => {
reject(reason);
})
})
})
}
}
async和await语法糖
- async关键字
通过在函数声明的前面添加async关键字,会将函数返回的结果封装为一个Promise实例对象。
返回的Promise实例对象的状态和值的规则与then方法返回的实例对象的状态和值的规则一致。
- 如果return一个非Promise实例对象值,则返回的Promise实例对象的状态为成功,且值为return 的值。
- 如果return一个Promise实例对象,则返回的Promiseshi里对象的状态和值与return的对象的状态和值一致。
- 如果抛出异常,throw,则返回的Promise实例对象的状态为失败,且值为异常信息。
- await表达式
作用:获取Promise实例对象成功的结果
- await右侧的表达式一般为Promise实例对象,但也可以是其他的值
- 如果表达式是Promise实例对象,await返回的是Promise实例对象成功的值
- 如果表达式时其他的值,则直接将此值作为await的返回值。
注意:
- await必须写在async函数中,但async函数中可以没有await
- 如果await的右侧的Promise实例对象状态为失败,就会抛出异常,需要使用try..catch捕获异常,并且在catch中获得失败的值。
- async和await结合使用
例子:
const fs = require('fs');
const util = require('util');
const mineReadFile = util.promisify(fs.readFile);
async function main() {
try {
let f1 = await mineReadFile('../html/test.html');
let f2 = await mineReadFile('../html/test2.html');
console.log((f1 + f2));
} catch (e) {
console.log(e);
}
}
main();
可以看到使用async/await语法糖可以不用手动调用then方法获取到成功/失败的结果,更加的方便。