想要搞懂JavaScript的异步机制,一定要先搞清楚JavaScript是怎么执行任务。
JavaScript是单线程的这点大家应该都是知道的,为什么JavaScript要被设计成为一门无法多线程的语言?这是因为,程序员在编写多线程的程序时,时常会陷入麻烦当中,如死锁、可读性差等问题。当JavaScript被设计出来的时候,我们只想让它作为一门简单的、动态的语言帮助我们处理HTML。
在最新的HTML5标准中,引入了Web Worker标准,这使得JavaScript具有了多线程功能。
单线程想要执行任务,就必须有一个任务队列,当执行完前一个任务时,再去执行下一个任务。这样又引出了一些问题:如果前一个任务是等待服务器的响应,而这时服务器选择挂起这次http请求,或者说这次响应时间非常的慢。这样就会导致我们的CPU存在大量的空闲时间,并且任务无法释放,也无法执行下一个任务。
并发与并行并不是一个概念。并发指的是在同一时间间隔执行不同任务。因为时间间隔很短给人一种所有任务在同步进行的错觉。
执行栈:我们从任务队列取出可以执行的任务,将其入栈,等待执行。
我们继续JS的任务队列,JS存在两条任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks),或者我们理解为一种是同步任务,另一种是异步任务。
我们上面讲到,当执行栈空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:
这块插点话,前段时间朋友说我这块逻辑有点难懂,所以详细解释一下。
首先,我们的执行栈存在的是
sript
任务,也就是全局任务,我们取它来进行执行,而在下面代码中cosole.log()
并不是宏任务。且我们在执行宏任务时,遇到异步操作,会在后台注册一个回调,当完成时,会将结果放入微任务队列等待事件循环(event loop)执行。
浏览器会不断重复这三步。
看一个例子:
console.log('script start');//我不是 宏任务或者微任务,直接执行我就完事!
// 微任务
Promise.resolve().then(() => {
console.log('p 1');//then()放入微任务队列
});
// 宏任务 setTimeout本身是宏任务 放入宏任务队列 继续执行我们当前的`script`全局宏任务
setTimeout(() => {
console.log('setTimeout');
}, 0);
var s = new Date();
while(new Date() - s < 50); // 阻塞50ms 我也不是宏任务直接执行
/*
上面之所以加50ms的阻塞,是因为 setTimeout 的 delayTime 最少是 4ms. 为了避免认为 setTimeout 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。
*/
// 微任务
Promise.resolve().then(() => {
console.log('p 2');
});
console.log('script ent');
/*** output ***/
// one macro task
script start
script ent
// all micro tasks
p 1
p 2
// one macro task again
setTimeout
首先,我们的执行栈是空的,要从任务队列取一个任务,我们先取一个script任务,也就是第一次输出的
// one macro task
script start
script ent
然后,我们再次从任务队列取任务,这一次取出所有微任务,也就会输出
// all micro tasks
p 1
p 2
最后,再次取得一个宏任务
// one macro task again
setTimeout
这里还要说一下微任务队列的产生,它与事件和异步函数有着密不可分的关系。
当我们运行一个异步函数时,它会与主线程并发执行,当异步函数执行完毕后,会向任务队列提交一个事件,告诉它:我已经拿到结果,等待回调!,后面的执行过程如上Event Loop所说一样。
我们此次不介绍Promise 函数的基础知识,如果对此不是很熟悉的同学请移步:Promise.
当Promise 被创建时,它会自动调用,例:
let prom = new Promise((resolve,reject)=>{
console.log("我会被直接执行")
setTimeout(()=>{
console.log("过了三秒!")
resolve()
},3000)
});
//out print
我会被直接执行
// 3s
过了三秒!
但有时候我们想封装一个异步操作,比如我们有一个需求:在与服务器建立websocket连接下收到信息,将信息通过ajax,发送到服务器。
let wss = new Object();
function onMsg(msg,state){
return new Promise((resolve,reject)=>{
if(state == true){
resolve(msg)
}else{
reject(msg)
}
})
}
function ajaxSendToServer(){
return new Promise((resolve,reject)=>{
console.log("ajax开始发送!")
setTimeout(res=>{
console.log("ajax已发送到服务器!")
resolve()
},3000)
})
}
wss.onmessage = onMsg
/*
可以看到这这一块,我们出现了回调金字塔。
逻辑似乎不太清楚了
*/
wss.onmessage("data",true).then(res=>{
console.log("接收到服务器消息!")
ajaxSendToServer().then(res=>{
///.....
console.log("继续回调")
})
}).catch(error=>{
console.log("服务器错误消息!")
})
async和await 是 Generator 函数与yield的语法糖, Generator 函数我就不在多讲了,不熟悉的同学可以看一下阮一峰老师的,Generator 函数的语法
async函数可以让我们的异步函数像同步一样进行。
async/await 永远是和Promise一起用的,我们用async函数改写上面的例子
let wss = new Object();
function onMsg(msg,state){
return new Promise((resolve,reject)=>{
if(state == true){
resolve(msg)
}else{
reject(msg)
}
})
}
function ajaxSendToServer(){
return new Promise((resolve,reject)=>{
console.log("ajax开始发送!")
setTimeout(res=>{
console.log("ajax已发送到服务器!")
resolve()
},3000)
})
}
async function asyncOnMsg(){
let i = await onMsg("hello",true);
console.log(i)
console.log("接收到信息")
let i1 = await ajaxSendToServer();
console.log("ajax完成")
}
asyncOnMsg().then(res=>{
console.log("async完成")
})
//out put
hello
接收到信息
ajax开始发送!
ajax已发送到服务器!
ajax完成
async完成
我们使用一个asyncOnMsg的async函数封装了整个异步过程,我们下面分析一下它的执行过程。
首先,程序执行asyncOnMsg(),在第一个await出停下,等待结果,结果返回给变量i
然后,执行console.log(i),输出hello,
再次,console.log(“接收到信息”),
往下,又停止在await位置,等待ajaxSendToServer()执行完毕。
其次,输出console.log(“ajax完成”)。
最后,asyncOnMsg执行完成,返回一个Promise对象,调用then()方法,执行console.log(“async完成”)。
至此整个函数执行完毕。
可以看到,我们已经把异步的逻辑整的非常明白了,这得益于async函数。
async函数是异步的终极解决方案!
参考: