推荐提前阅读文章: 事件循环规范
JavaScript是一门单线程的语言。单线程是指JavaScript在运行阶段(注意,是在运行阶段
)一直在单个栈中执行。
这个栈被称为
JavaScript栈
,因为闭包的存在,该栈和OS栈
不能合并。JS栈中栈帧和OS的栈帧有很大的差异,JS的栈帧保存的是引用,引用的对象中包含变量,OS栈的栈帧中储存的是变量,所以合并起来比较困难。
多个线程同时操作DOM会产生并发,无疑会增加操作DOM的难度。
虽然单个线程在运行JavaScript,但是JavaScript是一门非阻塞的语言。
之所以设计为非阻塞的语言,是因为JavaScript在设计的时候为了适应用户在不同环境下和浏览器交互。
例如:用户上传文件,读取文件等。非阻塞是指JavaScript遇到阻塞调用的时候不会等待结果的返回,立刻执行栈中的下一步。
JavaScript还可以执行异步操作。
非阻塞和异步是截然不同的两个概念:非阻塞针对单线程,异步针对多线程。
接下来我们通过一个Demo来描述非阻塞和异步之间的关系。
setTimeout(() => console.log('a'));
console.log('b')
上例中首先输出b,然后输出a。
过程描述:
除了setTimeout函数JavaScript还有很多异步API,例如:readFile、XMLHTTPRequest等。这些API是由JavaSript平台提供的。我们平时写的promise等异步函数归根结底也要依靠平台提供的异步API实现异步操作。用户不能自定义异步API。
JavaScript代码的运行一直是在JavaScript栈中进行的,该栈是单线程栈。
异步操作执行完毕之后,平台会在事件队列中添加一个任务,每个线程都有自己的队列,异步操作的结果通过队列发回主线程。任务中有关于调用的元信息,还有主线程中回调函数的引用。
主线程的调用堆栈清空后,平台将检查事件队列中有没有待处理的任务。如果有等待处理的任务,平台将着手处理,触发一个函数调用,把控制权返回给主线程中的那个函数。调用那个函数之后,如果主线程中的栈又变空了,平台再次检查事件队列中有没有可以处理的任务,这个循环会一直运转下去,直到调用堆栈和事件队列都为空,而且所有原生的异步API调用都已结束。
非阻塞是在单线程中而言,异步是在多线程中而言。异步+非阻塞机制帮助JavaScript完成高并发。
首先推荐大家阅读这篇文章,了解重绘,重排,合成。
规范中,EventLoop过程中包括渲染事件,详情参考本文,下面展开详细描述。
一般在浏览器中rendering被划分为以下几个阶段:
参考事件循环规范理解在EventLoop中什么是task。
下文是根据事件循环规范和NodeJS事件循环给出的浏览器事件循环机制的猜测。一定不会百分百正确。
前端工程师在编写网页的时候通过标签将代码包括起来,代码一般编辑如下:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>body>
html>
当需要与网页互动的时候,我们需要引入标签加入JavaScript代码。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>body>
<script>script>
html>
我们将写好的代码保存起来整理成****.html
文件,双击打开,在浏览器中就能查看我们写出的效果。
1、浏览器加载html文件过程中有一个主线程Main负责解析运行html文件。所以需要首先准备好解析器、JavaScript执行入口(可以类比Java的Call_Stub的实现)等前提工作。
// test1.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>body>
<script>
function fn() {
for (var i = 0; i < 1000000; i++) {
i = i + 1;
}
}
fn();
script>
html>
test1.html
运行过程图示:
这里的每一个task代表一轮Event Loop。
渲染线程和JavaScript执行线程同属于一个线程。
如上图所示:解析HTML和执行自定义JavaScript函数fn在同一个Task中,首先进行的解析HTML,生成DOM树,遇到节点会将对该节点特殊处理。开始Evaluate Script阶段。
如上图所示:浏览器会前我们编写的代码进行解析,也就是上面所说的Parse HTML阶段,当Parse HTML遇到script对应的Node节点时,找到浏览器厂商DOM类中对应的Native Code,开始Evaluate Script过程。执行过程中中断Parse HTML(如果使用async等关键字则不会打断Parse HTML的过程),Evaluate Script结束以后,还会继续解析没有解析的HTML标签。
执行JavaScript的过程可以类比执行Java的过程,C语言通过函数指针可以直接对内存操作,call_Stub函数是Java代码和C代码(JVM虚拟机)之间的桥梁,所以这里我的猜想是V8通过函数指针实现了对JavaScript代码的执行。这里的类比不一定正确,但是这种类比能想通V8是怎么执行JavaScript的,就不会对Parse HTML(无论该阶段是C语言编写或者是JavaScript编写)转化到Evaluate Script感到不可思议了。
现在流行的SPA应用,也是HTML标签和JavaScript语法的应用,组件式编程原理是通过Class或者Function等JavaScript代码来修改DOM。JSX则是HTML标签和JavaScript的语法糖,原理还是通过Function/Class的实例来修改DOM。
仔细观察会发现函数fn是由函数anonymous调用的。这里anonymous是谁呢?
事实上,chrome浏览器会把我们在单个