整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

这一次,彻底弄懂 JavaScript 执行机制

文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我。

不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因为javascript是一门单线程语言,所以我们可以得出结论:

  • javascript是按照语句出现的顺序执行的

看到这里读者要打人了:我难道不知道js是一行一行执行的?还用你说?稍安勿躁,正因为js是一行一行执行的,所以我们以为js都是这样的:

let a = '1';
console.log(a);

let b = '2';
console.log(b);

然而实际上js是这样的:

setTimeout(function(){
    console.log('定时器开始啦')
});

new Promise(function(resolve){
    console.log('马上执行for循环啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('执行then函数啦')
});

console.log('代码执行结束');

依照js是按照语句出现的顺序执行这个理念,我自信的写下输出结果:

//"定时器开始啦"
//"马上执行for循环啦"
//"执行then函数啦"
//"代码执行结束"

去chrome上验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?

我们真的要彻底弄明白javascript的执行机制了。

1.关于javascript

javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!

2.javascript事件循环

既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

  • 同步任务
  • 异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本彻底弄懂执行机制,所以我们用导图来说明:

导图要表达的内容用文字来表述的话:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

说了这么多文字,不如直接一段代码更直白:

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');

上面是一段简易的ajax请求代码:

  • ajax进入Event Table,注册回调函数success
  • 执行console.log('代码执行结束')
  • ajax事件完成,回调函数success进入Event Queue。
  • 主线程从Event Queue读取回调函数success并执行。

相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout。

3.又爱又恨的setTimeout

大名鼎鼎的setTimeout无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:

setTimeout(() => {
    console.log('延时3秒');
},3000)

渐渐的setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?

先看一个例子:

setTimeout(() => {
    task();
},3000)
console.log('执行console');

根据前面我们的结论,setTimeout是异步的,应该先执行console.log这个同步任务,所以我们的结论是:

//执行console
//task()
复制代码

去验证一下,结果正确! 然后我们修改一下前面的代码:

setTimeout(() => {
    task()
},3000)

sleep(10000000)

乍一看其实差不多嘛,但我们把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?

这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的:

  • task()进入Event Table并注册,计时开始。
  • 执行sleep函数,很慢,非常慢,计时仍在继续。
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
  • sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?

答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:

//代码1
console.log('先执行这里');
setTimeout(() => {
    console.log('执行啦')
},0);
复制代码
//代码2
console.log('先执行这里');
setTimeout(() => {
    console.log('执行啦')
},3000);  

代码1的输出结果是:

//先执行这里
//执行啦

代码2的输出结果是:

//先执行这里
// ... 3s later
// 执行啦

关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。

4.又恨又爱的setInterval

上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。

5.Promise与process.nextTick(callback)

传统的定时器我们已经研究过了,接着我们探究Promise与process.nextTick(callback)的表现。

Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:

setTimeout(function() {
   console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  • 这段代码作为宏任务,进入主线程。
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
  • 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log(),立即执行。
  • 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束。

事件循环,宏任务,微任务的关系如图所示:

我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
  • 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。

宏任务Event Queue

微任务Event Queue

setTimeout1

process1

setTimeout2

then1

  • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
  • 我们发现了process1和then1两个微任务。
  • 执行process1,输出6。
  • 执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。

宏任务Event Queue

微任务Event Queue

setTimeout2

process2


then2

  • 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
  • 输出3。
  • 输出5。
  • 第二轮事件循环结束,第二轮输出2,4,3,5。
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出9。
  • 将process.nextTick()分发到微任务Event Queue中。记为process3。
  • 直接执行new Promise,输出11。
  • 将then分发到微任务Event Queue中,记为then3。

宏任务Event Queue

微任务Event Queue


process3


then3

  • 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。 (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

6.写在最后

(1)js的异步

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

(2)事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

(3)javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

(4)setImmediate

微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。

(5)最后的最后

  • javascript是一门单线程语言
  • Event Loop是javascript的执行机制

牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!

、前提简介

1.1什么是JavaScript

JavaScript是一种动态的计算机编程语言。它是轻量级的,最常用作网页的一部分,其实现允许客户端脚本与用户交互并创建动态页面。它是一种具有面向对象功能的解释型编程语言。

1.2JavaScript和Java语言的区别

Javascript和Java没有任何关系,它们是不同的两种语言(java是一种程序设计语言,javascript 是客户端的脚本语言),只是名字上都有一个Java而已。

对了,在这里说一下,我目前是在职web前端开发,如果你现在正在学习前端,了解前端,渴望成为一名合格的web前端开发工程师,在入门学习前端的过程当中有遇见任何关于学习方法,学习路线,学习效率等方面的问题,都可以随时关注并私信我:前端,我都会根据大家的问题给出针对性的建议,缺乏基础入门的视频教程也可以直接来找我,我这边有最新的web前端基础精讲视频教程, 还有我做web前端技术这段时间整理的一些学习手册,面试题,开发工具,PDF文档书籍教程,都可以直接分享给大家。


1.3Html、Css和Javascript

这三个要素共同构成了Web开发的基础。

HTML:页面的结构-标题,正文,要包含的任何图像
CSS:控制该页面的外观(这将用于自定义字体,背景颜色等)
JavaScript:不可思议的第三个元素。创建结构(HTML)和美学氛围(CSS)后,JavaScript使您的网站或项目充满活力。

1.4Javascript作用

  1. 表单数据验证:表单数据验证是JavaScript最基本也是最能体现效率的功能。
  2. 动态HTML(即DHTML):动态HTML指不需要服务器介入而动态变化的网页效果,包括动态内容、动态样式、动态布局等。 比如改变盒子的尺寸,背景颜色,图片等。
  3. 用户交互:用户交互指根据用户的不同操作进行的响应处理。例如:联动菜单等。
  4. 数据绑定:HTML中表单和表格能够以.txt文件定义的数据源,通过对位于服务器端的数据源文件的访问,便可以将数据源中的数据传送到客户端,并将这些数据保存在客户端。
  5. 少量数据查找:能够实现在当前网页中进行字符串的查找和替换。
  6. AJAX核心技术:AJAX即异步JavaScript+XML。该对象提供一种支持异步请求的技术,使客户端可以使用JavaScript向服务器提出请求并处理响应,但并不影响用户在客户端的浏览。
  7. Nodejs就是使用的javascript做后端,是目前为止唯一的一个既能做前端、又能做后端的语言。

(上面这个作用是直接用的我的老师的课件,我可没这么6懂这么多。他一个10多年开发经验的资深程序员哈哈哈哈哈哈,有点想帮忙宣传一下他的网课,但想想还是算了吧,感觉打广告有点不好)

*********************************************一条华丽的分割线***************************************************

二、实操代码

2.1Javascript写在本html内

1. js程序必须写在script标签中。
2. script:可以写在网页中的任何位置。
3. type=“text/javascript”:表示当前的语言是javascript语言。这个属性是可以省略的

举例:上代码

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
		
	</head>
	<body>
		<script type="text/javascript">
			alert("出错啦")
		</script>
	</body>
</html>

拿代码去运行一下就知道了

2.2Javascript可以写在单独的文件中(外联方式)

创建一个js文件,在js文件中编写js代码。(外部文件中编写js代码就直接写代码就可以了,不用再添加script标签)

比如说在js目录下面创建一个 test.js文件 里面的代码为alert(“出错啦!”)

举例上代码

a.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		
		<script src="js/test.js" type="text/javascript" charset="UTF-8">
			
		</script>
		
	</body>
</html>

拿代码去运行一下就知道了

2.3实战:点击一个盒子,让另外一个盒子变色

举例上代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style type="text/css">
			#box1{
				width: 100px;
				height: 100px;
				background-color: red;
			}
			
			#box2{
				width: 100px;
				height: 100px;
				background-color: blue;
			}
		</style>
	</head>
	<body>
		<div id="box1">
			
		</div>
		<div id="box2">
			
		</div>
		
		<script type="text/javascript">
			//目标:点击box1时,让box2变颜色
			var b1 = document.getElementById("box1")
			b1.onclick=function(){
				// 当点击b1的时候,执行此处的代码
				document.getElementById("box2").style.backgroundColor="pink"
			}
		</script>
	</body>
</html>

运行效果拿去试试就知道了,点一下第一个小盒子

2.4实战:一个按钮绑定一个事件

  1. 在js中,使用关键字function可以定义一个函数,函数里面的代码不会自动执行,只有函数被调用后,函数里面的代码才会执行。
  2. 可以给网页中的任何html容器标签绑定点击事件。οnclick=“add();” onclick表示点击的时候执行。
  3. js中有两个函数parseInt 将字符串转为数字。 parseFloat():将字符串专为浮点类型。

举例上代码

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
	<input type="text" name="tb1" id="tb1" value="" />+<input type="text" name="tb2" id="tb2" value="" /> =<input type="text" name="tb3" id="tb3" value="" />
	<input type="button" id="btnjisuan" value="计算" onclick="add();" />
	
	<a href="javascript:void(0);"  onclick="bb();">腾讯</a>
	
	
	<script type="text/javascript">
		
		function add()
		{
			
			var v1=document.getElementById("tb1").value;
			var v2=document.getElementById("tb2").value;
			var v3=parseInt(v1) + parseInt(v2);
			document.getElementById("tb3").value=v3;
		}
		
		function bb()
		{
			location.href="http://www.qq.com"; //通过js代码实现页面的跳转 
			
		}
	</script>
	</body>
</html>

拿去运行一个就知道了哈哈哈哈,这个学会了,下面那个就容易多啦!
*********************************************一条华丽的哈哈哈哈哈哈哈哈***************************************************

2.4实战:变换皮肤

实现效果:点击什么颜色代表的小框框,就会弹出穿啥衣服的 fairy


(哈哈哈哈 本人敲爱看这些美丽的事物哈哈哈哈)

自己可以下载一些图片或者颜色渐变图片用来做背景,放在img里面,可自己命名。基本格式如下图:


上代码:

网页换肤.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<link rel="stylesheet" type="text/css" href="css/css2.css" id="btnlink"/>
	</head>
	<body>
		
		<div id="box1">
			<span id="s1" onclick="a1();">志玲</span><span id="s2" onclick="a2();">依林</span><span id="s3" onclick="a3();">昆凌</span>
			
			
		</div>
		
		<script type="text/javascript">
			function a1()
			{
				document.getElementById("btnlink").href="css/css1.css";
			}
			
			function a2()
			{
				document.getElementById("btnlink").href="css/css2.css";
			}
			
			function a3()
			{
				document.getElementById("btnlink").href="css/css3.css";
			}
			
			
			
		</script>
		
	</body>
</html>

css1.css

*{
	margin: 0;
	padding: 0;
}


html,body{
	width:100%;
	height: 100%;
}

body{
	background-image: url(../img/blue.jpg);
	background-repeat: repeat-x;  /* 设置不重复平铺 */
}

#box1{
	width: 186px;
	height: 60px;
	background-color: white;
	margin: 0 auto;
	position: relative;
}
#s1{
	width: 60px;
	height: 60px;
	background-color: blue;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;  /* 子绝父相 */
	left: 0;
	top: 0;
}
#s2{
	width: 60px;
	height: 60px;
	background-color:green;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;
	left: 62px;
	top: 0;
}
#s3{
	width: 60px;
	height: 60px;
	background-color: pink;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;
	right: 0;
	top: 0;
}

css2.css

*{
	margin: 0;
	padding: 0;
}


html,body{
	width:100%;
	height: 100%;
}

body{
	background-image: url(../img/green.jpg)
}

#box1{
	width: 186px;
	height: 60px;
	background-color: white;
	margin: 0 auto;
	position: relative;
}
#s1{
	width: 60px;
	height: 60px;
	background-color: blue;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;  /* 子绝父相 */
	left: 0;
	top: 0;
}
#s2{
	width: 60px;
	height: 60px;
	background-color:green;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;
	left: 62px;
	top: 0;
}
#s3{
	width: 60px;
	height: 60px;
	background-color: pink;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;
	right: 0;
	top: 0;
}

css3.css

*{
	margin: 0;
	padding: 0;
}


html,body{
	width:100%;
	height: 100%;
}

body{
	background-image: url(../img/pink.jpg)
}

#box1{
	width: 186px;
	height: 60px;
	background-color: white;
	margin: 0 auto;
	position: relative;
}
#s1{
	width: 60px;
	height: 60px;
	background-color: blue;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;  /* 子绝父相 */
	left: 0;
	top: 0;
}
#s2{
	width: 60px;
	height: 60px;
	background-color:green;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;
	left: 62px;
	top: 0;
}
#s3{
	width: 60px;
	height: 60px;
	background-color: pink;
	display: inline-block;
	margin: 1px;
	cursor: pointer;
	position: absolute;
	right: 0;
	top: 0;
}

一些很基础的东西,要是写起来那就太多了,很多不常用的,到了我们需要它的时候谷歌和百度就行了。
由于时间关系,暂时更到这里。

原文链接:https://link.zhihu.com/?target=https%3A//blog.csdn.net/hanhanwanghaha/article/details/109188646

作者:我一个超级无敌可爱的人鸭

出处:CSDN

介:本文是一个 V8 编译原理知识的介绍文章,旨在让大家感性的了解 JavaScript 在 V8 中的解析过程。

作者 | 子弈
来源 | 阿里技术公众号

一 简介

本文是一个 V8 编译原理知识的介绍文章,旨在让大家感性的了解 JavaScript 在 V8 中的解析过程。本文主要的撰写流程如下:

  • 解释器和编译器:计算机编译原理的基础知识介绍
  • V8 的编译原理:基于计算机编译原理的知识,了解 V8 对于 JavaScript 的解析流程
  • V8 的运行时表现:结合 V8 的编译原理,实践 V8 在解析流程中的具体运行表现

本文仅代表个人观点,文中若有错误欢迎指正。

二 解释器和编译器

大家可能一直疑惑的问题:JavaScript 是一门解释型语言吗?要了解这个问题,首先需要初步了解什么是解释器和编译器以及它们的特点是什么。

1 解释器

解释器的作用是将某种语言编写的源程序作为输入,将该源程序执行的结果作为输出,例如 Perl、Scheme、APL 等都是使用解释器进行转换执行:

2 编译器

编译器的设计是一个非常庞大和复杂的软件系统设计,在真正设计的时候需要解决两个相对重要的问题:

  • 如何分析不同高级程序语言设计的源程序
  • 如何将源程序的功能等价映射到不同指令系统的目标机器

中间表示(IR)

中间表示(Intermediate Representation,IR)是程序结构的一种表现方式,它会比抽象语法树(Abstract Syntax Tree,AST)更加接近汇编语言或者指令集,同时也会保留源程序中的一些高级信息,具体作用包括:

  • 易于编译器的错误调试,容易识别是 IR 之前的前端还是之后的后端出的问题
  • 可以使得编译器的职责更加分离,源程序的编译更多关注如何转换成 IR,而不是去适配不同的指令集
  • IR 更加接近指令集,从而相对于源码可以更加节省内存空间

优化编译器

IR 本身可以做到多趟迭代从而优化源程序,在每一趟迭代的过程中可以研究代码并记录优化的细节,方便后续的迭代查找并利用这些优化信息,最终可以高效输出更优的目标程序:

优化器可以对 IR 进行一趟或者多趟处理,从而生成更快执行速度或者更小体积的目标程序(例如找到循环中不变的计算并对其进行优化从而减少运算次数),也可能用于产生更少异常或者更低功耗的目标程序。除此之外,前端和后端内部还可以细分为多个处理步骤,具体如下图所示:

3 两者的特性比较

解释器和编译器的具体特性比较如下所示:

需要注意早期的 Web 前端要求页面的启动速度快,因此采用解释执行的方式,但是页面在运行的过程中性能相对较低。为了解决这个问题,需要在运行时对 JavaScript 代码进行优化,因此在 JavaScript 的解析引擎中引入了 JIT 技术。

4 JIT 编译技术

JIT (Just In Time)编译器是一种动态编译技术,相对于传统编译器而言,最大的区别在于编译时和运行时不分离,是一种在运行的过程中对代码进行动态编译的技术。

5 混合动态编译技术

为了解决 JavaScript 在运行时性能较慢的问题,可以通过引入 JIT 技术,并采用混合动态编译的方式来提升 JavaScript 的运行性能,具体思路如下所示:

采用上述编译框架后,可以使得 JavaScript 语言:

  • 启动速度快:在 JavaScript 启动的时候采用解释执行的方式运行,利用了解释器启动速度快的特性
  • 运行性能高:在 JavaScript 运行的过程中可以对代码进行监控,从而使用 JIT 技术对代码进行编译优化

三 V8 的编译原理

V8 是一个开源的 JavaScript 虚拟机,目前主要用在 Chrome 浏览器(包括开源的 Chromium)以及 Node.js 中,核心功能是用于解析和执行 JavaScript 语言。为了解决早期 JavaScript 运行性能差的问题,V8 经历了多个历史的编译框架衍变之后(感兴趣的同学可以了解一下早期的 V8 编译框架设计),引入混合动态编译的技术来解决问题,具体详细的编译框架如下所示:

1 Ignition 解释器

Ignition 的主要作用是将 AST 转换成 Bytecode(字节码,中间表示)。在运行的过程中,还会使用类型反馈(TypeFeedback)技术并计算热点代码(HotSpot,重复被运行的代码,可以是方法也可以是循环体),最终交给 TurboFan 进行动态运行时的编译优化。Ignition 的解释执行流程如下所示:

在字节码解释执行的过程中,会将需要进行性能优化的运行时信息指向对应的 Feedback Vector(反馈向量,之前也被称为 Type Feedback Vector),Feeback Vector 中会包含根据内联缓存(Inline Cache,IC)来存储的多种类型的插槽(Feedback Vector Slot)信息,例如 BinaryOp 插槽(二进制操作结果的数据类型)、Invocation Count(函数的调用次数)以及 Optimized Code 信息等。

这里不会过多讲解每个执行流程的细节问题。

2 TurboFan 优化编译器

TurboFan 利用了 JIT 编译技术,主要作用是对 JavaScript 代码进行运行时编译优化,具体的流程如下所示:

图片出处 An Introduction to Speculative Optimization in V8。

需要注意 Profiling Feedback 部分,这里主要提供 Ignition 解释执行过程中生成的运行时反馈向量信息 Feedback Vector ,Turbofan 会结合字节码以及反馈向量信息生成图示(数据结构中的图结构),并将图传递给前端部分,之后会根据反馈向量信息对代码进行优化和去优化。

这里的去优化是指让代码回退到 Ignition 进行解释执行,去优化本质是因为机器码已经不能满足运行诉求,例如一个变量从 string 类型转变成 number 类型,机器码编译的是 string 类型,此时已经无法再满足运行诉求,因此 V8 会执行去优化动作,将代码回退到 Ignition 进行解释执行。

四 V8 的运行时表现

在了解 V8 的编译原理之后,接下来需要使用 V8 的调试工具来具体查看 JavaScript 的编译和运行信息,从而加深我们对 V8 的编译过程认知。

1 D8 调试工具

如果想了解 JavaScript 在 V8 中的编译时和运行时信息,可以使用调试工具 D8。D8 是 V8 引擎的命令行 Shell,可以查看 AST 生成、中间代码 ByteCode、优化代码、反优化代码、优化编译器的统计数据、代码的 GC 等信息。D8 的安装方式有很多,如下所示:

  • 方法一:根据 V8 官方文档 Using d8 以及 Building V8 with GN 进行工具链的下载和编译
  • 方法二:使用别人已经编译好的 D8 工具,可能版本会有滞后性,例如 Mac 版
  • 方法三:使用 JavaScript 引擎版本管理工具,例如 jsvu,可以下载到最新编译好的 JavaScript 引擎

本文使用方法三安装 v8-debug 工具,安装完成后执行 v8-debug --help 可以查看有哪些命令:

# 执行 help 命令查看支持的参数
v8-debug --help

Synopsis:
  shell [options] [--shell] [<file>...]
  d8 [options] [-e <string>] [--shell] [[--module|--web-snapshot] <file>...]

  -e        execute a string in V8
  --shell   run an interactive JavaScript shell
  --module  execute a file as a JavaScript module
  --web-snapshot  execute a file as a web snapshot

SSE3=1 SSSE3=1 SSE4_1=1 SSE4_2=1 SAHF=1 AVX=1 AVX2=1 FMA3=1 BMI1=1 BMI2=1 LZCNT=1 POPCNT=1 ATOM=0
The following syntax for options is accepted (both '-' and '--' are ok):
  --flag        (bool flags only)
  --no-flag     (bool flags only)
  --flag=value  (non-bool flags only, no spaces around '=')
  --flag value  (non-bool flags only)
  --            (captures all remaining args in JavaScript)

Options:
    # 打印生成的字节码
  --print-bytecode (print bytecode generated by ignition interpreter)
        type: bool  default: --noprint-bytecode

    
    # 跟踪被优化的信息
     --trace-opt (trace optimized compilation)
        type: bool  default: --notrace-opt
  --trace-opt-verbose (extra verbose optimized compilation tracing)
        type: bool  default: --notrace-opt-verbose
  --trace-opt-stats (trace optimized compilation statistics)
        type: bool  default: --notrace-opt-stats

    # 跟踪去优化的信息
  --trace-deopt (trace deoptimization)
        type: bool  default: --notrace-deopt
  --log-deopt (log deoptimization)
        type: bool  default: --nolog-deopt
  --trace-deopt-verbose (extra verbose deoptimization tracing)
        type: bool  default: --notrace-deopt-verbose
  --print-deopt-stress (print number of possible deopt points)

    
    # 查看编译生成的 AST
  --print-ast (print source AST)
        type: bool  default: --noprint-ast

    # 查看编译生成的代码
  --print-code (print generated code)
        type: bool  default: --noprint-code

    # 查看优化后的代码
  --print-opt-code (print optimized code)
        type: bool  default: --noprint-opt-code

    # 允许在源代码中使用 V8 提供的原生 API 语法
  --allow-natives-syntax (allow natives syntax)
        type: bool  default: --noallow-natives-syntax

2 生成 AST

我们编写一个 index.js 文件,在文件中写入 JavaScript 代码,执行一个简单的 add 函数:

function add(x, y) {
    return x + y
}

console.log(add(1, 2));

使用 --print-ast 参数可以打印 add 函数的 AST 信息:

v8-debug --print-ast ./index.js

[generating bytecode for function: ]
--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . FUNCTION "add" = function add
. EXPRESSION STATEMENT at 41
. . ASSIGN at -1
. . . VAR PROXY local[0] (0x7fb8c080e630) (mode = TEMPORARY, assigned = true) ".result"
. . . CALL
. . . . PROPERTY at 49
. . . . . VAR PROXY unallocated (0x7fb8c080e6f0) (mode = DYNAMIC_GLOBAL, assigned = false) "console"
. . . . . NAME log
. . . . CALL
. . . . . VAR PROXY unallocated (0x7fb8c080e470) (mode = VAR, assigned = true) "add"
. . . . . LITERAL 1
. . . . . LITERAL 2
. RETURN at -1
. . VAR PROXY local[0] (0x7fb8c080e630) (mode = TEMPORARY, assigned = true) ".result"

[generating bytecode for function: add]
--- AST ---
FUNC at 12
. KIND 0
. LITERAL ID 1
. SUSPEND COUNT 0
. NAME "add"
. PARAMS
. . VAR (0x7fb8c080e4d8) (mode = VAR, assigned = false) "x"
. . VAR (0x7fb8c080e580) (mode = VAR, assigned = false) "y"
. DECLS
. . VARIABLE (0x7fb8c080e4d8) (mode = VAR, assigned = false) "x"
. . VARIABLE (0x7fb8c080e580) (mode = VAR, assigned = false) "y"
. RETURN at 25
. . ADD at 34
. . . VAR PROXY parameter[0] (0x7fb8c080e4d8) (mode = VAR, assigned = false) "x"
. . . VAR PROXY parameter[1] (0x7fb8c080e580) (mode = VAR, assigned = false) "y"

我们以图形化的方式来描述生成的 AST 树:

VAR PROXY 节点在真正的分析阶段会连接到对应地址的 VAR 节点。

3 生成字节码

AST 会经过 Ignition 解释器的 BytecodeGenerator 函数生成字节码(中间表示),我们可以通过 --print-bytecode 参数来打印字节码信息:

v8-debug --print-bytecode ./index.js

[generated bytecode for function:  (0x3ab2082933f5 <SharedFunctionInfo>)]
Bytecode length: 43
Parameter count 1
Register count 6
Frame size 48
OSR nesting level: 0
Bytecode Age: 0
         0x3ab2082934be @    0 : 13 00             LdaConstant [0]
         0x3ab2082934c0 @    2 : c3                Star1 
         0x3ab2082934c1 @    3 : 19 fe f8          Mov <closure>, r2
         0x3ab2082934c4 @    6 : 65 52 01 f9 02    CallRuntime [DeclareGlobals], r1-r2
         0x3ab2082934c9 @   11 : 21 01 00          LdaGlobal [1], [0]
         0x3ab2082934cc @   14 : c2                Star2 
         0x3ab2082934cd @   15 : 2d f8 02 02       LdaNamedProperty r2, [2], [2]
         0x3ab2082934d1 @   19 : c3                Star1 
         0x3ab2082934d2 @   20 : 21 03 04          LdaGlobal [3], [4]
         0x3ab2082934d5 @   23 : c1                Star3 
         0x3ab2082934d6 @   24 : 0d 01             LdaSmi [1]
         0x3ab2082934d8 @   26 : c0                Star4 
         0x3ab2082934d9 @   27 : 0d 02             LdaSmi [2]
         0x3ab2082934db @   29 : bf                Star5 
         0x3ab2082934dc @   30 : 63 f7 f6 f5 06    CallUndefinedReceiver2 r3, r4, r5, [6]
         0x3ab2082934e1 @   35 : c1                Star3 
         0x3ab2082934e2 @   36 : 5e f9 f8 f7 08    CallProperty1 r1, r2, r3, [8]
         0x3ab2082934e7 @   41 : c4                Star0 
         0x3ab2082934e8 @   42 : a9                Return 
Constant pool (size = 4)
0x3ab208293485: [FixedArray] in OldSpace
 - map: 0x3ab208002205 <Map>
 - length: 4
           0: 0x3ab20829343d <FixedArray[2]>
           1: 0x3ab208202741 <String[7]: #console>
           2: 0x3ab20820278d <String[3]: #log>
           3: 0x3ab208003f09 <String[3]: #add>
Handler Table (size = 0)
Source Position Table (size = 0)
[generated bytecode for function: add (0x3ab20829344d <SharedFunctionInfo add>)]
Bytecode length: 6
// 接受 3 个参数, 1 个隐式的 this,以及显式的 x 和 y
Parameter count 3
Register count 0
// 不需要局部变量,因此帧大小为 0 
Frame size 0
OSR nesting level: 0
Bytecode Age: 0
         0x3ab2082935f6 @    0 : 0b 04             Ldar a1
         0x3ab2082935f8 @    2 : 39 03 00          Add a0, [0]
         0x3ab2082935fb @    5 : a9                Return 
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

add 函数主要包含以下 3 个字节码序列:

// Load Accumulator Register
// 加载寄存器 a1 的值到累加器中
Ldar a1
// 读取寄存器 a0 的值并累加到累加器中,相加之后的结果会继续放在累加器中
// [0] 指向 Feedback Vector Slot,Ignition 会收集值的分析信息,为后续的 TurboFan 优化做准备
Add a0, [0]
// 转交控制权给调用者,并返回累加器中的值
Return 

这里 Ignition 的解释执行这些字节码采用的是一地址指令结构的寄存器架构。

关于更多字节码的信息可查看 Understanding V8’s Bytecode。

4 优化和去优化

JavaScript 是弱类型语言,不会像强类型语言那样需要限定函数调用的形参数据类型,而是可以非常灵活的传入各种类型的参数进行处理,如下所示:

function add(x, y) { 
    // + 操作符是 JavaScript 中非常复杂的一个操作
    return x + y
}

add(1, 2);
add('1', 2);
add(, 2);
add(undefined, 2);
add([], 2);
add({}, 2);
add([], {});

为了可以进行 + 操作符运算,在底层执行的时候往往需要调用很多 API,比如 ToPrimitive(判断是否是对象)、ToString、ToNumber 等,将传入的参数进行符合 + 操作符的数据转换处理。

在这里 V8 会对 JavaScript 像强类型语言那样对形参 x 和 y 进行推测,这样就可以在运行的过程中排除一些副作用分支代码,同时这里也会预测代码不会抛出异常,因此可以对代码进行优化,从而达到最高的运行性能。在 Ignition 中通过字节码来收集反馈信息(Feedback Vector),如下所示:

为了查看 add 函数的运行时反馈信息,我们可以通过 V8 提供的 Native API 来打印 add 函数的运行时信息,具体如下所示:

function add(x, y) {
    return x + y
}

// 注意这里默认采用了 ClosureFeedbackCellArray,为了查看效果,强制开启 FeedbackVector
// 更多信息查看: A lighter V8:https://v8.dev/blog/v8-lite
%EnsureFeedbackVectorForFunction(add);
add(1, 2);
// 打印 add 详细的运行时信息
%DebugPrint(add);

通过 --allow-natives-syntax 参数可以在 JavaScript 中调用 %DebugPrint 底层 Native API(更多 API 可以查看 V8 的 runtime.h 头文件):


点击链接查看原文V8 编译浅谈,关注公众号【阿里技术】获取更多福利!

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。