【JS背后】JavaScript 运行机制:事件循环Event Loop

JS背后的运行原理是很多JS的书上都没有提及的,因此一直似懂非懂,今天专门搜了很多相关的博客来看,在这里总结记录一下。

本文主要讲解JS的线程和事件循环问题,捎带提一下浏览器线程:

  • 一、JavaScript的单线程
    • 为什么JavaScript是单线程?
    • 任务队列
    • 回调函数
    • 事件循环Event Loop
    • 定时器
  • 二、Node.js的Event Loop
  • 三、浏览器线程
  • 参考资料

一、JavaScript的单线程

1. 为什么JavaScript是单线程?

从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

  • 单线程:同一时间只能做一件事
  • JS主要用途:与用户互动、操作DOM

若JS是多线程的,会带来很复杂的同步问题

假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

HTML5提出Web Worker标准:允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。

为了利用多核CPU的计算能力,并没有改变JavaScript单线程的本质。

异步执行的运行机制:(同步执行可以被视为没有异步任务的异步执行)

  1. 同步任务进入主线程执行,形成一个”执行栈”(execution context stack);
  2. 当异步任务有了运行结果,异步任务的回调函数进入”任务队列”等待;
  3. “执行栈”中所有同步任务执行完毕,读取”任务队列”中的事件,其中的异步任务结束等待,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

总结:只要主线程空了,就会去读取”任务队列”

【只要主线程执行栈空了,”任务队列”上第一位的事件就自动进入主线程】

2. 任务队列

主线程之外,还存在一个”任务队列”。

  • 主线程:同步任务
  • 任务队列:异步任务(有回调函数的事件)
    • IO设备事件
    • 用户事件(比如鼠标点击、页面滚动等等)

任务分为两种:

  • 同步任务(synchronous):进入主线程,排队执行,前一个任务执行完毕,才能执行后一个任务;
  • 异步任务(asynchronous):不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

3. 回调函数

“回调函数”(callback):被主线程挂起来的代码。

异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

4. 事件循环Event Loop

事件循环Event Loop:主线程从”任务队列”中读取事件,这个过程循环不断

执行栈中的代码(同步任务),总是在读取”任务队列”(异步任务)之前执行。

主线程运行的时候,产生堆(heap)和栈(stack)

  • 执行栈中的代码调用各种外部API,产生各种异步任务(click,load,done)加入”任务队列”;
  • 只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。
1
2
3
4
5
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

req.send方法:Ajax操作,向服务器发送数据 —— 异步任务

只有当前脚本的所有代码执行完,系统才会去读取”任务队列”。所以,它与下面的写法等价。

1
2
3
4
5
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};

也就是说,指定回调函数的部分(onload和onerror)属于执行栈的一部分,系统总是执行完它们,才会去读取”任务队列”,因此在send()方法的前面或后面无关紧要。

举例:定时器

两种定时器的内部运行机制完全一样,区别:

  • setTimeout():只执行一次
  • setInterval():反复执行

HTML5标准规定:setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。
对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

定时器只是将回调函数插入了”任务队列”,必须等到”执行栈”里的任务执行完,主线程才会去执行回调函数。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。
在”任务队列”的尾部添加一个事件,等到”执行栈”和”任务队列”现有的事件都处理完,才会得到执行。

1
2
3
4
5
6
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
// 1
// 3
// 2
1
2
3
4
5
6
console.log(1);
setTimeout(function(){console.log(2);},0); // 当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
console.log(3);
// 1
// 3
// 2

二、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

Node.js的运行机制:

  1. V8引擎解析JavaScript脚本。
  2. 解析后的代码,调用Node API。
  3. libuv库负责Node API的执行。将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  4. V8引擎再将结果返回给用户。

除了setTimeout和setInterval,Node.js还提供了另外两个与”任务队列”有关的方法:

  • process.nextTick:当前”执行栈”的尾部插入回调函数【回调函数在本次”事件循环”触发(所有异步任务之前)】
  • setImmediate:当前”任务队列”的尾部插入回调函数【回调函数在下次”事件循环”触发】—— 同setTimeout()

很显然,process.nextTick总是比setImmediate发生得早,而且执行效率也高(因为不用检查”任务队列”)。

多个process.nextTick语句总是在当前”执行栈”一次执行完,多个setImmediate可能则需要多次loop才能执行完

如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前”执行栈”执行:

1
2
3
4
5
6
7
8
9
10
11
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

换做setImmediate则运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2:

1
2
3
4
5
6
7
8
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);

Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2

2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

三、浏览器线程

浏览器中的js任务:

  • 执行JavaScript代码
  • 对用户的输入(包含鼠标点击、键盘输入等等)做出反应
  • 处理异步的网络请求

js是单线程的,在浏览器中,js始终在一个线程上执行,这个线程称为js引擎线程。
js引擎有多个线程,一个主线程,其它的后台配合主线程。

浏览器是多线程的:

  • UI渲染线程:渲染页面
  • js引擎线程:执行js任务
  • 浏览器事件触发线程:控制交互,响应用户
  • http请求线程:处理请求,ajax是委托给浏览器新开一个http线程
  • EventLoop轮询的处理线程:轮询消息队列

多线程之间会共享运行资源,js可以操作dom,影响渲染,所以 js引擎线程和UI线程是互斥的 。这也就解释了js执行时会阻塞页面的渲染。

参考资料

您的支持将鼓励我继续创作!
------本文结束感谢阅读------