JavaScript 之事件循环 (Event Loop)
导读:学过 JavaScript(下文简称 JS) 的都知道它是一门单线程的、非阻塞的脚本语言。单线程意味着,JS 代码在执行的任何时候,都只有一个主线程来处理所有的任务,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念,我们如何理解呢?理解异步和非阻塞靠的就是 Event Loop(事件循环),本文就围绕 JS 线程、同步异步、任务队列等方面讲解事件循环(Event Loop)。
JS 线程
为了我们更方便容易了解事件循环,在此之前我们先简单了解下什么叫做 JS 线程。如浏览器的渲染进程是多线程的,主要有以下几个线程:
JS 引擎线程(主线程):负责解析 JS 脚本,运行代码。
GUI 渲染线程:负责渲染浏览器界面,解析 HTML、CSS、构 DOM 树和 RenderObject 树,布局和绘制等,当界面需要重绘(Repaint)或由于某种操作引发回流 (reflow) 时,该线程就会执行。
定时器触发线程 (setTimeout):浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此通过单独线程来计时并触发定时,在计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行。
http 请求线程(ajax):XMLHttpRequest 连接后,通过浏览器新开一个线程请求,当检测到状态变更时,如果同时设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由 JS 引擎执行。
浏览器事件触发线程 (onclick):归属于浏览器而不是 JS 引擎,用来控制事件循环,可以这么理解:JS 引擎自己都忙不过来,需要浏览器另开线程协助。
主线程和渲染线程互斥 :JS 引擎线程与 GUI 渲染线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
浏览器内核
EventLoop 轮询处理线程:我们可以把它理解为一个中介,在主线程、异步线程与消息队列三者之间进行交流与沟通。如下图所示:从主线程那里顺时针的看,整个的流程是循环往复的。只有当主线程的同步代码都执行完了,才会去队列里看看还有什么要执行的。
主线程把 setTimeout、ajax、dom.onclick 分别给三个线程,他们之间有些不同。
1、对于 setTimeout 代码,定时器触发线程在接收到代码时就开始计时,时间到了将回调函数扔进消息队列。
2、对于 ajax 代码,http 异步线程立即发起 http 请求,请求成功后将回调函数扔进消息队列。
3、对于 dom.onclick,浏览器事件线程会先监听 dom,直到 dom 被点击了,才将回调函数扔进消息队列。
同步与异步
JS 分为同步任务和异步任务:
同步任务:立即执行的任务队列,比如一个简单的函数;
异步任务:请求接口发送 ajax,发送 promise,或时间计时器等等;
任务队列(Event Queue)
什么是任务队列呢?可以理解为一个静态的队列存储结构,遵循先进先出原则:同步任务会立刻执行,进入到主线程当中;异步任务会被放到任务队列(Event Queue)当中。
宏任务队列和微任务队列
宏任务(MacroTask):整体代码 Script、UI 渲染、setTimeout、setInterval、setImmediate(Node.js 环境)。
微任务(MicroTask):Promise.then()、catch、finally。
不同点:event loop 里 MacroTask 队列可能有多个,MicroTask 队列只有一个。
MicroTask 优先于 MacroTask 执行,所以如果有需要优先执行的逻辑,放入 MicroTask 队列会比 MacroTask 更早的被执行。
下面几个代码例子可以让我们充分的了解各个任务之间的执行顺序:
执行栈
MacroTask 和 MicroTask 都是推入栈中执行的。JS 是单线程,也就是说只有一个主线程,主线程有一个栈,每一个函数执行的时候,都会生成新的执行上下文,执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中,正在执行的上下文始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。
总结
同步和异步任务分别进入不同的执行环境, 先执行同步任务,把异步任务放入循环队列当中,等待同步任务执行完,再执行队列中的异步任务。异步任务先执行微观任务,再执行宏观任务。一直这样循环,反复执行,就是我们说的 Event Loop (事件循环)。
事件循环是 JS 这门语言中非常重要且基础的概念。让我们可以清楚的了解事件循环的执行顺序和每一个阶段的特点,可以使我们对一段异步代码的执行顺序有一个清晰的认识,从而减少代码运行的不确定性。合理的使用各种延迟事件的方法,有助于代码更好的按照其优先级去执行。
如果在阅读期间您发现了文章中的一些问题,欢迎在留言中提出,感谢您阅读此文章。
作者介绍
倪萌,网易云信 web 前端开发工程师,目前在从事云信金融线业务相关开发工作。
评论