目录
- 同步与异步
- Promise
- Promise基本方法
- Promise构造函数: new Promise (executor) ${}
- Promise.prototype.then方法: Promise实例.then(onFulfilled,onRejected)
- Promise.prototype.catch方法: Promise实例.catch(onRejected)
- Promise.resolve方法: Promise.resolve(value)
- Promise.reject方法: Promise.reject方法(reason)
- Promise.all方法: Promise.all(promiseArr)
- Promise.race方法: Promise.race(promiseArr)
- 回调地狱
- Promise的优势
- Promise关键问题
- 如何改变一个Promise实例的状态?
- 改变Promise实例的状态和指定回调函数谁先谁后?
- Promise实例.then()返回的是一个【新的Promise实例】,它的值和状态由什么决定?
- 如何中断promise链:
- Promise错误穿透的原理:
- async & await
- 宏任务与微任务
- 自我检测
- 初级版
- 高级版
同步与异步
在学习Promise之前我们需要先明白一些基础知识,首先我们要知道什么叫实例对象,什么叫函数对象。所谓实例对象就是我们使用 new 关键字创建出来的对象,称为实例对象,一般简称对象。而函数对象是指当我们把一个函数当作对象使用时,此时我们称这个函数为函数对象
//函数的两种身份:函数、函数对象
function foo(){
console.log('ok');
}
foo() // ok 当我们用()调用函数时,此时函数称为一个函数
foo.a = 20 // 当我们像这样给函数添加属性或者方法时,此时函数称为一个函数对象
按函数的调用者分类我们可以将某些函数称为回调函数,回调函数就是我们定义的,我们没调用,最终执行了。最典型的例子就是定时器里我们传入的函数。根据回调函数执行时机的不同,我们又可以将回调函数分为同步回调函数和异步回调函数。
同步回调函数的特点就是立即执行,完全执行完了才结束,不会放入回调队列中。换言之就是它是在主线程上执行的。例如数组遍历相关的回调函数、Promise的excutor函数
let arr = [10,20,30]
arr.forEach(value => console.log(value))
console.log('主线程')
// 输出顺序为: 10 20 30 主线程
上述例子中可以明显看到,数组forEach方法里的回调函数先于主线程上的 console.log 输出,这表明这个回调函数就是一个同步回调
与同步回调函数相反,异步回调函数的特点就是延迟执行,它会被放入回调队列里,等主线程上的函数都执行完以后将来再根据条件执行。例如定时器上的回调函数、ajax回调、Promise的成功 | 失败回调
setTimeout(()=>(console.log('hello!')),0)
console.log('主线程')
//输出顺序为: 主线程 hello!
即便定时器的延迟设为 0 ,它里面的回调函数依然要等待主线程执行完毕才能执行。这就是一个典型的异步回调
通过理解同步回调和异步回调的例子,我们可以明白。所谓同步就是绝对的串行执行,只有上一步执行完了下一步才能继续执行。想象这样的一个场景,我们去做饭,电饭煲在煮饭的同时我们可以继续处理我们的菜,我们不必等饭完全煮好了才开始做菜,这样太浪费时间了。这种就是异步执行。如果我们必须等到饭煮好了才开始炒菜之类的,那这种就是同步执行。
Promise
有了上面同步异步的概念以后,接下来我们就可以开始学习Promise了。我们先来看看它是什么,官方给的定义是啥。
-
抽象表达:
- Promise是JS中进行异步编程的新方案(旧的是谁?---纯回调)
-
具体表达:
-
从语法上来说: Promise是一个构造函数
-
从功能上来说: promise对象用来封装一个异步操作并可以获取其结果
其实看了上面的表达我们还是不懂什么是Promise。从异步这个词入手,我们都知道ajax就是一个典型的异步操作。那么我们就可以用Promise来封装ajax请求。那我们为什么非要用Promise来封装异步操作,我们用普通的回调函数形式不一样可以吗?这就涉及到回调地狱这个问题,关于[Promise的优越性](# 回调地狱 )我们后面再谈。首先来明确一些基础的Promise知识
- Promise不是回调,是一个内置的构造函数,是程序员自己new调用的。
- new Promise的时候,要传入一个回调函数,它是同步的回调,会立即在主线程上执行,它被称为executor函数
- 每一个Promise实例都有3种状态:初始化(pending)、成功(fulfilled)、失败(rejected)
- 每一个Promise实例在刚被new出来的那一刻,状态都是初始化(pending)
- executor函数会接收到2个参数,它们都是函数,分别用形参:resolve、reject接收
- 调用resolve函数会:
- 让Promise实例状态变为成功(fulfilled)
- 可以指定成功的value。
- 调用reject函数会:
- 让Promise实例状态变为失败(rejected)
- 可以指定失败的reason。
- 调用resolve函数会:
//new 一个实例
const p = new Promise((resolve,reject)=>{})
//根据上面描述这个Promise必须传入一个excutor函数,而这个excutor函数又两个形参也是函数,我们不必定义该两个形参函数,可以直接调用
(resolve,reject)=>{} //excutor
当我们使用一个Promise管理异步操作的时候,我们要在excutor函数内启动异步任务然后再用它的 then 方法来指定异步任务结束后根据Promise实例的状态来调用相应的回调函数。
//用Promise封装一个自己的get请求ajax
function myAjax(url,datas){
return new Promise(
(resolve,reject)=>{
const xhr = new XHLHttpRequest();
xhr.onreadystatuschange = ()=>{
if(xhr.readyState === 4){
if(xhr.status === 200) resolve('ok') //请求成功将Promise状态置为fulfilled
else reject('falure') //请求失败将Promise状态置为rejected
}
}
xhr.opne('GET',url);
xhr.send();
}
);
}
const p = myAjax('http://127.0.0.1/get',{test:'test'});
p.then( //为Promise实例指定成功与失败的回调函数
(value)=>{ console.log('请求成功1',value); }, //fulfilled状态调用
(reason)=>{ console.log('请求失败1',reason); } //rejected状态调用
)
//一个promise指定多个成功/失败回调函数, 则会依次调用并不会覆盖
p.then( //为Promise实例指定成功与失败的回调函数
(value)=>{ console.log('请求成功2',value); }, //fulfilled状态调用
(reason)=>{ console.log('请求失败2',reason); } //rejected状态调用
)
//若该次请求失败则依次输出:请求失败1 请求失败2
- 关于状态的注意点:
- 三个状态:
- pending: 未确定的------初始状态
- fulfilled: 成功的------调用resolve()后的状态
- rejected: 失败的-------调用reject()后的状态
- 两种状态改变
- pending ==> fulfilled
- pending ==> rejected
- 状态只能改变一次!
- 三个状态:
Promise基本方法
Promise构造函数: new Promise (executor) {}
- executor函数: 是同步执行的,(resolve, reject) => {}
- resolve函数: 调用resolve将Promise实例内部状态改为成功(fulfilled)。
- reject函数: 调用reject将Promise实例内部状态改为失败(rejected)。
- 说明: excutor函数会在Promise内部立即同步调用,异步代码放在excutor函数中。
Promise.prototype.then方法: Promise实例.then(onFulfilled,onRejected)
- onFulfilled: 成功的回调函数 (value) => {}
- onRejected: 失败的回调函数 (reason) => {}
- 特别注意(难点):then方法会返回一个新的Promise实例对象
- 如果上一个回调返回的是一个非promise对象,则这个新的Promise实例状态为fulfilled
- 当上一个回调返回一个Promise对象则该新返回的Promise对象的状态与回调返回的Promise对象一致
const p = new Promise((resolve,reject) =>{ resolve('ok'); })
const x = p.then(
value => {return 1}, //返回非Promise值
reason => {return new Promise.reject(996)}
)
x.then(
value => console.log('成功了'), //x状态为fulfilled,输出成功了
reason => console.log('失败了')
)
const z = p.then(
value => {return new Promise((resolve,reject)=>{})}, //返回Promise值
reason => {return new Promise.reject(996)}
)
z.then( //z状态为 pending ,不调用回调函数
value => console.log('成功了'),
reason => console.log('失败了')
)
Promise.prototype.catch方法: Promise实例.catch(onRejected)
- onRejected: 失败的回调函数 (reason) => {}
- 说明: catch方法是then方法的语法糖, 相当于: then(undefined, onRejected)
Promise.resolve方法: Promise.resolve(value)
- 说明: 用于快速返回一个状态为fulfilled或rejected的Promise实例对象
- 备注:value的值可能是:(1)非Promise值 (2)Promise值
- 当传入的值为非Promise值时或空值时,直接返回一个 fulfilled 状态的 Promise实例
- 当传入的值为 Promise 时,返回的Promise状态跟随传入的Promise
Promise.reject方法: Promise.reject方法(reason)
- 说明: 用于快速返回一个状态必为rejected的Promise实例对象
const x = Promise.reject(996);
x.then(
value => console.log('成功了',value),
reason => console.log('失败了',reason) //调用该函数 输出:失败了 996
)
Promise.all方法: Promise.all(promiseArr)
- promiseArr: 包含n个Promise实例的数组
- 说明: 返回一个新的Promise实例, 只有所有的promise都成功才成功, 只要有一个失败了就直接失败。
- 若没有失败的值,且没有pending的值即全都成功返回的新Promise状态为fulfilled
- 若没有失败的值,但存在pending的值即数组内仅有pending和fulfilled两种值,则返回的新Promise状态为pending
Promise.race方法: Promise.race(promiseArr)
- promiseArr: 包含n个Promise实例的数组
- 说明: 返回一个新的Promise实例, 成功还是很失败?以最先出结果的promise为准。
- 若最先出结果的promise为pending则跳过该promise
- 这也就意味着race返回的Promise实例仅有fulfilled和rejected两种状态,不存在pending状态的值
回调地狱
上面我们学习了基本Promise使用,但是我们依然没有看出来Promise的优越性在哪里。现在我们有这么一个需求,连发三次Ajax请求,仅当上次请求成功时才发送下一次请求,若请求失败则中断以后的所有请求。
//用纯回调的方式封装ajax
function sendAjax(url,data,success,error){
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = ()=>{
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300) success(xhr.response);
else error('请求出了点问题');
}
}
//整理参数
let str = ''
for (let key in data){
str += `${key}=${data[key]}&`
}
str = str.slice(0,-1)
xhr.open('GET',url+'?'+str)
xhr.responseType = 'json'
xhr.send()
}
//传统解决方案,链式连发三个请求
sendAjax(
'https://api.apiopen.top/getJoke',
{page:1,count:2,type:'video'},
response =>{
console.log('第1次成功了',response);
sendAjax(
'https://api.apiopen.top/getJoke',
{page:1,count:2,type:'video'},
response =>{
console.log('第2次成功了',response);
sendAjax(
'https://api.apiopen.top/getJoke',
{page:1,count:2,type:'video'},
response =>{
console.log('第3次成功了',response); //回调地狱
},
err =>{console.log('第3次失败了',err);}
)
},
err =>{console.log('第2次失败了',err);}
)
},
err =>{console.log('第1次失败了',err);}
)
可以看到当我们要进行三次链式的异步请求时,采用纯回调的方式来处理就导致了回调地狱的问题。要对代码进行维护十分困难。而如果我们采用Promise去封装异步请求,则可以解决回调地狱的问题
//Promsie封装ajax
function sendAjax(url,data){
return new Promise((resolve,reject)=>{
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = ()=>{
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
else reject('请求出了点问题');
}
}
//整理参数
let str = ''
for (let key in data){
str += `${key}=${data[key]}&`
}
str = str.slice(0,-1)
xhr.open('GET',url+'?'+str)
xhr.responseType = 'json'
xhr.send()
})
}
//利用Promise进行三次链式ajax请求
sendAjax('https://api.apiopen.top/getJoke',{page:1})
.then(
value => {
console.log('第1次请求成功了',value);
//发送第2次请求
return sendAjax('https://api.apiopen.top/getJoke',{page:1})
}
)
.then(
value => {
console.log('第2次请求成功了',value);
//发送第3次请求
return sendAjax('https://api.apiopen.top/getJoke',{page:1})
}
)
.then(
value => {console.log('第3次请求成功了',value);}
)
.cathe(
err => console.log(err); //利用错误穿透进行兜底
)
可以明显地对比出来,利用Promise进行链式异步操作能清晰地看到调用结构,维护起来相比纯回调方便了很多。这就解决了回调地狱的问题。
Promise的优势
-
指定回调函数的方式更加灵活:
- 旧的: 必须在启动异步任务前指定
- Promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定)
-
支持链式调用, 可以解决回调地狱问题
- 什么是回调地狱:回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件
- 回调地狱的弊病:代码不便于阅读、不便于异常的处理
- 一个不是很优秀的解决方案:then的链式调用
- 终极解决方案:async/await(底层实际上依然使用then的链式调用)
Promise关键问题
如何改变一个Promise实例的状态?
- 执行resolve(value): 如果当前是pending就会变为fulfilled
- 执行reject(reason): 如果当前是pending就会变为rejected
- 执行器函数(executor)抛出异常: 如果当前是pending就会变为rejected
改变Promise实例的状态和指定回调函数谁先谁后?
- 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
- 如何先改状态再指定回调?
- 延迟一会再调用then()
- Promise实例什么时候才能得到数据?
- 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
- 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据
Promise实例.then()返回的是一个【新的Promise实例】,它的值和状态由什么决定?
- 简单表达: 由then()所指定的回调函数执行的结果决定
- 详细表达:
- 如果then所指定的回调返回的是非Promise值a:
- 那么【新Promise实例】状态为:成功(fulfilled), 成功的value为a
- 如果then所指定的回调返回的是一个Promise实例p:
- 那么【新Promise实例】的状态、值,都与p一致
- 如果then所指定的回调抛出异常:
- 那么【新Promise实例】状态为rejected, reason为抛出的那个异常
- 如果then所指定的回调返回的是非Promise值a:
如何中断promise链:
- 当使用promise的then链式调用时, 在中间中断, 不再调用后面的回调函数。
- 办法: 在失败的回调函数中返回一个pendding状态的Promise实例。
Promise错误穿透的原理:
- 当使用promise的then链式调用时, 可以在最后用catch指定一个失败的回调,
- 前面任何操作出了错误, 都会传到最后失败的回调中处理了
- 备注:如果不存在then的链式调用,就不需要考虑then的错误穿透。
const p = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(-100)
},1000)
})
p.then(
value => {console.log('成功了1',value);return 'b'},
reason => {throw reason}//底层帮我们补上的这个失败的回调
)
.then(
value => {console.log('成功了2',value);return Promise.reject(-108)},
reason => {throw reason}//底层帮我们补上的这个失败的回调
)
.catch(
reason => {throw reason}
)
async & await
-
async修饰的函数
函数的返回值为promise对象
Promise实例的结果由async函数执行的返回值决定,返回非Promise值则返回的Promise对象状态为fulfilled,返回Promise则状态跟随返回的Promise,但是不能返回一个 rejected的Promise,否则报错
const p = (async () =>{return 0;})();
console.log(p) //输出fulfilled
p.then(
value => console.log('成功了',value), //执行成功回调
reason => console.log('失败了',reason)
)
----------
const p = (async () =>{return Promise.reject(996);})();
console.log(p) //输出pending ,同时也说明了Promise.reject()是个异步函数
p.then(
value => console.log('成功了',value),
reason => console.log('失败了',reason)//执行失败回调
)
----------
const p = (async () =>{return new Promise(()=>{});})();
console.log(p)
p.then(
value => console.log('成功了',value),
reason => console.log('失败了',reason)
)//不调用任何一个,说明最后状态为pending
----------
const p = (async () =>{return Promise.resolve(200);})();
console.log(p) //输出pending ,同时也说明了Promise.resolve()是个异步函数
p.then(
value => console.log('成功了',value), //执行成功回调
reason => console.log('失败了',reason)
)
-
await表达式
await右侧的表达式一般为Promise实例对象, 但也可以是其它的值
(1).如果表达式是Promise实例对象, await后的返回值是promise成功的值
(2).如果表达式是其它值, 直接将此值作为await的返回值
-
注意:
await必须写在async函数中, 但async函数中可以没有await
如果await的Promise实例对象失败了, 就会抛出异常, 需要通过try…catch来捕获处理
const p1 = new Promise((resolve,reason)=>{
setTimeout(function(){
resolve('ok了')
},2000)
})
const p2 = Promise.reject('中断')
const p3 = new Promise((resolve,reason)=>{
setTimeout(function(){
resolve('Ok啊')
},4000)
})
;(async()=>{
try{
const x1 = await p1;
console.log('1',x1); //输出:1 ok了
const x2 = await p2; //该点直接失败转入失败回调 throw error
console.log('2',x2);
const x3 = await p3;
console.log('3',x3);
}catch(err){
console.log(err); //catch接收上面throw 的 error 输出:中断
}
})()
------
//上面try里面的代码被浏览器翻译为
p1.then(
value => {console.log('1',value) return p2;} //输出:1 ok了
// 不写失败回调底层补上了 reason => throw reason
).then(
value => {console.log('2',value); return p3;}
).then(
value => {console.log('3',value);}
)
宏任务与微任务
目前为止,除了Promise里的成功和失败回调是微任务,其它异步回调都是宏任务
宏队列:[宏任务1,宏任务2…]
微队列:[微任务1,微任务2…]
规则:每次要执行宏队列里的一个任务之前,先看微队列里是否有待执行的微任务
1.如果有,先执行微任务
2.如果没有,按照宏队列里任务的顺序,依次执行
自我检测
初级版
// //代码一
setTimeout(()=>{
console.log('timeout')
},0)
Promise.resolve(1).then(
value => console.log('成功1',value)
)
Promise.resolve(2).then(
value => console.log('成功2',value)
)
console.log('主线程')
//代码二
setTimeout(()=>{
console.log('timeout1')
})
setTimeout(()=>{
console.log('timeout2')
})
Promise.resolve(1).then(
value => console.log('成功1',value)
)
Promise.resolve(2).then(
value => console.log('失败2',value)
)
//代码三
setTimeout(()=>{
console.log('timeout1')
Promise.resolve(5).then(
value => console.log('成功了5')
)
})
setTimeout(()=>{
console.log('timeout2')
})
Promise.resolve(3).then(
value => console.log('成功了3')
)
Promise.resolve(4).then(
value => console.log('失败了4')
)
高级版
setTimeout(() => {
console.log('0');
},0);
new Promise((resolve,reject) => {
console.log('1');
resolve()
}).then(()=>{
console.log('2');
new Promise((resolve,reject) => {
console.log('3');
resolve();
}).then(()=>{
console.log('4');
}).then(()=>{
console.log('5');
})
}).then(()=>{
console.log('6');
})
new Promise((resolve,reject) =>{
console.log('7');
resolve();
}).then(()=>{
console.log('8');
})