阮一峰博客 :https://es6.ruanyifeng.com/#docs/promise
目录
2.7.6 Promise.prototype.then()
2.7.7 Promise.prototype.catch()
2.7.8 Promise.prototype.finally()
2.8.2 字节面试题,看代码写输出 (与宏任务和微任务有关)
2.8.4 字节面试题,看代码写输出 (async 和 await)
2.8.6 字节面试题,看代码写输出 (与宏任务和微任务有关)
2.8.7 js如何实现异步操作,浏览器事件循环机制(宏任务和微任务)
2.8.9 将一个典型回调风格的功能函数变为promise风格
2.8.10 用Promise实现传统的回调函数,尝试捕获错误;如何让两个异步函数并发执行。
一、学习promise的前置条件
- 回调函数
- 定时器
- JS单线程
- Ajax
- 异常处理
- IO流
1.1 区别实例对象和函数对象
注意:
- ()的左边是函数。例如:Fn() ,Fn是函数
- 点的左边是对象。 例如:Fn.prototype ,Fn是对象(若Fn是函数,则Fn就是函数对象,即将函数作为对象使用)
1.2 两种类型的回调函数
回调函数详解参考文章:https://blog.csdn.net/itcodexy/article/details/111027346
- 回调函数是自己定义的
- 回调函数不是自己调用
1.2.1 同步回调
理解:立即执行,完全执行完了才结束,不会放入回调队列中。主程序A会因回调函数C的执行而阻塞
例1:A.B(参数,回调函数C),主程序A调用函数B,并传入回调函数C,主程序A会等待回调函数C执行完成
例2:forEach中的回调函数执行完(不会放入队列中,一上来就执行),才会执行后面的操作
打印结果:
1.2.2 异步回调
理解:不会立即执行,会放入回调队列中将来执行。回调函数C的执行不会阻塞调用方A。
例1:A.B(参数,回调函数C)。把回调函数C放到另一个线程(进程)、甚至另一台机器上,因此回调函数的执行和我们主程序A的运行同时进行。
例2:setTimeout()内的是回调函数,会放入队列将来执行,故先执行了其后面的内容。
打印结果:
1.3 JS中的error处理(错误处理)
1.3.1 常见的内置错误
1) ReferenceError:引用的变量不存在
2)TypeError:数据类型不正确的错误
3)RangeError:数值不在其所允许的范围内
4)SyntaxError:语法错误
1.3.2 错误的处理
1)捕获错误 try ... catch(捕获try中的异常)
2)抛出错误:throw error (将异常抛出)
调用该函数时,捕获抛出的异常,并打印
1.3.3 错误对象的属性
二、promise的理解和使用
2.1 promise是什么?
Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。
1)所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果。(这个事件通常是一个异步操作)
2)从语法上,Promise是一个构造函数,用来生成Promise实例。
3)从功能上,Promise对象用来封装一个异步操作,可以获取其结果。
4)Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
例1:生成Promise实例
1)Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署;
2)resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
3)reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
例2:
Promise
实例生成后,用then
方法接收resolved
状态和rejected
状态的回调函数。
等价于下面的写法:
1)then
方法可以接受两个回调函数作为参数。这两个函数的参数值对应promise实例中传入的参数。这两个函数都是可选的,不一定要提供。
2)第一个回调函数是:Promise
对象的状态变为resolved
时调用(即成功时调用)
3)第二个回调函数是:Promise
对象的状态变为rejected
时调用(即失败时调用)
补充:resolved
统一只指fulfilled
状态,不包含rejected
状态。
2.2 promise解决的问题?
1) 解决回调地狱问题:在promise之前,我们使用回调函数来发送异步请求,回调函数的问题是当我们有很多请求,并且下一个请求要依赖上一个请求的结果,这样会导致我们的请求
层层嵌套,使得代码非常臃肿,可读性差,容易产生bug导致所谓的回调地狱。
解决:使用Promise的then方法
2)任何时候都可以得到fulfilled
(已成功)或 rejected
(已失败)的结果,如果状态改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件
(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
2.3 promise的特点?
1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前
是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
2)一旦状态改变,就不会再变,并且任何时候都可以得到操作成功或失败的结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要
这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结
果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
3) Promise 新建后就会立即执行。下面代码中,Promise 新建后立即执行,所以首先输出的是“开始执行promise”。然后将在当前脚本所有同步任务执行完后,then
方法指定的回调函数才会执行,所以“操作成功”最后输出。
2.4 promise的状态改变?
pending
(进行中)->fulfilled
(已成功)pending
(进行中)->rejected
(已失败)
注意:
- 只有这两种状态改变,且一个promise对象只能改变一次状态;
- 无论状态变为成功还是失败,都分别会有一个结果数据;
2.5 promise的缺点?
- 首先,无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 - 其次,如果不设置回调函数(reject),
Promise
内部抛出的错误,不会反应到外部。 - 第三,当处于
pending(进行中)
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
2.6 promise的基本流程?
注意:
- 成功时,执行resolve(), 返回一个Promise 实例
失败时,执行reject(),
返回一个 Promise 实例,该实例的状态为rejected。
- then() 返回的是一个新的Promise实例(不是原来的Promise实例),因此可以采用链式写法,即
then
方法后面再调用另一个then
方法。
2.7 Promise的API
Promise自身的方法:
- resolve() --- 返回一个Promise对象,其参数分为四种情况。
- reject() --- 返回一个新的 Promise 实例, 其参数会原封不动地作为
reject
的理由,变成后续方法的参数(例如 reject(e).catch(console.log(e)))。 - all() --- 将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。全部成功,p才会成功;有一个失败p就失败,返回失败结果。以数组的形式返回所有promise的执行结果。
- race() --- 将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。只要
p1
、p2
之中有一个先改变状态,p
的状态就跟着改变。先改变状态的返回值,传递给p
的回调函数。 - any() --- 将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。只要有一个成功,p就会成功,先成功的返回值传给p;所有都失败,p才会变成失败状态,返回错误AggregateError。
- allSettled() --- 将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。只有等到p1,p2都返回结果,不管是
fulfilled
还是rejected
,实例p才会结束。成功和失败的信息以数组的形式传给p
Pomise原型上的方法:
- then() --- 用来对成功或失败的状态进行回调。
then
方法的第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数,它们都是可选的。then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。 - catch() --- 用于指定发生错误时的回调函数。处理错误,可以放在链的最后,用来处理整条链的错误。跟传统的
try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。通俗的说法就是“Promise 会吃掉错误”。 - finally() --- 不管 Promise 对象最后状态如何,都会执行finally。
finally
方法的回调函数不接受任何参数,表明finally
方法里面的操作与状态无关,不依赖于 Promise 的执行结果。finally
本质上是then
方法的特例, 有了finally
方法,不需要为成功和失败两种情况各写一次, 只需要写一次。
2.7.1 Promise.resolve()
根据参数不同,分为以下四种情况
1)参数是一个Promise的实例:Promise.resolve将不作任何修改,原封不动的返回这个实例。
2)参数是一个thenable对象:Promise.resolve方法会将这个对象转为Promise对象,然后立即执行thenable对象的then方法。
thenable对象指的是具有then方法的对象
3)参数是一个原始值,或者是一个不具有then方法的对象:返回一个新的Promise对象,状态为Resolved。
4)不带任何参数:直接返回一个状态为resolved的Promise对象,所以如果希望得到一个Promise对象,最直接的方法就是直接调用Promise.resolve方法。
第四种情况下,需注意:立即resolve的Promise对象是在本轮“事件循环”event loop结束时,不是在下一轮“事件循环”开始时,如下面代码所示:
2.7.2 Promise.reject()
返回一个新的 Promise 实例, 其参数会原封不动地作为reject
的理由,变成后续方法的参数(例如 reject(e).catch(console.log(e)))
2.7.3 Promise.all()
将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。全部成功,p才会成功;有一个失败p就失败,返回失败结果。
以数组的形式返回所有promise的执行结果。
接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例。
p
的状态由p1
、p2
、p3
决定,分成两种情况:
1)只有p1
、p2
、p3
的状态都变成fulfilled(已成功)
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
2)只要p1
、p2
、p3
之中有一个被rejected(已失败)
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
注意:如果p1定义了catch
方法,被rejected了,
并不会触发Promise.all()
的catch
方法。
(因为p1执行完catch
方法后,也会变成resolved
,导致Promise.all()
方法参数里面的两个实例都会resolved,
而不会触发Promise.all()
的catch
方法。)
例子:Promise.all([p1,p2]) 。 p1自己捕获了错误,所以对p而言,p1和p2状态都是已成功
如果p1没有捕获错误,则p1失败,直接返回错误值,p视为失败。只打印错误信息
2.7.4 Promise.race()
将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。只要p1
、p2
之中有一个先改变状态,p
的状态就跟着改变。先改变状态的返回值,传递给p
的回调函数。
例子:Promise.race([p1,p2]) 。p1先执行完,p随p1的状态改变
2.7.5 Promise.any()
将多个 Promise 实例p1、p2,包装成一个新的 Promise 实例p。
只要有一个成功,p就会成功,先成功的返回值传给p;所有都失败,p才会变成失败状态,返回错误AggregateError。
例子:Promise.any([p1,p2]) 。p1和p2都失败,返回一行错误提示。
2.7.6 Promise.prototype.then()
用来对成功或失败的状态进行回调。 then
方法的第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数,它们都是可选的。
then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
如果还需要调用then方法,需要return一个结果,才能被下一个then方法拿到
例如:如果第一个then 不返回value,第二个then获取不到value。
注意:对同一个promise对象的多次then方法并不是promise链
2.7.7 Promise.prototype.catch()
用于指定发生错误时的回调函数。处理错误,可以放在链的最后,用来处理整条链的错误。
问题:跟传统的try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。通俗的说法就是“Promise 会吃掉错误”。
例如:p1对象出现错误,但是2s后,仍然执行了下面的代码,输出了123
2.7.8 Promise.prototype.finally()
不管 Promise 对象最后状态如何,都会执行finally。finally
方法的回调函数不接受任何参数,表明finally
方法里面的操作与状态无关,不依赖于 Promise 的执行结果。finally
本质上是then
方法的特例, 有了finally
方法,不需要为成功和失败两种情况各写一次, 只需要写一次。
例1:即使p1被rejected了,finally方法的回调函数仍然执行了
2.8 promise的使用
根据代码说出执行结果(大概20+道题)
2.8.1 字节面试题,看代码写输出
console.log('begin')
setTimeout(() => { console.log('setTimeout 1')
Promise.resolve().then(() => {
console.log('promise 1')
setTimeout(() => { console.log('setTimeout2 between promise1&2') })
}).then(() => {
console.log('promise 2')
}) }, 0);
console.log('end')
输出:begin -> end -> setTimeout 1 -> promise 1 -> promise 2 -> setTimeout2 between promise1&2
宏任务类型:包括整体代码script,setTimeout,setInterval(循环),setImmediate
微任务类型:Promise,process.nextTick
2.8.2 字节面试题,看代码写输出 (与宏任务和微任务有关)
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve()
}).then(() => {
console.log(5);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
输出:1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6
2.8.3 字节面试题,看代码写输出
const first = () =>
new Promise((resovle, reject) => {
console.log(1)
const p = new Promise((resovle, reject) => {
console.log(2)
setTimeout(() => {
console.log(3)
resovle(4)
}, 0)
resovle(5)
})
resovle(6)
p.then(arg => {
console.log(arg)
})
})
first().then(arg => {
console.log(arg)
})
console.log(7)
输出:1 -> 2 -> 7 -> 5 -> 6 -> 3
2.8.4 字节面试题,看代码写输出 (async 和 await)
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 start');
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
}).then(function() {
console.log('promise3');
});
console.log('script end')
输出:srcipt start -> async1 start -> async2 start -> async2 promise -> promise1 -> srcipt end ->
promise2 -> promise3 -> async1 end -> setTimeout
1)例1
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
输出结果:
async1 start
async2
start
async1 end
分析过程:
- 首先执行函数中的同步代码async1 start,之后碰到了await,它会阻塞async1后面代码的执行,因为此会先去执行async2中的同步代码async2,然后跳出async1
- 跳出async1函数后,执行同步代码start
- 在一轮宏任务全部执行完之后,再来执行await后面的内容async1 end
这里可以理解为await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中。
2)例2
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
输出结果:
async1 start
async2
start
async1 end
timer2
timer3
timer1
分析过程:
- 首先进入async1,打印出async1 start,这个是毋庸置疑的
- 之后遇到async2,进入async2,遇到定时器timer2,加入宏任务队列,之后打印async2
- 由于async2阻塞了后面代码的执行,所以执行后面的定时器timer3,将其加入宏任务队列,之后打印start
- 然后执行async2后面的代码,打印出async1 end,遇到定时器timer1,将其加入宏任务队列
- 最后,宏任务队列有三个任务,先后顺序为timer2,timer3,timer1,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。
3)例3
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
输出结果:
script start
async1 start
promise1
script end
分析过程:
这里需要注意的是在async1
中await
后面的Promise是没有返回值的,也就是它的状态始终是pending
状态,所以在await
之后的内容是不会执行的,也包括async1
后面的 .then
。
2.8.5 字节面试题,看代码写输出
new Promise(() => {
throw new Error()
}).catch(() => {
console.log(1);
}).then(() => {
console.log(2);
})
输出结果:1 -> 2
2.8.6 字节面试题,看代码写输出 (与宏任务和微任务有关)
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
}).then(function() {
console.log('promise3');
});
console.log('script end');
// script start -> promise1 -> script end -> promise2 -> promise3 -> setTimeout
输出结果: script start -> promise 1 -> script end -> promise 2 -> promise 3 -> setTimeout
2.8.7 js如何实现异步操作,浏览器事件循环机制(宏任务和微任务)
(1)回调函数
概念: 封装了异步操作的函数接受一个匿名函数作为参数,当异步操作执行完后调用这个传递进来的匿名函数
缺点:容易出现多层的嵌套(回调地狱问题),例如ajax请求来的数据作为下一个ajax请求的参数,一个异步完成的结果作为另一个异步操作函数的参数
(2)promise
(3)async await
2.8.8 把 ajax 封装成 promise 的形式
为了简便起见没有考虑浏览器的兼容问题
1)使用get请求(在url尾部传递参数)
function getJson(url){
return new Promise((resolve, reject) => {
//1. 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();
//2. 创建请求
xhr.open("GET",url,true);
//3. 监听readyState状态,获取数据
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){//3.1已获取响应数据
if(xhr.status === 200){ //3.2服务器成功处理请求
resolve(JSON.parse(xhr.responseText)); //返回响应数据
}else{//3.3请求失败(需要返回失败时的状态码)
reject({
status : xhr.status,
data : JSON.parse(xhr.responseText)
});
}
}
}
//4. 发送请求
xhr.send();
})
}
2)使用post请求(send方法中传递参数,必须设置请求头content-type)
function getJson(url,data){
return new Promise((resolve, reject) => {
//1. 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();
//2. 创建请求
xhr.open("POST",url,true);
//3. 设置请求头content-type
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
//4. 监听readyState状态,获取数据
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){//3.1已获取响应数据
if(xhr.status === 200){ //3.2服务器成功处理请求
resolve(JSON.parse(xhr.responseText)); //返回响应数据
}else{//3.3请求失败(需要返回失败时的状态码)
reject({
status : xhr.status,
data : JSON.parse(xhr.responseText)
});
}
}
}
//4. 发送请求
xhr.send(JSON.stringify(data));
})
}
2.8.9 将一个典型回调风格的功能函数变为promise风格
2.8.10 用Promise实现传统的回调函数,尝试捕获错误;如何让两个异步函数并发执行。
这里实际上问的很基础了,改写回调函数直接new 一个Promise对象,判断状态执行resolve跟reject即可,后面捕获错误我直接在Promise里面捕获了,后续了解到在Promise外部 await的时候捕获更好一点。异步函数并发执行直接采用Promise.all([]),即可实现。
传统异步函数,必须传入回调函数。
(1)方式一:有了Promise之后,就这么封装一下
连续.then().then()没有回调黑洞了,变成了火车黑洞。
(2)方式二:sync/await
它能够等异步执行完毕,再执行后面的语句。
它能够让我们以写同步的方法写异步: