一文说清楚JS的事件循环机制

211 阅读4分钟

动机

为什么想写这篇总结?是因为基本大部分公司,包括大厂都喜欢出一个包含了同步任务、异步任务(微任务、宏任务)的代码片段,然后问你按照顺序会输出什么。只有理解了Js的事件循环机制(Event Loop),再遇到多复杂的,都可以对他的执行过程顺手拈来。那么就开始吧。

1、JS的特点:单线程

Js是单线程运行的语言,顾名思义就是,它在同一时间内只能做一件事,那为什么设计者一开始要设计它单线程的特性呢?主要原因还是js的用途决定的吧,浏览器的脚本语言,它主要是要和用户进行互动,然后操作DOM,如果不是单线程的话,就会引发类似后端线程同步的问题,当两个任务操作同一个DOM同时进行的时候,此时就不知道到底哪一个任务生效。 后来HTML5新增了web worker,允许创建多个js线程,但是web worker的子线程是完全受控于主线程,而且子线程不能操作DOM,还是不改JS单线程的特性。

2、同步任务和异步任务(微任务和宏任务)

既然JS是单线程的,那么他是怎么实现异步的呢?

单线程指的是JS引擎中负责解析执行JS代码的线程只有一个即主线程,它每次只能处理一件事,当我们发起一个异步请求的时候,主线程在等待请求响应的时候是可以去做其他的事情的,浏览器先在事件表注册ajax的回调函数,响应回来后回调函数被添加到任务队列中等待执行,这样不会造成线程阻塞,所以处理请求的时候是异步任务。

JS任务分为同步任务和异步任务,同步任务是直接在主线程上依次排队执行,异步任务会被放到任务队列中进行排队等待,等到调用栈为空的时候,任务队列里的任务会被依次移到调用栈,然后主线程执行调用栈的任务。 事实上,异步任务又包含了:微任务与宏任务。微任务和宏任务虽然都是异步任务,都在任务队列中,但是他们在两个不同的队列中。微任务的优先级大于宏任务,同时存在时,先执行微任务。

(1)微任务:Promise.then、MutationObserver、process.nextTick(Node.js) (2)宏任务:setTimeout、setInterval、I/O、事件、postMessage、setImmediate(Node.js中的特性,浏览器已废弃该API)、requestAnimationFrame、UI渲染

3、调用栈和任务队列

调用栈是一个栈结构,函数调用会形成一个栈帧,帧中包含了当前执行函数的参数和局部变量等上下文信息,函数执行完后,它的执行上下文会从栈中弹出。

任务队列"实质上是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。具体可以去看Philip Roberts的演讲《Help, I'm stuck in an event-loop》网址:vimeo.com/96425312 只有二十分钟,相信会有更清晰的理解。

image.png 如上图所示,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

4、事件循环

一次完整的事件循环步骤是:异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。

5、题目测试

下面拿一道某厂出过的题来检验下学习效果, 以下代码依次输出什么?

setTimeout(()=>{
   console.log(1);
},20);

for(let i=0;i<100000000;i++){console.log("循环")};
console.log(2);
setTimeout(()=>{
   console.log(3);
},0);

Promise.resolve().then(function(){
   console.log(4);
});
console.log(5);