篇头
我们工作或面试中,经常遇到这些问题:如何更编写更优美的异步代码?你了解promise, async/await吗?知道他们的执行顺序吗?那么接下来我们将从浅入深逐步吃透这些问题。
引子
众所周知 Javascript 是采用的单线程的工作模式?那么为什么会用这种模式呢?
很重要的一点是因为我们页面交互的合适是操作DOM,为避免多线程可能会产生的线程同步问题,因为采用的是单线程工作模式。
- 优点:程序执行更安全、更简单
- 缺点:容易被耗时长的任务卡住,造成程序假死
在这种单线程的工作模式下,我们为解决以上问题就出现了:同步模式、异步模式
同步模式
大多数任务都会异常同步模式执行,任务会一次进入任务的调用栈依次执行,执行后推出调用栈,这里我们不过多展开。
异步模式
一般多是耗时长的任务,开启后就立即执行下个任务,而自己本身的任务是已回调函数的方式去处理后续逻辑。这是我们解决单线程模式缺点的关键。
Promise
Promise 对象代表一个异步操作,有三种状态:
- pending: 初始状态,不是成功或失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
const promise = new Promise((resolve,reject) => {
rej(new Error("promise rejected"))
})
promise.then(val => {
console.log("resolve", val);
}).catch(err => {
console.log("reject", err);
})
console.log("end")
// end
// eject Error: promise rejected
then方法的参数是函数,如果不是函数可以直接无视
模拟Ajax
function ajax(url) {
return new Promise((resolve,reject) => {
let xhr = new XMLHttpRequest()
xhr.open("get", url)
xhr.responseType = "json"
xhr.onload = function() {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax("package.json").then(res => {
console.log(res)
})
由此我们可以看出promise
的本质就是已回调函数的方式去定义异步任务结束之后需要执行的任务
但是好多刚开始学习用promise
的同学经常会陷入一个误区,一个异步任务在另一个异步任务回调结束后执行仍然使用了嵌套,这就背离了我们使用promise
的初衷。那么解决这个问题的方式就是利用promise
的链式调用
Promise的链式调用
const promise = new Promise((resolve,reject) => {
resolve()
})
promise.then(() => {
return 1;
}).then((res) => {
console.log(res);
// 输出1
}).then(() => {
console.log("3");
})
-
Promise
的then
方法会返回一个全新的Promise
对象 - 后面的
then
都是在为上一个的then
返回的promise
添加状态明确过后的回调 - 前面的
then
方法中回调函数的返回值会作为后面then
方法回调的参数 - 如果回调中返回的是
Promise
, 那么后面的then
方法的回调会等待它的结束
Promise的异常处理
常见的异常处理
//第一种
promise.then(resolve => {
console.log("resolve", resolve);
}, reject => {
console.log("reject", reject);
})
//第二种
promise.then(resolve => {
console.log("resolve", resolve);
}).catch(err => {
console.log("reject", err);
})
是挂载在全局上,用来处理代码中没有被手动捕获的异常,虽说提供了该方法,但还是建议全手动捕获可能出现的异常
浏览器中的使用方式:
window.addEventListener("unhandledrejection", event => {
const {reason, promise} = event;
//reason => Promise 失败的原因, 一般是个对象
//promise => 出现异常的 Primose对象
event.preventDefault()
},false)
node
环境下的使用方式:
process.on("unhandledRejection", (reason, promise) => {
//reason => Promise 失败的原因, 一般是个对象
//promise => 出现异常的 Primose对象
})
Promise的静态方法
resolve
Promise.resolve("1")
.then(val => {console.log(val)})
new Promise((resolve,reject) => {
resolve("1")
})
//两者等价
let p1 = new Promise((resolve,reject) => {
resolve("1")
})
let p2 = Promise.resolve(p1)
console.log(p1 === p2)
// true
由此可见将一个Promise
对象包裹在另一个Promise
对象里面返回的还是最开始的Promise
初此之外还一个特殊的点,如果Promise
的参数是一个包含着如下的一个then
的对象,那么也可以当做一个回调去执行
Promise.resolve({
then: (onFulfilled,onReject) => {
onFulfilled("test 1")
}
}).then(val => {
console.log(val)
})
// test 1
reject
同理
Promise.reject("err").catch(err => console.log(err))
Promise 并行执行
我们常见的场景是一个页面加载时需要同时请求多个接口,而接口之间又不相互关联,假如依次串行请求明显时间会很长,那么我们需要将其并行执行。
那么如何知道并行的请求都已完成返回的时间点呢?那么需要我们利用到Promise.all()
Promise.all
Promise.all([promise1, promise2])
.then(res => {
//res 是一个返回值的数组
})
.catch(err => {
//只要有一个promise 异常
})
Promise.race
Promise.all([promise1, promise2])
.then(res => {
//等待第一个promise结束
})
Promise的执行时序 (宏任务、微任务)
宏任务
大部分异步调用都是宏任务
微任务
常见微任务: Promise & MutationObserver & process.nextTick(node环境下)
Generator 异步方案
Generator(生成器函数)是ES2015被提出来的, 下面我们先回顾一下
function * foo() {
console.log("start")
try{
let val = yield "foo"
console.log(val)
} catch(e) {
console.log(e)
}
}
const generator = foo() //没有立即执行
const result1 = generator.next() //会执行foo() 直到遇到关键字 yield为止
console.log(result1)
//start
//{value: "foo", done: false}
const result2 = generator.next("bar") //刚才yield继续往下执行, next里的参数可以复制给 yield前边的变量
console.log(result2)
// bar
generator.throw(new Error("generator error"))
那么接下来我们可以利用Generator
中这一特性结合Promise
使用
function * main() {
const json = yield ajax("package.json") //那么我们这里就近似一种同步的书写方式
console.log(json)
}
const g = main();
const result = g.next();
result.value.then(data => {
g.next(data)
})
为避免多个promise造成代码的堆积,我们可以利用一下递归, 并且再做一下封装
function * main() {
const json1 = yield ajax("package.json")
console.log(json1)
const json2 = yield ajax("package.json")
console.log(json2)
const json3 = yield ajax("package.json")
console.log(json3)
}
function co(generator) {
const g = main();
function loopHandle(result) {
if (result.done) return
result.value
.then(data => loopHandle(g.next(data)), err => g.throw(err))
}
loopHandle(g.next())
}
co(main)
Aysnc/Await
aysnc/await
是generator的语法糖
async function main() {
const json1 = await ajax("package.json")
console.log(json1)
const json2 = await ajax("package.json")
console.log(json2)
const json3 = await ajax("package.json")
console.log(json3)
}
main()