本文摘自 阿尔卑斯de秘密 的 面向面试题和实际谈promise。
Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一。这句话说的很明白了,Promise是一种用于解决异步问题的思路、方案或者对象方式。在js中,常用异步的地方是Ajax交互。好比在es5时代,jQuery的ajax的使用success来完成异步的:html
$.ajax({
url:'/xxx',
success:()=>{},
error: ()=>{}
})复制代码
这种方法能够清楚的让读代码的人明白那一部分是Ajax请求成功的回调函数和失败的回调函数。可是问题来了,当一次请求须要连续请求多个接口时,这段代码仿佛进入了一团乱麻中:es6
// 第一次
$.ajax({
url:'/xxx',
success:()=>{
// 第二次
$.ajax({
url:'/xxx',
success:()=>{
// 第三次
$.ajax({
url:'/xxx',
success:()=>{
// 可能还会有
},
error: ()=>{}
})
},
error: ()=>{}
})
},
error: ()=>{}
}) 复制代码
也许由于success和error这两个函数的存在,理解这段代码会很简单,可是当咱们更改需求的时候,这将成为一个棘手的问题。这就是回调地狱。面试
固然,这是es5时代。当js这门语言发展到es6时代时,Promise的出现给异步带来了变革。Promise提供一个then,来为异步提供回调函数:ajax
而其先进之处则是,能够在then方法中继续写Promise对象并返回,而后继续调用then来进行回调操做。编程
说完了Promise是什么,下面让咱们研究一下Promise怎么使用。首先,Promise是一个对象,所以,咱们使用new的方式新建一个。而后给它传一个函数做为参数,这个函数呢也有两个参数,一个叫resolve(决定),一个叫reject(拒绝),这两个参数也是函数。紧接着,咱们使用then来调用这个Promise:数组
const fn = new Promise(function (resolve, reject) {
setTimeout(()=>{
let num = Math.ceil(Math.random() * 10) // 假设num为7
if (num > 5) {
resolve(num) //返回7
} else {
reject(num)
}
},2000)
})
fn.then((res)=>{
console.log(res) // 7
},(err)=>{
console.log(err)
})复制代码
这就是最简单的Promise的使用。假设2秒钟以后生成随机数为7,所以resolve回调函数运行,then走第一个函数,console.log(7)。假设2秒钟以后生成随机数为3,所以reject回调函数运行,then走第二个函数,console.log(3)。promise
那你可能说了,Promise要是就这点能耐也没什么大不了的啊?咱们上面说了Promise的先进之处在于能够在then方法中继续写Promise对象并返回,而后继续调用then来进行回调操做:bash
fn = new Promise(function (resolve, reject) {
let num = Math.ceil(Math.random() * 10)
if (num > 5) {
resolve(num)
} else {
reject(num)
}
})
// 第一次回调
fn.then((res)=>{
console.log(`res==>${res}`)
return new Promise((resolve,reject)=>{
if(2*res>15){
resolve(2*res)
}else{
reject(2*res)
}
})
},(err)=>{
console.log(`err==>${err}`)
}).then((res)=>{ // 第二次回调
console.log(res)
},(err)=>{
console.log(`err==>${err}`)
})复制代码
这就能够代替了上面相似es5时代的jQurey的success的嵌套式的回调地狱的产生,让代码清爽了许多。这里的resolve就至关于之前的success。app
在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。dom
(1) promise 对象初始化状态为 pending。
(2) 当调用resolve(成功),会由pending => fulfilled。
(3) 当调用reject(失败),会由pending => rejected。
所以,看上面的的代码中的resolve(num)实际上是将promise的状态由pending改成fulfilled,而后向then的成功回掉函数传值,reject反之。可是须要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变(记住,必定要记住,下面会考到)。
当状态为fulfilled(rejected反之)时,then的成功回调函数会被调用,并接受上面传来的num,进而进行操做。promise.then方法每次调用,都返回一个新的promise对象 因此能够链式写法(不管resolve仍是reject都是这样)。
then方法用于注册当状态变为fulfilled或者reject时的回调函数:
// onFulfilled 是用来接收promise成功的值
// onRejected 是用来接收promise失败的缘由
promise.then(onFulfilled, onRejected);复制代码
须要注意的地方是then方法是异步执行的。
// resolve(成功) onFulfilled会被调用
const promise = new Promise((resolve, reject) => {
resolve('fulfilled'); // 状态由 pending => fulfilled
});
promise.then(result => { // onFulfilled
console.log(result); // 'fulfilled'
}, reason => { // onRejected 不会被调用
})
// reject(失败) onRejected会被调用
const promise = new Promise((resolve, reject) => {
reject('rejected'); // 状态由 pending => rejected
});
promise.then(result => { // onFulfilled 不会被调用
}, reason => { // onRejected
console.log(rejected); // 'rejected'
})复制代码
catch在链式写法中能够捕获前面then中发送的异常。
fn = new Promise(function (resolve, reject) {
let num = Math.ceil(Math.random() * 10)
if (num > 5) {
resolve(num)
} else {
reject(num)
}
})
fn..then((res)=>{
console.log(res)
}).catch((err)=>{
console.log(`err==>${err}`)
})复制代码
其实,catch至关于then(null,onRejected),前者只是后者的语法糖而已。
Promise.resolve 返回一个fulfilled状态的promise对象,Promise.reject 返回一个rejected状态的promise对象。
Promise.resolve('hello').then(function(value){
console.log(value);
});
Promise.resolve('hello');
// 至关于
const promise = new Promise(resolve => {
resolve('hello');
});
// reject反之复制代码
但从字面意思上理解,可能为一个状态所有怎么样的意思,让我看一下其用法,就能够看明白这个静态方法:
var p1 = Promise.resolve(1),
p2 = Promise.reject(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((res)=>{
//then方法不会被执行
console.log(results);
}).catch((err)=>{
//catch方法将会被执行,输出结果为:2
console.log(err);
});复制代码
大概就是做为参数的几个promise对象一旦有一个的状态为rejected,则all的返回值就是rejected。
当这几个做为参数的函数的返回状态为fulfilled时,至于输出的时间就要看谁跑的慢了:
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('1s') //1s后输出
resolve(1)
},1000)
})
let p10 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('10s') //10s后输出
resolve(10)
},10000)
})
let p5 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('5s') //5s后输出
resolve(5)
},5000)
})
Promise.all([p1, p10, p5]).then((res)=>{
console.log(res); // 最后输出
})复制代码
这段代码运行时,根据看谁跑的慢的原则,则会在10s以后输出[1,10,5]。over,all收工。
promise.race()方法也能够处理一个promise实例数组但它和promise.all()不一样,从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,就向下传递谁的状态和异步结果。可是,其他的仍是会继续进行的。
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('1s') //1s后输出
resolve(1)
},1000)
})
let p10 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('10s') //10s后输出
resolve(10) //不传递
},10000)
})
let p5 = new Promise((resolve)=>{
setTimeout(()=>{
console.log('5s') //5s后输出
resolve(5) //不传递
},5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
console.log(res); // 最后输出
})复制代码
所以,在这段代码的结尾咱们的结果为
1s
1
5s
10s复制代码
咱们能够根据race这个属性作超时的操做:
//请求某个图片资源
let requestImg = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
});
//延时函数,用于给请求计时
let timeOut = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
Promise.race([requestImg, timeout]).then((res)=>{
console.log(res);
}).catch((err)=>{
console.log(err);
});复制代码
1.
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);复制代码
输出结果为:1,2,4,3。
解题思路:then方法是异步执行的。
2.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
reject('error')
}, 1000)
})
promise.then((res)=>{
console.log(res)
},(err)=>{
console.log(err)
})复制代码
输出结果:success
解题思路:Promise状态一旦改变,没法在发生变动。
3.
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)复制代码
输出结果:1
解题思路:Promise的then方法的参数指望是函数,传入非函数则会发生值穿透。
4.
setTimeout(()=>{
console.log('setTimeout')
})
let p1 = new Promise((resolve)=>{
console.log('Promise1')
resolve('Promise2')
})
p1.then((res)=>{
console.log(res)
})
console.log(1)复制代码
输出结果:
Promise1
1
Promise2
setTimeout
解题思路:这个牵扯到js的执行队列问题,整个script代码,放在了macrotask queue中,执行到setTimeout时会新建一个macrotask queue。可是,promise.then放到了另外一个任务队列microtask queue中。script的执行引擎会取1个macrotask queue中的task,执行之。而后把全部microtask queue顺序执行完,再取setTimeout所在的macrotask queue按顺序开始执行。(具体参考www.zhihu.com/question/36…)
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});复制代码
输出结果:1 2
解题思路:Promise首先resolve(1),接着就会执行then函数,所以会输出1,而后在函数中返回2。由于是resolve函数,所以后面的catch函数不会执行,而是直接执行第二个then函数,所以会输出2。
6.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});复制代码
输出结果:
开始
success 5002
success 5002
解题思路:promise 的.then
或者.catch
能够被调用屡次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,而且有了一个值,那么后续每次调用.then
或者.catch
都会直接拿到该值。
7.
let p1 = new Promise((resolve,reject)=>{
let num = 6
if(num<5){
console.log('resolve1')
resolve(num)
}else{
console.log('reject1')
reject(num)
}
})
p1.then((res)=>{
console.log('resolve2')
console.log(res)
},(rej)=>{
console.log('reject2')
let p2 = new Promise((resolve,reject)=>{
if(rej*2>10){
console.log('resolve3')
resolve(rej*2)
}else{
console.log('reject3')
reject(rej*2)
}
})
return p2
}).then((res)=>{
console.log('resolve4')
console.log(res)
},(rej)=>{
console.log('reject4')
console.log(rej)
})复制代码
输出结果:
reject1
reject2
resolve3
resolve4
12
解题思路:咱们上面说了Promise的先进之处在于能够在then方法中继续写Promise对象并返回。
function Promise(fn){
var status = 'pending'
function successNotify(){
status = 'fulfilled'//状态变为fulfilled
toDoThen.apply(undefined, arguments)//执行回调
}
function failNotify(){
status = 'rejected'//状态变为rejected
toDoThen.apply(undefined, arguments)//执行回调
}
function toDoThen(){
setTimeout(()=>{ // 保证回调是异步执行的
if(status === 'fulfilled'){
for(let i =0; i< successArray.length;i ++) {
successArray[i].apply(undefined, arguments)//执行then里面的回掉函数
}
}else if(status === 'rejected'){
for(let i =0; i< failArray.length;i ++) {
failArray[i].apply(undefined, arguments)//执行then里面的回掉函数
}
}
})
}
var successArray = []
var failArray = []
fn.call(undefined, successNotify, failNotify)
return {
then: function(successFn, failFn){
successArray.push(successFn)
failArray.push(failFn)
return undefined // 此处应该返回一个Promise
}
}
}复制代码
解题思路:Promise中的resolve和reject用于改变Promise的状态和传参,then中的参数必须是做为回调执行的函数。所以,当Promise改变状态以后会调用回调函数,根据状态的不一样选择须要执行的回调函数。
首先,Promise是一个对象,如同其字面意思同样,表明了将来某时间才会知道结果的时间,不受外界因素的印象。Promise一旦触发,其状态只能变为fulfilled或者rejected,而且已经改变不可逆转。Promise的构造函数接受一个函数做为参数,该参数函数的两个参数分别为resolve和reject,其做用分别是将Promise的状态由pending转化为fulfilled或者rejected,而且将成功或者失败的返回值传递出去。then有两个函数做为Promise状态改变时的回调函数,当Promise状态改变时接受传递来的参数并调用相应的函数。then中的回调的过程为异步操做。catch方法是对.then(null,rejectFn)的封装(语法糖),用于指定发生错误时的回掉函数。通常来讲,建议不要再then中定义rejected状态的回调函数,应该使用catch方法代替。all和race都是竞速函数,all结束的时间取决于最慢的那个,其做为参数的Promise函数一旦有一个状态为rejected,则总的Promise的状态就为rejected;而race结束的时间取决于最快的那个,一旦最快的那个Promise状态发生改变,那个其总的Promise的状态就变成相应的状态,其他的参数Promise仍是会继续进行的。