为什么要引入异步?

JS运行在浏览器中是一个单线程的,即只能同时做一件事。JS的代码如果是按照我们编写的顺序一行一行地执行,那如果遇到像定时器这样的任务,我们的页面就会卡住,一直等定时器的代码执行完成。这样用户的体验就会非常地差。这时候,如果我们就引入了异步。
异步就是在主线程中发送一个子线程来完成我们的异步任务,异步任务一般都是耗时间比较长的,例如:定时器、Ajax请求。引入了异步后,我们的浏览器如果遇到异步任务,则会跳过异步任务执行我们的同步任务的代码,异步任务由另一个子线程来完成。这样我们的浏览器就不会出现卡顿现象。

任务分为两种,一种为同步,一种为异步

同步任务

代码按照我们编写的顺序执行。

异步任务

代码不是按照我们编写的顺序进行执行。例如:ajax、定时器、加载一张图片等

回调函数的引入

由于在执行异步任务时,子线程是独立于主线程的,当我异步任务完成时,我是没有办法检测到子线程的异步任务的完成情况。所以我们要给异步任务传一个回调函数,这个回调函数就是在异步任务完成的时检测到异步任务已经完成,并且这个回调函数回到主线程来执行。

异步任务的实现原理

先看一段代码:

function callback()
{
    console.log("calkback");
}
let timer=setTimeout(callback,1000);
console.log("123");

这里的执行顺序是:

lua 异步执行 异步实现_javascript


简单来说,浏览器先会按照顺序执行我们的同步代码,如果遇到异步代码,它会跳过异步代码进行下一个同步任务的执行。同步任务执行完,有回调函数回来才进行回调函数的执行。

是不是说了还不懂,我们看下面的详细介绍。

lua 异步执行 异步实现_开发语言_02


如上图:当我们执行到let timer=setTimeout()时,我们将setTimeout函数推入Call Stack函数执行栈,遇到异步任务,将异步任务放到Web APIs等待它的回调执行时机:1s后,此时的主线程并不会停下来。setTimeout执行完成之后就会出栈,console.log()入栈,打印出"123",然后栈。1s之后,callback进入Callback Queue,当我们的同步任务执行完,执行调用栈为空时,就会进行事件的轮询,即循环Callback Queue,遇到callback,将他调入Call Stack,执行完成之后,使得它出栈。

总结:

1.异步任务的回调总是等待同步任务执行完
2、只有当函数调用栈为空时,才会进行Event Loop。
3、Callback Queue中的任务只用放入Call stack中才会执行。

异步任务与DOM渲染

let body=document.getElementsByTagName("body")[0];
let paragraph=document.createElement("p");
paragraph.innerHTML="Hello";
body.appendChild(paragraph);
console.log(body.children.length);
alert("已包含DOM,但是还没有进行渲染,body含有的子元素"+body.children.length);
setTimeout(function(){
   alert("111");
,1000);

执行的效果如下:

lua 异步执行 异步实现_异步任务_03


lua 异步执行 异步实现_异步任务_04


(1s后弹出框:111)

由执行的效果可知:DOM渲染是在JS同步代码执行完成之后,再异步回调函数调用之前。

总结:当函数调用栈为空时,才进行DOM的渲染,DOM渲染完成之后才开始进行事件的轮询机制。

宏任务与微任务

宏任务

Web APIs:setTimeout、ajax、setInterval等等

微任务:

ES6的新特性:Promise、await/async

微任务与宏任务的执行时机

微任务它有它自己的队列:macro Queue。微任务比宏任务执行时机要早,微任务是在DOM渲染前进行执行的,宏任务是在DOM渲染后进行执行的。