前言
事件循环可以理解为我们编写的js代码与浏览器或者node之间的一个桥梁
- 浏览器的事件循环是我们编写的js代码与浏览器API调用(setTimeout/AJAX/监听事件等)的一个桥梁,桥梁之间他们通过回调函数进行沟通。
- Node的事件循环是我们编写的js代码与系统调用(file system、network等)之间的一个桥梁,桥梁之间他们通过回调函数进行沟通。
JS是单线程的
JS是单线程的,或者说只有一个主线程,也就是它一次只能执行一段代码。JS中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具备并行任务处理的特性,我们称之为“单线程”。
虽然JS运行在浏览器中是单线程的,但是浏览器是事件驱动的(Event driven),浏览器中很多行为是异步(Asynchronized)的,会创建事件并放入执行队列中。浏览器中很多异步行为都是由浏览器新开一个线程去完成,一个浏览器至少实现三个常驻线程:
- JS引擎线程
- GUI渲染线程
- 事件触发线程
JS引擎
不同浏览器有不同的JS引擎:
浏览器 | JS引擎 |
---|---|
WebKit , Safari浏览器 | ->SquirrelFish Extreme |
Firefox | TraceMonkey引擎 |
Google Chrome | V8引擎,(C++) |
Opera | -> Carakan |
Mozilla | ->SpiderMonkey(C语言) |
Mozilla | Rhino( Java) |
Mozilla | ->JaegerMonkey |
JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,比如最出名的就是Chrome浏览器的V8引擎,如下图所示,JS引擎主要有两个组件构成:
- 堆-内存分配发生的地方
-
栈-函数调用时会形一个个栈帧(frame)
执行栈
每一个函数执行的时候,都会生成新的execution context(执行上下文),执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中, running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。
举个简单的例子:
function bar() {
console.log('bar');
}
function foo() {
console.log('foo');
bar();
}
foo();
执行过程中栈的变化:
event loop(事件循环)
Wikipedia这样定义:
"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"
简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。
事件循环与任务队列
事件循环可以简单描述为:
函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待;
-
当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。
Event Loop是由javascript宿主环境(像浏览器)来实现的;
WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
JavaScript 的并发模型基于"事件循环";
Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数
接下来看一个异步函数执行的例子:
var start=new Date();
setTimeout(function cb(){
console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
- main(Script) 函数入栈,start变量开始初始化
- setTimeout入栈,出栈,丢给WebAPIs,开始定时500ms;
- while循环入栈,开始阻塞1000ms;
- 500ms过后,WebAPIs把cb()放入任务队列,此时while循环还在栈中,cb()等待;
- 又过了500ms,while循环执行完毕从栈中弹出,main()弹出,此时栈为空,Event Loop,cb()进入栈,log()进栈,输出'时间间隔:1003ms',出栈,cb()出栈
宏任务(Macrotasks)和微任务(Microtasks)
“任务队列”不止一个,它分为 Microtasks queue 与 Macrotasks queue,分别存放着 Microtasks 与 Macrotasks:
Microtasks: process.nextTick、promise的then回调、MutationObserver、queueMicrotask
Macrotasks: ajax、setTimeout、setInterval、DOM监听、UI Rendering等
优先级:
- 优先执行微任务队列。
- 在执行任何一个宏任务前(不是队列,是宏任务),都会查看微任务队列中是否有微任务需要执行
- 也就是宏任务执行之前必须保证微任务队列为空
- 如果不空,优先执行微任务队列中的任务