同步与异步
什么是同步?
同步
意为代码必须一步一步走下去。只有执行完当前的代码,才会执行下一条代码
let x = 1
let y = x + 1
console.log(y)
2
什么是异步?
异步
则是代码的执行顺序不一样了。
如上一条指令写了一条三秒的延时,或上一条指令需要等待接口的返回,而下一步指令却已经执行了
上一条代码还未执行完成,下一条却已经开始执行了,就是异步
setTimeout(() => {
let z = 1
});
console.log(z)
Uncaught ReferenceError: z is not defined // 没有执行到为z赋值的语句就执行了console.log
早期javascript处理异步的方式
早期javascript对异步函数的处理是稍许僵硬的,如:
异步代码的返回值,只能在异步代码执行结束之后调用回调函数传递
function YiBu(callback){
setTimeout(() => {
let x = 1 + 1
callback(x)
})
}
function someFunc(x){
console.log(x)
}
YiBu(someFunc)
2
这种方法只能解决简单的异步问题,遇到较复杂的代码则容易遇上回调地狱
为了解决回调地狱的问题,ES6就出现了专门解决回调地狱的工具-Promise
什么是Promise?
Promise:期约,承诺,约定
Promise是现在JavaScript的一种优秀的异步解决方案,解决了回调地狱的问题。
平时异步编程时一般常用的有async
和await
使用Promise需要先使用new关键字创建一个Promise的实例出来,并且在构造函数的入参携带一个匿名函数,叫做执行器
let a = new Promise(() => {
// 我是执行器
})
console.log(a)
Promise {<pending>}
返回了一个默认为pending
状态的Promise实例
在创建Promise实例时,入参的执行器是必填的
Promise的状态
每个Promise实例都有对应的状态,分别为pending
,resolved
,rejected
Promise执行器
执行器是Promise构造函数的入参,主要有以下两个作用:
- 初始化Promise的行为
- 控制Promise的状态转换
你可以在这个执行器中任意地方改变这个Promise的状态,如在定时器中
let a = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('定义了一个解决的值')
}, 2000);
});
console.log(a)
亦或是在接口拿到值之后:
const xhr = new XMLHttpRequest()
let a = new Promise((resolve, reject) => {
xhr.onreadystatechange = () => {
if(xhr.readyState != 4) return;
if(xhr.readyState == 4 && xhr.status == 200){
// 请求成功的处理
resolve(xhr.responsetext)
}else {
// 请求失败的处理
reject('服务器错误')
}
}
xhr.open('get', url);
xhr.send(null)
});
console.log(a)
resolve
:解决、决定
如果你认为运行成功,就可以调用resolve
reject
:拒绝、驳回
如果你认为运行失败,就可以调用reject
这两个方法可以让Promise改变为对应的状态
执行器中,必须保证有这两个内部函数,通过两个相同名称的参数传到执行器里面来
触发promise对应的处理程序,需要改变Promise的状态。这就是改变Promise状态的作用
resolve()和reject()中的值即为解决时返回的值或拒绝时返回的值,若未填写则返回undefined
执行器中的代码块是和new Promise同步执行的
Promise.resolve和Promise.reject
原先通过执行器构造函数那种方式创建的Promise对象默认是Pending状态
使用Promise.resolve
和Promise.reject
方法可以直接创建一个已经解决/已经拒绝的Promise
这两个函数接受的参数可以是任意类型,也可以接收一个Promise。他们入参是任意类型
,出参是一个Promise
let a = new Promise((resolve, reject) => resolve(3))
console.log(a)
Promise {<fulfilled>: 3}
包装规则
目的:返回Promise类型的数据,方便.then
链式调用
入参基本可以分为以下几种情况:
// 基础数据类型
let p1 = Promise.resolve(1)
// 入参为一个Promise
let p2 = Promise.resolve(new Promise(() => {}))
// 入参是一个异常
let p3 = Promise.resolve(new Error(''))
// 多个参数
let p4 = Promise.resolve(1, new Promise(() => {}), new Error())
console.log(p1) // Promise {<fulfilled>: 1}
console.log(p2) // Promise {<pending>}
console.log(p3) // Promise {<fulfilled>: Error at <anonymous>:1:26}
console.log(p4) // Promise {<fulfilled>: 1}
- 一个值,无论是undefined、null还是false,表示的都是一个值,目标是一个Promise,即:包装成一个解决值为这个基础值的已解决Promise
- 已经是个Promise,传入这个Promise与入参Promise完全对应,直接返回pending
- 创建一个解决值为异常的Promise,所以返回值是一个解决值为这个异常的已解决Promise,
- 会自动忽略第一个参数外的其他参数,并将第一个参数按上述规则包装。
reject的包装规则大致相同
.then
.then
是Promise规范规定所有Promise必须支持的方法,实现这个then方法也叫做实现了Thenable接口
作用:为Promise添加处理程序的主要方式
入参:两个,分别是onResolved
和onRejected
,分别代表这个Promise状态被解决的时候执行和被拒绝的时候执行
返回值:一个Promise,如上文解释,可链式调用
入参规则:两个参数都是可选的。如果都提供,则分别在Promise进入解决状态或拒绝状态时执行,入参多于两个则自动忽略多余参数,不是函数也会自动忽略
let a = new Promise((resolve, reject) => {
resolve('111')
})
a.then((res) => {
// onResolved处理程序放这里
console.log(res)
console.log('解决')
},(res) => {
console.log('拒绝')
},(res) => {
// 如果有多余的参数则会被忽略
})
// 如若只想传递一个onRejected处理程序,第一个入参函数不可不写,需要写一个占位的空函数或无意义的非函数入参,建议写null
let b = new Promise((resolve, reject) => {
reject('111')
})
b.then(null, (res) => {console.log('推荐这样写');console.log(res)})
.catch/.finally
.catch
相当于一个小马甲,是.then中第二个参数onRejected的另一种写法
let a = new Promise((resolve, reject) => {
reject('111')
})
// 以下两种写法是完全相同的
a.then(null, () => {
console.log('拒绝')
})
a.catch(() => {
console.log('拒绝')
})
.finally
在Promise转换为已拒绝或者已解决时都会执行,一般用于兜底或清理代码
.finally和.catch也都返回一个Promise
非重入Promise方法
Promise状态改变的时候,对应的程序如onResolve和onRejected不是立马被执行的,而是会被排到一个异步事件队列,这个异步事件队列
一般情况是会在同步代码之后执行,同时这个异步事件队列中的事件讲究先进先出:即假如有两个Promise都改变到对应状态了,先改变状态的程序就先执行。
这里涉及到一个知识点叫EventLoop。
Promise .then里面的所有状态处理程序都不会立即执行,而是先放到这个异步事件队列里面,这就叫做非重入Promise方法
Promise连锁
这个概念其实就是Promise的链式调用,由于Promise的事件处理程序只会在Promise状态改变之后触发,而事件处理程序最后又会返回一个包装的Promise。
那么在处理程序中,再创建另一个异步事件的Promise,就可以继续逻辑清晰地调用.then,把多个任务像链子一样串起来
Promise.all
Promise.all将它入参的所有Promise全部resolved解决之后,它才会变成resolved,只要入参中有一个rejected,那么它就是rejected
入参:包含Promise的可迭代对象(一般传Promise数组就行)
let a = new Promise((resolve, reject) => {
console.log('promise1')
resolve(1)
})
let b = new Promise((resolve, reject) => {
console.log('promise2')
resolve(2)
})
let finalP = Promise.all([a.b])
console.log(finalP)
返回值:Promise
Promise.race
race:竞赛、竞争
Promise.race创建的新Promise就是让他入参的几个Promise进行竞赛,如果谁先改变状态,那么Promise.race的状态就改变成这个第一个改变状态的入参Promise状态
用例:如现在调用一个接口,创建一个Promise,然后和另一个定时Promise进行race。这里定时任务写个5秒,意为:接口要是5秒还没结果,那么定时任务就要拒绝了,这样就可以做个接口超时的处理
let a = new Promise((resolved, reject) => {
console.log('比如我是一个接口')
setTimeout(() => {
// 花一点时间接收数据
resolve('接收到了接口数据')
},10000)
})
let b = new Promise((resolved, reject) => {
setTimeout(() => {
reject('接收数据超时')
},2000);
})
let request = Promise.race([a,b])
request.then(res => {
console.log('解决:接口成功接收到了数据')
},res => {
console.log('拒绝:接口超时接收')
})
.then返回规则
- 由于.then的两个入参都是可选的,如果都不填,那么就会直接返回上一个Promise
传入了解决处理程序且上一个Promise resolve了,走进了onResolved回调,那么就得看返回值了:无返回值则返回undefined,有则返回包装成解决值为这个的resolve
let a = new Promise((resolve, reject) => { console.log('比如我调一个接口') resolve(111) }) let b = a.then((res) => { // 啥也不写,就等于返回一个undefined // 等同如下 return undefined }) let c = a.then((res) => { // 都返回reject的undefined return undefined return 1 return '1' return null })