Tip:个人博客主要在CSDN:CSDN博客 ,文章内容有修改的话也在CSDN上,感兴趣的能够去CSDN上关注我~javascript
JS中的事件循环原理以及异步执行过程这些知识点对新手来讲可能有点难,可是是必须迈过的坎,逃避是解决不了问题的,本篇文章旨在帮你完全搞懂它们。html
说明:本篇文章主要是基于浏览器环境,Node环境没有研究过暂时不讨论。文章的内容也是博采众长以及结合本身的理解完成的,相关参考文献文章末尾也会给出,如有侵权请告知整改,或有描述不正确的也欢迎提醒。文章会有点长,耐心看完哦~java
不废话,让咱们从简单到复杂,步步深刻,去享受知识的盛宴~git
咱们都知道JS是单线程执行的(缘由:咱们不想并行地操做DOM,DOM树不是线程安全的,若是多线程,那会形成冲突),one thread --> one call stack --> one thing at a time
,也就是说,它只有一个执行栈(call stack),在同一时刻,JS引擎只能作一件事(JS在执行时有一个很是重要的特性:run to complete
,只要运行就直到完成)。看到 “JS是单线程的”这句话的时候,不知道你有没有这样的疑惑:既然JS是单线程的,那么我在网页向后端请求数据的时候,我怎么还能够操做页面:我能够滚动页面,我也能够点击按钮,这不是跟JS是单线程的冲突吗?这个问题困扰了我很久,很大的一个缘由是:我觉得浏览器单单只是由一个JS引擎构成的。以下图(我觉得的浏览器构造,这里以谷歌浏览器chrome为例): github
function foo() {
bar()
console.log('foo')
}
function bar() {
baz()
console.log('bar')
}
function baz() {
console.log('baz')
}
foo()
复制代码
咱们定义了foo、bar、baz三个函数,而后调用foo函数,控制台输出的结果为:web
baz
bar
foo
复制代码
执行过程以下:chrome
console.log('baz')
,执行,在控制台打印:baz,而后baz函数执行完毕弹出执行栈。baz()
语句已经执行完,接着执行下一条语句(console.log('bar')
),在控制台打印:bar,而后bar函数执行完毕弹出执行栈。bar()
语句已经执行完,接着执行下一条语句(console.log('foo')
),在控制台打印:foo,而后foo函数执行完毕弹出执行栈。仍是图直观点,以上步骤对应的执行流程图以下: segmentfault
咱们改变一下代码1,代码2以下:后端
function foo() {
bar()
console.log('foo')
}
function bar() {
baz()
console.log('bar')
}
function baz() {
setTimeout(() => {
console.log('setTimeout: 2s')
}, 2000)
console.log('baz')
}
foo()
复制代码
其余都不变,就在baz函数中增长了一个setTimeout函数。根据1中的假设,浏览器只由一个JS引擎构成的话,那么全部的代码必然同步执行(由于JS执行是单线程的,因此当前栈顶函数无论执行时间须要多久,执行栈中该函数下面的其余函数必须等它执行完弹出后才能执行(这就是代码被阻塞的意思)),执行到baz函数体中的setTimeout时应该等2秒,在控制台中输出setTimeout: 2s
,而后再输出:baz
。因此咱们指望的输出顺序应该是:setTimeout: 2s -> baz -> bar -> foo(这是错的)。api
浏览器若是真这样设计的话,确定是有问题的!!! 遇到AJAX请求、setTimeout等比较耗时的操做时,咱们页面须要长时间等待,就被阻塞住啥也干不了,出现了页面“假死”,这样绝对不是咱们想要的结果。
实际固然并不是我觉得的那样,这里先重点提醒一下:JS是单线程的,这一点也没错,可是浏览器中并不只仅只是由一个JS引擎构成,它还包括其余的一些线程来处理别的事情。以下图(此图参考了Philip Roberts的演讲:《Help, I'm stuck in an event-loop》(YouTube版),被墙的能够看这:《Help, I'm stuck in an event-loop》(bilibili版),这视频推荐你们观看):
线程名 | 做用 |
---|---|
JS引擎线程 | 也称为JS内核,负责处理JavaScript脚本。(例如V8引擎) ①JS引擎线程负责解析JS脚本,运行代码。 ②JS引擎一直等待着任务队列中的任务的到来,而后加以处理。 ③一个Tab页(renderer进程)中不管何时都只有一个JS线程运行JS程序。 |
事件触发线程 | 归属于渲染进程而不是JS引擎,用来控制事件循环 ①当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其余线程,如鼠标点击、Ajax异步请求等),会将对应任务添加到事件线程中。 ②当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。 注意:因为JS的单线程关系,因此这些待处理队列中的事件都是排队等待JS引擎处理,JS引擎空闲时才会执行。 |
定时触发器线程 | setInterval和setTimeout所在的线程 ①浏览器定时计数器并非由JS引擎计数的。 ②JS引擎时单线程的,若是处于阻塞线程状态就会影响计时的准确,所以,经过单独的线程来计时并触发定时。 ③计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。 注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。 |
异步http请求线程 | XMLHttpRequest在链接后经过浏览器新开一个线程请求 将检测到状态变动时,若是设置有回调函数,异步线程就产生状态变动事件,将这个回调放入事件队列中,再由JS引擎执行。 |
GUI渲染线程 | 负责渲染浏览器界面,包括: ①解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等。 ②重绘(Repaint)以及回流(Reflow)处理。 |
这里让咱们对事件循环先来作个小总结:
让咱们来看看真正的浏览器中执行代码1是什么个流程吧! TIP:这里的流程示例网站也是Philip Roberts的演讲中提到的(是他本人写的),能够本身去尝试尝试:传送门
相信有了以上的铺垫以后,你对浏览器中JS的执行流程有点感受了,让咱们趁热打铁,进一步探讨事件循环和异步吧~
如今让咱们试试0秒延时的setTimeout执行会如何,按道理来讲0秒延迟就是当即执行,那么控制台打印结果应该为:setTimeout: 0s -> foo,事实如此吗?
function foo() {
console.log('foo');
}
setTimeout(function() {
console.log('setTimeout: 0s');
}, 0);
foo();
复制代码
实际控制台打印结果的顺序为:foo -> setTimeout: 0s,来看看实际代码执行的过程:
如今是时候再深刻一步了,咱们ES6中新增的promise已经火烧眉毛地亮相了!(本篇文章不讨论Promise相关的知识点,若是你对Promise不了解的话,建议先去看看相关知识点)。 其实以上的浏览器模型是ES5标准的,ES6+标准中的任务队列在此基础上新增了一种,变成了以下两种:
setTimeout(func)
便可将func
函数添加到宏任务队列中(使用场景:将计算耗时长的任务切分红小块,以便于浏览器有空处理用户事件,以及显示耗时进度)。queueMicrotask(func)
能够将func
函数添加到微任务队列中。那么,如今的事件循环模型就变成了以下的样子:
一图胜千言,画个流程图更加清晰,帮助记忆:
先来个只有Promise的例子热热身:
function foo() {
console.log('foo')
}
console.log('global start')
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('promise then')
})
foo()
console.log('global end')
复制代码
控制台输出的结果为:
global start
promise
foo
global end
promise then
复制代码
代码执行的解释:
console.log('global start')
语句,打印出:global start。new Promise(....)
,执行之(这里说明一点:在使用new关键字来建立Promise对象时,传递给Promise的函数称为executor,当promise被建立的时候executor函数会自动执行,而then里面的东西才是异步执行的部分),Promise参数中的匿名函数与主线程同步执行,执行console.log('promise')
打印出:promise。在执行resolve()
以后Promise状态变为resolved,再继续执行then(...)
,遇到then则将其提交给Web API处理,Web API将其添加到微任务队列(注意:此时微任务队列中已有一个Promise事件待处理)。foo()
,执行foo函数,打印出:foo。console.log('global end')
,执行后打印出:global end。至此,本轮事件循环已结束,执行栈为空。console.log('promise then')
,打印出:promise then。至此,新的一轮事件循环(Promise事件)已结束,执行栈为空。(注意:此时微任务队列为空)用动图来展现一下执行的流程(备注:该demo网站并未画出微任务队列,咱们需本身脑补一下microtask queue):
咱们已经对单独的宏任务和微任务的执行流程分别作了分析,如今让咱们混合这两种任务的事件来看看结果如何,来个代码示例小试牛刀:
function foo() {
console.log('foo')
}
console.log('global start')
setTimeout(() => {
console.log('setTimeout: 0s')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('promise then')
})
foo()
console.log('global end')
复制代码
控制台输出的结果为:
global start
promise
foo
global end
promise then
setTimeout: 0S
复制代码
代码执行的解释:
console.log('global start')
语句,打印出:global start。new Promise(....)
,执行之,Promise参数中的匿名函数同步执行,执行console.log('promise')
打印出:promise。在执行resolve()
以后Promise状态变为resolved,再继续执行then(...)
,遇到then则将其提交给Web API处理,Web API将其添加到微任务队列(注意:此时微任务队列中已有一个Promise事件待处理)。foo()
,执行foo函数,打印出foo。console.log('global end')
,执行后打印出:global end。至此,本轮事件循环已结束,执行栈为空。console.log('promise then')
,打印出:promise then。至此,新的一轮事件循环(Promise事件)已结束,执行栈为空。(注意:此时微任务队列为空)console.log('setTimeout: 0s')
语句,打印出:setTimeout: 0s。至此,新的一轮事件循环(setTimeout事件)已结束,执行栈为空。(注意:此时微任务队列为空,宏任务队列也为空)这个例子比较详细地解释了一遍,一共发生了三次事件循环。同理,仍是用个动图来直观地展现代码执行过程吧!
这里简单介绍下async函数:
async
关键字的做用就2点:①这个函数老是返回一个promise。②容许函数内使用await
关键字。await
使async函数一直等待(执行栈固然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。await
只在async函数函数里面奏效。像上面同样,咱们先单独拎出async函数来看看是怎么样个执行流程吧~
function foo() {
console.log('foo')
}
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('global start')
async1()
foo()
console.log('global end')
复制代码
这里就增长了两个async函数:async一、async2。执行的结果以下:
global start
async1 start
async2
foo
global end
async1 end
复制代码
咱们再来逐条解析一下代码的执行过程吧(前面那些咱们已经懂的就不重复了):
console.log('global start')
,打印出:global start。async1()
,进入到async1函数体内,执行console.log('async1 start')
,打印出:async1 start。接着执行await async2()
,这里await
关键字的做用就是await下面的代码只有当await后面的promise返回结果后才能够执行(此时,微任务队列有一事件,其实就是Promise事件),而await async2()
语句就像执行普通函数同样执行async2()
,进入到async2函数体中;执行console.log('async2')
,打印出:async2。async2函数执行结束弹出执行栈。await
关键字以后的语句已经被暂停,那么async1函数执行结束,弹出执行栈。JS主线程继续向下执行,执行foo()
函数打印出:foo。console.log('global end')
,打印出:global end。该语句以后再无其余需执行的代码,执行栈为空,则本轮事件执行结束。await async2()
语句,async2函数执行完毕后,promise状态变为settled,以后的代码就能够继续执行了(能够这么理解:用一个匿名函数包裹await
语句以后的代码做为一个微任务事件),执行console.log('async1 end')
语句,打印出:async1 end。执行栈又为空,本轮事件也执行结束。至此,单一事件类型咱们都掌握了,下面咱们综合演练一下!
这里来几道常见的题目来考察本身的掌握程度以及进一步巩固吧!这里再也不逐步分析了,有困惑的能够留言再解答。
//请写出输出内容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
复制代码
输出结果:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
复制代码
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2作出以下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
复制代码
输出的结果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
复制代码
async function async1() {
console.log('async1 start');
await async2();
//更改以下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改以下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
复制代码
输出的结果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
复制代码
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
复制代码
输出的结果:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
复制代码
怎么样,都作对了吗?其实就是将这几个异步事件糅合在一块儿罢了,只要咱们分别掌握了它们的执行过程,一步步拆开分析,一点都不难,这都是纸老虎而已!
呼~长吁一口气,相信看到这的你已经疲惫了,不过恭喜:你应该彻底掌握了事件循环以及异步执行机制了吧!最后,让咱们再总结一下本文涉及的要点吧!
JS是单线程执行的,同一时间只能处理一件事。可是浏览器是有多个线程的,JS引擎经过分发这些耗时的异步事件(AJAX请求、DOM操做等)给Wep APIs线程处理,所以避免了单线程被耗时的异步事件阻塞的问题。
Web APIs线程会将接收到的全部事件中已完成的事件根据类别分别将它们添加到相应的任务队列中。其中任务队列分如下两种:
task queue
,也即本文图中的callback queue
,macrotask是咱们给它的别名,缘由只是为了与ES6新增的microtask队列做区分而这样称呼,HTML标准中并无macrotask这种说法。它存放的是DOM事件、AJAX事件、setTimeout事件等。事件循环(event loop) 机制是为了协调事件(events)、用户交互(user interaction)、JS脚本(scripts)、页面渲染(rendering)、网络请求(networking)等等事件的有序执行而设置(定义由HTML标准给出,实现方式是靠各个浏览器厂商本身实现)。事件循环的过程以下:
打个比方帮助理解:宏任务事件就像是普通用户,而微任务事件就像是VIP用户,执行栈要先把全部在等待的VIP用户服务好了之后才能给在等待的普通用户服务,并且每次服务完一个普通用户之后都要先看看有没有VIP用户在等待,如有,则VIP用户优先(PS:人民币玩家真的能够随心所欲,hah...)。固然,执行栈正在给一个普通用户服务的时候,这时即便来了VIP用户,他也是须要等待执行栈服务完该普通用户后才能轮到他。
setTimeout设置的时间其实只是最小延迟时间,并非确切的等待时间。实际上最小延时 >=4ms,小于4ms的会被当作4ms。
promise 对象是由关键字 new 及Promise构造函数来建立的。该构造函数会把一个叫作“处理器函数”(executor function)的函数做为它的参数(即 new Promise(...)
中的...
的内容)。这个“处理器函数”是在promise建立时是自动执行的,.then
以后的内容才是异步内容,会交给Web APIs处理,而后被添加到微任务队列。
async/await:async函数实际上是Generator函数的语法糖(解释一下“语法糖”:就是添加标准之外的语法以方便开发人员使用,本质上仍是基于已有标准提供的语法进行封装来实现的),async function 声明用于定义一个返回 AsyncFunction 对象的异步函数。执行async函数时,遇到await
关键字时,await 语句产生一个promise,await 语句以后的代码被暂停执行,等promise有结果(状态变为settled)之后再接着执行。
呼~ 结束了,休息一下~
参考文献: