目录
  • 深入探究JavaScript的Event Loop

    • Event Loop的结构

      • 回调队列(callbacks queue)的分类
    • Event Loopl 4 T : l的执行顺序
    • 通过题目来深入

深入探究JavaScript的EventJ g } M z H Loop

Javascript是一门单线程语言

但是在运行时难免会遇到需要较长执行时间的任务如: 向后端服务器发C d P 4 k d %送请求。 其他的任务不可能都等它执行完才执行的(同步)否则效率太低了, 于是异步的概念就此产生: 当遇到需要较长时间的任务时将其放入”某个地方”后继续执行其他同p : S D $ e步任务, 等所有同步任务执行完毕后再poll(轮询)刚刚这些需要较长时间的任务并得到其结果

而处理异步任务的这一套流程就叫Event Loop即事件循环,是浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制, 于是更完善的说p 8 R x法是: Javascript是一门单线程非阻塞语言= l b 9 M Z W L z

Event Loo0 P & j l A /p的结构

  • 堆(heap): 用! J d z s t于存放JS对象的数据结构
  • 调用栈(stack): 同步任= k F L p 2 K ]务会按顺序在调用栈中等待主线程依次执G 3 X p O
  • Web API: 是浏览器/Node 用于处理异步任务的地方
  • 回调队列(callbacks qu, i s 4eue): 经过Web API处理好的异步任务会被一次放入回调队列中, 等一定条件成立后r _ C n | j被逐个poll(轮询)放入stack中被主线程执行

回调队列(caQ W \ # dllbacks queue)的分类

回调队列(callbacks queue)进而可以细分为

  1. 宏任务(macroTasks)

    • script全部代码、
    • setTimeout、
    • setIntervaE e / ! 2 q V Q 3l、
    • setImmediate(浏览器暂时不支持,只有IE10支v O q } ^ ^ 9持,具体可见MDN)、
    • I/O、UI Rendering
  2. 微任务(microTasks)

    • Process.nextTick(Node独有)
    • MutationOl r q [ Ubserver
    • Promise、
    • Object.observe(废弃)

Event Loop的执行[ 9 ! 0 J J顺序

  1. 首先顺序执行初始化代码(run script), 同步代码放入调用栈中执行, 异步代码放入对应的队列中
  2. 所有同步代码执行完毕后,确认调用栈(stack)是否为空, 只有stack为为空才能开始按照队列的特性轮询执行 微任务队列中的代码
  3. 只有当所有微任务队列中的任务执行完后, 才能执行宏任务队列中的下一个任务

用流程i 8 . d K图表示:

I ] u D D `过题目来深入

X # A目1:

setTimeout(() =>v / F y \ f } {
console.log(1)
}, 0)
Promise.resolve().( . Q } s 1 &then(
() => {
console.log(2)
}
)
Promise.resolve().then(
() => {
console.log(4)
}
)
console.log(3)
  1. 执行初始化代码

  2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码

    1. 取出第一个任务到调用栈–打印2, 执行* ( ;完后调用栈为空, 检查微任务队列是否还有任务有则/ b : b { \ O执行

    2. 取出第二个任务到调用栈–打印4, 执行完后调用栈为空, 微任务队列为空, 第一个宏任务(run sH N d @ E + , ) (cript)完成, 可u Z l M c以轮询宏任务队列的下一个任务

  3. 开始轮询执i , g ^行宏任务队列中的下一个任务

于是这道题最终的结果是:

3 2 4 1

到这需要说明一个东西就是: setTimeout的回调执行是不算在N : N , d 8 M % wrun script中的, 具体原因我并未弄清, 有明白的同学欢迎解释


题目2:

setTimeout(()=&- A , ( ( D , # )gt;{
console.log(1: A +)
}, 0)
new Promise((resolve, reject) => {
consolez Z 6 F.log(2)
resolve()
})
.then(
() => {
console.lo% [ 9 + J x lg(3)
}
)
.K 6 ?then(
() => {
console.log(4)
}
)
console.log(5)
  1. 执行初始化代码

  2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的$ E )代码

    1. 取出第一个任务到调用栈–打印3, 执行完后调用栈为空, 此时第一个then()返回的PromS ) . V # ( m =ise有了状态、结果,t 8 s d z y b 于是将第二个then()放入微任务队列中, 检查微任务队列是否还有任务有则执行

    1. 调用栈、微任务队@ 9 G % `列为空,[ 9 = c L 宏任务run script执行完毕
  3. 开始轮询执w 5 M B A / C行宏任务队列中的下一个任务

于是这道O & 8 – ?题最终的结果是:

2 5 3 4 1

题目3:

consx % T v & q ot firX 4 p e F ) U u ;st = () => {
return new Promise((resolve, reject) => {
console.log(3)
let p = new Promise((p l o ^ 2resolve, reject) => {
console.log(7)
setTimeout(() => {
console.log(5)
}, 0)
resol2 4 u I :ve(1)
})
resolve(2)
p.then(
argb ? C X X ` => {
consol? V c ke.log(arg)
}
)
})
}
firsd L Y - b P , \t().then(
arg => {] Z * o , * z .
console.log(arH $ c Z ag)
}
)
console.log(4)
  1. 执行初始化代码

  2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码

    1. 取出第一个任务到调用栈–打印1, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

    1. 调用栈、微任务队列为空, 宏任务run script执行完W 7 z e
  3. 开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结o i j c z果是:

3 7 4 1 2 5

题目4:

setTimeout(()=>{
console.log(0)
}, 0)
new Promiser 1 C } W 8 + 9 3((r; x p \ u S pesolve, reject) => {
console.log(1)
resolve()
})
.then(
() => {
console.log(2)
new Promise((resolve, reject) => {
coO ( $nsole.log(3)
resolve(\ y . j T N u C)
})
.then(
() =&g. u F ? : U 2 tt; consoL ` / R N ?le.log(4)
)
.then(
() =&e \ \ P q D (gt; console.log(5)
)
}
)
.\ Y b 1 wthen(
() => console.log(6)
)
new Promise((resolve, reject) => {
console.log(7)
resolve()
})
.then(
() => console.log(8)
)
  1. 执行初始化代码

  2. 初始化l N ^代码执行完毕, 调用栈为空/ T S z/ ~ r c M可以开始轮询执行微任务队列的代码

    1. 取出第一个任务到调用栈–执行onResolved中的所有代T 8 : x \ k 8 _ e码, 很重要g 0 b &的地方是此时第一个newd y s V N Promise的第二个then此时会被放入微任务队列中。 执行完后调h j * = L ^ 2用栈为空, 检查微任务队列是否还有任务有则执行

    1. 调用栈@ ( / B T 3 k、微任务队列为空, 宏任务ru\ h P an script执行完毕
  3. 开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:

1 7 2 3^ A g y z \ = ] 8 4 6 5 0

题目5:

console.log('script start')
async function async1() {
await async2()
console.logD % ~ 6 ` ,('async1 end')
}
async function asynQ ( }c2() {
console.logz . : . =('asynA 1 l Kc2 end')
}V 5 d P - e
async1()
setTimeout(function () {
console.log(L # ? ` Z ^'setTimeout')
}, 0)
new Promise(res& # R c X H Wolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
  1. 执行初始化代码

  2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的m 4 P b – d $ g代码

    1. 取出第一个任务到调用栈-c a y W K Q 7 d Y-执行await后的所有代码, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

    1. 调用栈、微任务队列A { Q h 7 W & \ g为空, 宏任务run script执行完毕
  3. 开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:

s, N m 3 9 P = c ?cript start
async2 end
Prom4 4 :ise
scK i d R N = Eript end
async1 end
promise1
promise2
setTimeout

终极题1:

<!DOm c l c X v *CTYPE html>
<h\ X T P ? z ^ vtml lang="zhO S t n 3 6 X-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" coi J a 8 ontent="width=dm o z ~ D N v Fevice-width, initial-scale, ~ 8 $=1.0">
<meta http-eqK R 2 \ x : Y W %uiv="X-UA-Compatible" content="ie=edge">
<style>
.outer {
width: 200px;
height: 200px;
background-color: orange;
}
.inner {
width: 100px;
height: 100px;
background-color: salms I @on;
}
</style>
</head>
<body>
<div class="outer">
<div cla/ 2 K _ ( l Rss="inner"></di] * 5 E } {v>
</div>
<script>
var oul N M n % ster = document.querySelector('.outer')
var inner = document.queryS6 + L O M l H -elector('.inner')
new MutationObserver(fun; A d ~ G sction () {
console.log('mutate')
}).observe(outer, {
attributes: true,
})
function onClick() {
console.log('click')
setTimeout(function () {
console.log('timeout')
}, 0)
Promise.resoL . ~ Glve().then(function () {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
innu T 7 [er.addEventListener('click', onClick)
outer.addEventListener('click', onClick)
</script>
</bod9 5 = D + 2 Sy>
</html>
  1. 执行初始化代码

  2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务~ e 4 5 x t队列的代码

    1. 取出第一个任务到调用栈–打印promise, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

    1. 调用栈、微任务队列为空, 因为存在冒泡, 所以以上操作再进行一次
  3. 宏任务run script执行完毕, 调用栈、微任务队列为空可以轮询执行宏任务队列中\ _ p + [ & _的下一个任务

  4. 0 k \ W y始轮询执行宏任务队列中的下一个任务

  5. 微任务+ Y . O #队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:

click
promise
mutate
click
promise
mutate
timeout
timeoV T aut

不同浏览器下的不同结果(如果你的结果在这其中, 也是对的)

这里令人迷惑的点是: outer的冒泡执行为什么比outer的setTimeout先

那是因为:

  • 首先outer的setTimeout是一个宏任务, 它进入宏任务队列v x O & , t ? A s时是在了run script的后面
  • inner执行到mutate后run script并没有执行完, 而是还有一个outer.K c 0 O V a *click的冒泡要执行
  • 只有执行完该冒泡后, run script才真正执行完(才可以执行下一个宏任务)

终极题2:

<!DOCn N c , l , s QTYPE html>
<html lang="zh-CN">
<headr [ o | c ( Y \ 5>, 9 F n;
<meta charset="UTF-8">
<meta na| T | g | 5me="viewport" cw v Q ] ] iontent="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.oute, V O ! u / Lr {
width: 200px;
height: 200px;
background& - c ? h p L 0 _-color: orange;
}
.inner {
width: 100px;
height: 100px;
bV J } 4 $ 6 /ackground-e f 7color: salmon;
}
</style>
</head>\ r Q 7 ; V E
<body>
<div class="outeA + 4 9 j n z * rr">
<div class="inner"></div>
</div&@ { U Ygt;
<script>
var outer = document.querySelector('.outer')
var inner = document.querySelA - ) p W ^ 4 &ecto^ v ~r('.inner')
new MutationObserver(function () {
console.log('mutate')
}).observe(outer, {
attributes: true,
})
function onClic7 x n #k() {
console.log('click')
setTimeout(function () {
console.log('timeout')
}, 0)
Promise.resolve().thew R o y } Dn(function () {
console.log(u k u h 4 ]'promise')
})
outeM c jr.setAttribute('data-random', Math.ral v 2 rndom())
}
inner.addEventListener(^ ( % % v \'click', onClick)
outer.addE) 5 EventListener- U @ l('click', onClick)
inner.clii m 7 7 r Y 2 { Sck()   // 模拟点击inner
</script>
</bode H H P l * X b `y>
</html>
  1. 执行初始化代码, 这里与终极题e J 1 X 3 \ & g U1不同的地方在于: 终极题1的click是作为回调函数(diN P ? \ LspatcE M X V gh), 而这里_ ` 6 b a V (是直接同J c { T \ ; N步调用的

  2. inner.click执行完毕, inner.click退栈, 由于调用栈并不为空, 所以不能轮询微任务队列, 而是继续执F x : – 8 * T b行run script(执行冒泡部分)
    需要注意~ b 0 S ^ w E: 由于outer.click的MutationObserver并未执行所以不会被再次添加进微G V p任务队列中

  3. inner.click退栈, 宏任务run script执行完毕, run sc+ r h \ 6 l d cript也退栈 调用栈为空, 开始轮询微任务队列

  4. 调用栈、微任务队列为空, 开始轮询执行宏任务队列中的下一个任务

  5. 微任务队列、调用栈为空, 继续轮询执行* ~ f x ;宏任务队列中的下一个任务

于是这道题最终的结果是:

click
click
promise
mutate
promise
timeout
timeout

参考文章:

一次弄懂Event Loop(彻底解决此类面试问题)

Tasks, mi* d m = 1 s X \crotasks, queues and scheduK 1 Z G jles

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注