一、引言
众所周知,JS是一门单线程,异步非阻塞的编程语言。那单线程是如何做到异步的呢?
二、单线程和异步
在JS中,只有一个线程处理JS任务,这也意味着所有任务只能同步执行。如果遇到网络请求,那就必须等待请求结果返回,JS才能继续往下执行。这明显是不合情理的。所以浏览器引入了Event Loop
机制来帮助处理这种长时间挂起的任务。
三、浏览器事件循环模型
本篇文章不涉及浏览器底层真实运行环境,只对浏览器的事件循环模型进行抽象,感性理解:
概念理解
把JS引擎想象成美国总统,他的职务是给国会通过的法案签名(执行任务),形成法律。由于他不能找人代签(单线程),所以法案(task)在他的办工作上(task queue)堆积如山。他一般会先抽出压在最下面的法案(按照队列顺序),签完之后在再从最底下抽出来开签,但是秘书和总统的关系比较好,很多重要的事情(microtask)都是由秘书交给总统处理,秘书只要看见总统手头签完名(调用栈为空),就直接把手上所有的法案交给总统,不用排队。总统有个习惯,每16.6ms(一般的屏幕渲染周期)他要喝一口伏特加(渲染),如果不喝,他就会暴躁(阻塞渲染),所以他手速非常快,一般能在几毫秒之内给所有法案签名,剩下的时间就能翘着二郎腿。
段子写完了,回到JS~
- 调用栈
(call stack)
: JS引擎唯一工作线程,用于函数调用执行,所有的任务都要推入调用栈才能执行; WebApis
: 浏览器提供的事件,如:DOM操作,AJAX请求,定时器等等- 任务队列
(task queue)
: 事件循环将完成的webApi事件按顺序排队形成队列,在调用栈为空时,队首的回调函数推入调用栈执行 - 微任务队列
(microtask queue)
: 每次当一个任务执行完成且调用栈为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。
事件循环流程 (未包含渲染)
- 执行当前调用栈中的
task
, - 执行
microtask queue
中所有microtask
,即使是中途加入的microtask
,直到microtask queue
为空; - 执行
task queue
中队首的task
;如果调用栈为空,执行2;
举例说明
<script>
function task1() {
console.log('task1')
task2()
return
}
function task2() {
console.log('task2')
task3()
return
}
function task3() {
console.log('task3')
return
}
document.body.addEventListener('click', () => {
setTimeout(function handleClick(){
console.log('handleClick called')
})
})
new Promise((resolve) => {
document.body.addEventListener('click', () => {
resolve('promise')
})
}).then(function success(value) {
console.log(value)
})
task1()
document.body.click()
</script>
- 主函数
main
推入调用栈,代码从上往下执行,函数声明task1,task2,task3
; - 添加
body
的click
事件处理函数handleClick
到WebAPIs,等待事件触发。 - 添加
promise
,当body
的click
事件触发时,调用resolve('promise')
; task1
调用,task1
推入调用栈执行,控制台输出'task1'
;task1
内部调用task2
,task2
推入调用栈执行,控制台输出'task2'
;task2
内部调用task3
,task3
推入调用栈执行,控制台输出'task3'
;task3
执行完毕,返回,弹出调用栈;task2
执行完毕,返回,弹出调用栈;task1
执行完毕,返回,弹出调用栈;document.body.click()
,click
事件触发,监听函数执行;handleClick
推入task queue
等待执行;resolve
调用,success
推入maricotask queue
等待执行;main
执行完毕,返回,弹出调用栈;调用栈清空;- 事件循环机制检查调用栈为空,执行
microtask queue
中的回调函数success
,控制台打印'promise'
; - 检查
microtask queue
为空,执行task queue
中的回调函数handleClick
,控制台打印'handleClick called'
;