本文主要参阅了以下两篇文章,对JS的Event Loop运行机制基础知识进行了整理。
背景知识
进程与线程
大家都知道JavaScript是单线程的,这就引申出一个问题,进程与线程是什么,他们的区别是什么?
先给出进程和线程的定义:- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
用工厂和工人的例子来形象阐述:
- 进程是一个工厂,工厂有它的独立资源 -> 系统分配的内存(独立的一块内存)- 工厂之间相互独立 -> 进程之间相互独立- 线程是工厂中的工人,多个工人协作完成任务 -> 多个线程在进程中协作完成任务- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成- 工人之间共享工厂的资源 -> 同一进程下的各个线程之间共享进程的内存空间(包括代码段、数据集、堆等)
补充:
- 我们所说的单线程和多线程,是指一个进程内是单一线程还是多线程。
- 进程间的通信方式包括: 管道pipe、 命名管道FIFO、消息队列MessageQueue、共享存储SharedMemory、信号量Semaphore、套接字Socket、信号。
浏览器是多进程的
关于浏览器进程问题可以简单基础三点:
- 浏览器是多进程的。
- 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)。
- 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
平时 coding 接触到最多的一个浏览器进程是浏览器渲染进程(浏览器内核),它管理着页面渲染。脚本执行,事件处理等。要同时处理这么多事情,渲染进程显然是多线程的,它主要包括以下5个常驻线程:
- GUI渲染线程,负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- JS引擎线程,也称为JS内核,负责处理Javascript脚本程序,(例如V8引擎)。
- 事件触发线程,用来控制事件循环(可以理解为,JS引擎线程自己都忙不过来,需要浏览器另开线程协助)。
- 定时触发器线程,浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确),JS中常用的
setInterval
和setTimeout
就归这个线程管理。 - 异步http请求线程,也就是
ajax
发出http请求后,接收响应、检测状态变更等都是这个线程管理的。
我们常说的JavaScript是单线程的,其实就是说的JS引擎是单线程的,它仅仅是浏览器渲染进程种的一个线程。为什么呢?因为JavaScript的主要作用是与用户互动,以及操作DOM
,如果JavaScript有两个线程,一个线程对一个DOM
节点执行 A 操作,另一个线程这个DOM
节点执行 B 操作,那么就会起冲突,所以JavaScript在前端的应用就注定了它是单线程的。
然而JavaScript的单线程特性就注定我们不用它去完成密集的 cpu 运算,因为密集 cpu 运算耗时过长,阻塞页面渲染。为了解决这个问题,HTML5提出 Web Worker 标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
。
Event Loop
JavaScript 是单线程的带来的问题是:所有任务都必须同步执行,问题就出现了,很多 I/O 过程是非常耗时的(如http 请求数据),如果要等到 I/O 过程结束再执行后续任务,就会出现页面的卡顿、cpu 的闲置。于是异步的任务就出现了,异步任务是指挂起处于等待中的任务,继续执行同步任务,等到结果返回再去继续执行被挂起的任务。于是,JavaScript 的任务可以分为同步任务和异步任务。下面就引出 Event Loop 机制:
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到执行栈中,开始执行。
如上图所示,执行栈中的代码会调用一个异步的API,它们会在任务队列中添加各种事件(或者说回调函数),另外用户的操作如click
、mousedown
等都会在任务队列中添加事件。只要执行栈中的代码执行完毕,主线程就会去读取任务队列,将可执行的回调函数放到执行栈中执行。
总结一下:
执行栈执行完毕 -> 主线程读取任务队列,并执行回调函数 -> 执行栈执行完毕 -> 主线程读取任务队列,并执行回调函数 ...
这个过程一直循环下去,所以就叫事件循环(Event Loop)。
setTimeout 和 setInterval
前面提到了浏览器的定时触发器线程,它的主要作用就是计时,setTimeout
和 setInterval
就由它来控制,原理就是到达设置时间后,往任务队列中添加这两个函数中指定的回调函数。
setTimeout()
方法用于在指定的毫秒数后调用函数或计算表达式。但是需要注意的是,实际是计时结束后定时触发器线程才会将回调函数放到任务队列中去,此时任务队列中这个回调之前可能已经有一些事件待处理,并且一定要执行栈的任务执行完后才会开始执行任务队列中的任务,所以 setTimeout()
中回调开始执行的时间是:执行栈执行时间 + 任务队列前方回调执行时间 + 延迟时间
setInterval()
方法可按照指定的时间间隔来周期性调用函数或计算表达式。它的问题在于:每次都精确的隔一段时间将一个回调放到任务队列中,并没有考虑到内部回调函数执行所需时间,这就会导致两种问题:
- 回调函数执行需要时间,两个函数执行的时间间隔会小于设定值;
- 如果回调函数执行时间大于设定间隔,就会出现上一个加入任务队列中的回调还没执行完,下一个回调就被加入任务队列了,就会出现累计效应,即后面的回调会连续执行。