天生同步
js一开始是被设计出来用于进行dom操作,如果存在多个线程对同一个dom进行操作则可能存在部分操作无效化的问题,最后可能只采纳一个线程对dom的操作,简单起见,js被设计成了单线程,这样就避免了复杂的线程操作,但是js就没有办法做多线程的操作了。
如何异步
虽然js是单线程的,但是页面是多线程的,页面的线程有:
1.GUI渲染线程
负责渲染浏览器界面。
2.JS引擎线程
主线程负责解析Javascript脚本,运行同步代码。
3.事件触发线程
处理用户操作事件。
4.定时触发器线程
setInterval与setTimeout所在线程。
5.http请求线程
用于向外部服务器发送请求,每次请求都会新开一个线程。
7.轮询处理线程*
用于分发主线程提交的异步事件到对应的线程,以及维护事件队列。
有了这些基础,js就可以做一些异步的操作了。Js代码在执行中会把同步的函数放入到同步任务栈中,栈内函数依次执行,执行完后退栈,遇到异步事件时会把事件交由轮询处理线程,轮询处理线程分发给对应的线程处理,比如说定时器会交给定时触发器线程处理,http请求会交给http请求线程处理,事件处理完成后对应的线程通知轮询处理线程,轮询处理线程会把事件及其回调加入到事件队列中,当同步任务栈清空时,主线程通知轮询处理线程,轮询处理线程会从事件队列中获取队首的事件,如果该事件没有回调函数则销毁该事件,否则将该事件交由主线程入栈执行,执行完成后函数出栈,主线程进行下一个循环,这个过程叫事件循环,事件循环会持续到任务队列清空。
有这样一个问题
有这样一段代码:
var data='nothing'
ajax({
async:true,
success:(res)=>{
data=res.data
}
})
console.log(data)
毫无疑问,打印出来的是nothing,因为js解析到ajax那一块时会把时间丢给对应的线程处理,没等它处理完对data赋值就执行console,在ajax回调函数执行前是拿不到服务器返回的数据的,这个问题可能困扰过一些初学的新手。
如何解决
最简单的方法就是async设置为false,但是这样做会导致代码执行到这里是主线程被挂起,知道请求完成,此时可能造成页面白屏或者用户无法操作页面,用户体验极差。
或者我们可以改一下代码:
function getData(cb){
ajax({
async:false,
success:(res)=>{
cb(res.data)
}
})
}
function logData(data){
console.log(data)
}
getData(logData)
使用回调函数的形式,我们可以在不阻塞页面的情况下获取了异步代码中的值。核心理念就是先定义,需要时再执行。
新的需求来了
有三个请求, 现在要求后面一个请求要带上前面的请求的结果。改一下代码:
function getData(cb1,cb2){
ajax({
success:(res)=>{
cb1(res.data,cb2)
}
})
}
function getData2(data,cb){
ajax({
data:data
success:(res)=>{
cb(res.data)
}
})
}
function getData3(data){
ajax({
data:data
success:(res)=>{}
})
}
getData(getData2,getData3)
此时需要不断的传递回调函数,写大量的代码,不直观还容易出错,这就是人们所说的‘回调地狱’。
promise来了
es6中我们有了promise这一强大的api解决了es5中回调地狱的问题。上面的代码可以改为:
promise.resolve()
.then(()=>{
return new promise((resolve)=>{
ajax({
success:(res)=>{
resolve(res.data)
}
})
})
})
.then((data)=>{
return new promise((resolve)=>{
ajax({
data:data,
success:(res)=>{
resolve(res.data)
}
})
})
})
.then((data)=>{
return new promise((resolve)=>{
ajax({
data:data,
success:(res)=>{
resolve(res.data)
}
})
})
})
这样写代码变得直观一点了,原来的一层层的套娃变成了一条调用链,如果要增加环节,只需要简单的复制一份代码改改就可以了,也容易维护,promise结合all和rush这两个api可以完成日常开发中的大多数需求。但是仍然存在缺点:链式调用之间跨环节传参麻烦而且代码仍然不够简洁。
终极方案
这个方案毫无疑问就是async/await,其实这两api就是promise.resolve()和then()的语法糖,借助他们,我们的异步代码会变得非常直观而且容易维护,把上面的代码改一下:
function getPromise1(){
return new promise((resolve)=>{
ajax({
success:(res)=>{
resolve(res.data)
}
})
})
}
function getPromise2(data){
return new promise((resolve)=>{
ajax({
data:data,
success:(res)=>{
resolve(res.data)
}
})
})
}
function getPromise3(data){
return new promise((resolve)=>{
ajax({
data:data,
success:(res)=>{
resolve(res.data)
}
})
})
}
async function asyncFn(){
let data1=await getPromise1()
let data2=await getPromise1(data1)
let data3=await getPromise1(data2)
}
asyncFn()
这样子直接把套娃和链条分割成了一块一块的代码,可读性和可维护性非常高,而且可以根据需求调整asyncFn函数内await 的执行顺序和传参,完美的解决了上面提出的问题。