彻底搞懂JavaScript异步机制

彻底搞懂JavaScript异步机制

JavaScript 事件循环机制

想要搞懂JavaScript的异步机制,一定要先搞清楚JavaScript是怎么执行任务。

单线程的JavaScript


​ JavaScript是单线程的这点大家应该都是知道的,为什么JavaScript要被设计成为一门无法多线程的语言?这是因为,程序员在编写多线程的程序时,时常会陷入麻烦当中,如死锁、可读性差等问题。当JavaScript被设计出来的时候,我们只想让它作为一门简单的、动态的语言帮助我们处理HTML。

​ 在最新的HTML5标准中,引入了Web Worker标准,这使得JavaScript具有了多线程功能。

任务队列与执行栈


​ 单线程想要执行任务,就必须有一个任务队列,当执行完前一个任务时,再去执行下一个任务。这样又引出了一些问题:如果前一个任务是等待服务器的响应,而这时服务器选择挂起这次http请求,或者说这次响应时间非常的慢。这样就会导致我们的CPU存在大量的空闲时间,并且任务无法释放,也无法执行下一个任务。

​ 熟悉操作系统的同学,可能会想到解决方案:并发
彻底搞懂JavaScript异步机制_第1张图片

​ 并发与并行并不是一个概念。并发指的是在同一时间间隔执行不同任务。因为时间间隔很短给人一种所有任务在同步进行的错觉。

执行栈:我们从任务队列取出可以执行的任务,将其入栈,等待执行。

我们继续JS的任务队列,JS存在两条任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks),或者我们理解为一种是同步任务,另一种是异步任务。

  • 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • 微任务:process.nextTick, Promise, Object.observer, MutationObserver.

浏览器的Event Loop


我们上面讲到,当执行栈空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:

  1. 取一个宏任务来执行,若碰到异步。执行完毕后,下一步。
  2. 取一个队列中已完成的微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

这块插点话,前段时间朋友说我这块逻辑有点难懂,所以详细解释一下。

首先,我们的执行栈存在的是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.

当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

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函数是异步的终极解决方案!

如有错误,望请指出,共同进步,不胜感激。

参考:

  1. JavaScript为什么是单线程?
  2. 理解javascript中的事件循环(Event Loop)
  3. JavaScript 异步、栈、事件循环、任务队列

你可能感兴趣的:(JavaScript)