# 一、Promise 的 2W1H
# W1 什么是 Promise
Promise 是一个函数,为了解决回调地狱的一个函数,它在 js 中进行异步编程的新解决方案;它有三个状态和两个改变状态的方法,以及三个 Promise 常用的方法
# 三个状态
- pengding 执行中
- fulfilled 已成功,正常完成
- rejected 已失败,执行以错误结束
# 两个改变状态的方法
- resolve () 把状态改成 fullfilled
- reject () 把状态改成 rejected
# 三个常用的方法
- then()
- catch()
- finally()
# W2 什么时候使用 Promise?
解决多层套娃的异步调用导致项目代码向右下漂移,以一种只向下扩展的方式使得代码看上去就像不是异步代码一样,读起来不费劲,更优雅一些;
不使用 Promise 的代码如
setTimeout(()=>{ | |
console.log('套娃第一次开始') | |
setTimeout(()=>{ | |
console.log('套娃第二次开始') | |
setTimeout(()=>{ | |
console.log('套娃第三次开始') | |
setTimeout(()=>{ | |
.... | |
},4000) | |
},3000) | |
},2000) | |
},1000); |
一层套一层,右下漂移了!!😲 通过 Promise 改造,可以得到以下样子的代码
new Promise((resolve, reject)=>{ | |
setTimeout(()=>console.log('套娃第一次开始'),1000) | |
resolve() | |
}).then(()=>{ | |
return new Promise((resolve,reject)=>{ | |
setTimeout(()=>console.log('套娃第二次开始'),2000) | |
resolve() | |
}) | |
}).then(()=>{ | |
return new Promise((resolve,reject)=>{ | |
setTimeout(()=>console.log('套娃第三次开始'),3000) | |
resolve() | |
}) | |
}) |
看上去是不是心情好得多呢,甚至可以进一步优化成下面这样:
function createTimeout(logContent,intevel=1000){ | |
return new Promise((resolve, reject)=>{ | |
setTimeout(()=>console.log(logContent),intevel) | |
resolve() | |
}) | |
} | |
createTimeout('套娃第一次开始') | |
.then(()=>createTimeout('套娃第二次开始',2000)) | |
.then(()=>createTimeout('套娃第三次开始',3000)) |
这个代码看上去就赏心悦目多了✌️
# 1H 如何使用
如 W2 的第二部分代码可以看到,可以通过 new 的方式构造一个 Promise, 其中构造函数传入一个函数,这个函数带入两个参数 (resolve, reject),他们就是 W1 里面所说的两个改变状态的方法 🌹
如果方法体正常完成,则调用一下 resolve (), 失败就调用一下 reject ();
# 二、使用详解
# 如何参数传递?
上面说的方法体内容里面其实是没有传参数到下一个异步执行的方法?自然是直接放到 resolve (),然后下一步通过 then () 的形参方法参数直接传入到下一个异步执行的方法体,比如我们常需要在项目中拿到用户信息之后,根据用户 id 拿到角色列表,这肯定就需要第一次异步请求拿到的用户 Id,传入第二次异步请求
我们假设 user.json
和 role.json
请求返回的结果分别为
//user.json | |
{ | |
"userId": 666, | |
"name": "张三" | |
} |
//role.json?userId=666 | |
{ | |
"userId":666, | |
"roles":["管理员","普通用户"] | |
} |
function getData(url, params={}){ | |
return new Promise((resolve,reject)=>{ | |
// 用了 jQuery 的 ajax 方法,调试的话请先引入 jQuery | |
$.ajax({ | |
type:"GET", | |
url: url, | |
params:params, | |
success:(res)=>{ | |
resolve(res); | |
} | |
}) | |
}) | |
} | |
getData('user.json') | |
.then((userInfo)=>{ | |
const {userId} = userInfo; | |
console.log("用户ID: "+userId) | |
return getData('role.json',{userId:userId}) | |
}) | |
.then((roleData)=>{ | |
console.log(roleData) | |
}) |
如上上面通过 resolve 方法把 ajax 请求的结果传给了 then, 在 then 的参数分别通过 userInfo 和 roleData 接收,然后对数据进行了进一步的处理;
# 什么时候使用 reject ()
说清楚这个问题之前,先说一下 W1 里面说的三个状态,pengding 执行中,fulfilled 已成功,rejected 已失败,执行中基本上不用关注,我们通过执行下面代码
console.dir(new Promise((resolve,reject)=>{})) |
然后浏览器 F12 查看打印结果可以发现 Promise 是这样的,这个 Promise 对象的状态 PromiseState 是 pengding
然后我们通过在方法体调用 resolve ()
console.dir(new Promise((resolve,reject)=>{resolve()})) |
发现此时 PromiseState 的状态已经改成 fulfilled;
如果调用 reject ()
console.dir(new Promise((resolve,reject)=>{reject()})) |
发现 PromiseState 已经改成为 rejected,这就是 Promise 的三种状态和两种改变状态的方法,那么既然 resolve 表示正常的时候执行,并且可以把执行结果传入到下一次异步调用进行使用,那么 reject 方法能传值吗?传的值去哪里了?怎么接收?
答案三连:
- 能传值
- reject 代表执行过程出现错误了,自然是给接收错误的地方
- 通过 catch () 方法接收
同样我们通过上面的例子,假如 user.json
由于服务器或者网络原因不能被调用了,我们需要在 ajax 的方法加上一个出现错误的处理情况
function getData(url, params={}){ | |
return new Promise((resolve,reject)=>{ | |
// 用了 jQuery 的 ajax 方法,调试的话请先引入 jQuery | |
$.ajax({ | |
type:"GET", | |
url: url, | |
params:params, | |
success:(res)=>{ | |
resolve(res); | |
}, | |
error:()=>{ // 这里加上执行错误的处理 | |
reject("调用超时或者服务器错误") | |
} | |
}) | |
}) | |
} | |
getData('user1.json') // 直接把调用改成不存在的 user1.json | |
.then((userInfo)=>{ | |
const {userId} = userInfo; | |
console.log("用户ID: "+userId) | |
return getData('role.json',{userId:userId}) | |
}) | |
.then((roleData)=>{ | |
console.log(roleData) | |
}) | |
.catch(msg=>{ | |
console.log("接收的错误消息: " + msg) | |
}) |
通过上面的代码,我们发现 reject 传入的消息通过 catch 方法接收了,这就是三大方法中的 then 方法和 catch 方法了,剩下一个 finally () 意思就是无论执行过程中是否正常执行,都会执行里面的代码,和 java 的 finally 代码意思一致,这里需要说明的是 catch 方法,除了能捕获 reject () 调用改变 Promise 状态的结果之外,还能能捕获上面所有执行的异步代码块的问题,如果前面的异步方法执行发生错误,后面的 then () 不会被执行。
# 三、async 和 await 的使用
简单说这两个关键字是基于 Promise 之上的语法糖,它们能使得对异步的操作更加简洁明了
# async 和 await 的基本使用
我们可以对方法名进行 async 修饰,标记这个方法为异步方法, 意思就是这个行数返回的值是 Promise 对象
async function myFunc(){ | |
return "async function result" | |
} | |
console.log(myFunc()) |
比如上面的代码的打印结果就是一个 Promise 结果为 "async function result"
这个字符串,状态 fulfilled
的 Promise 对象;
如果我们要在这个方法里面对异步方法结果进行操作,则可以通过下面的方式使用异步方法执行之后的结果,这个例子我们使用上面的 getData 方法
async function myFunc(){ | |
let res = await getData("user.json"); | |
console.log("await请求结果: "+res.userId) | |
} | |
console.log(myFunc()) | |
console.log("后面执行的代码") |
题外:上面方法没有返回结果,想想打印出来的结果是什么?
😃
这里虽然加上了 await,代码看上去也非常像是同步的代码,会暂停程序的往下执行,但在等待的过程中 javascript 还可以同时处理其他的任务,比如界面的更新,执行其他的程序代码等,如上面代码中最后一行的打印会在 async 方法打印之前打印出来
# await 使用陷阱
# 为保证效率,可以组合使用之后再用 await 方法,比如
async function myFunc(){ | |
let user1 = await getData("user1.json"); | |
let user2 = await getData("user2.json"); | |
// 下面省略对 res1 和 res2 的操作 | |
//..... | |
} |
可以使用以下方式
async function myFunc(){ | |
let res1 = getData("user1.json"); | |
let res2 = getData("user2.json"); | |
const {user1,user2} = await Promist.promiseAll([res1,res2]) | |
// 下面省略对 res1 和 res2 的操作 | |
//..... | |
} |
效率理论上能提升一倍
# 循环中使用异步操作,不能调用 forEach ()/map ()
let arr = [1, 2, 3] | |
arr.forEach(async element => { | |
let {userId} = await getData("user.json") | |
console.log(element + " - " + userId) | |
}); | |
console.log("the end") |
如上第二行代码不会等到所有代码执行完毕再执行,而是马上执行后面的 "the end"
输出,尽管使用了 await 进行修饰,可以使用传统的 for 循环来达到我们先要的效果
async function myFunc(){ | |
for(i=1;i<4;i++){ | |
let {userId} = await getData("user.json") | |
console.log(i + " - " + userId) | |
}; | |
console.log("the end") | |
} | |
myFunc() |
# 不能在全局和普通函数中使用 await,必须在 async 修饰的函数体里面使用
不在 async 函数体里面会报错😒