整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          這一次,徹底弄懂 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 編譯淺談,關注公眾號【阿里技術】獲取更多福利!

          版權聲明:本文內容由阿里云實名注冊用戶自發貢獻,版權歸原作者所有,阿里云開發者社區不擁有其著作權,亦不承擔相應法律責任。具體規則請查看《阿里云開發者社區用戶服務協議》和《阿里云開發者社區知識產權保護指引》。如果您發現本社區中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社區將立刻刪除涉嫌侵權內容。


          主站蜘蛛池模板: 精品国产AⅤ一区二区三区4区| 国产福利电影一区二区三区久久久久成人精品综合 | 日亚毛片免费乱码不卡一区| 乱色熟女综合一区二区三区| 99精品国产高清一区二区三区| 日本伊人精品一区二区三区| 激情久久av一区av二区av三区| 国产不卡视频一区二区三区| 日韩免费无码视频一区二区三区| 中文激情在线一区二区| 一区二区三区午夜| 亚洲一区动漫卡通在线播放| 国产高清一区二区三区视频| 国产午夜三级一区二区三| 精品香蕉一区二区三区| 一区二区三区在线|日本| 91在线视频一区| 日本夜爽爽一区二区三区| 无码人妻精品一区二区三区久久| 国模极品一区二区三区| 国产亚洲一区二区三区在线观看| 天堂Av无码Av一区二区三区| 国产婷婷一区二区三区| 亚洲丶国产丶欧美一区二区三区| 国产精品一区三区| 无码少妇A片一区二区三区| 综合久久久久久中文字幕亚洲国产国产综合一区首 | 狠狠爱无码一区二区三区| 看电影来5566一区.二区| 国产一区二区三区在线观看免费| 国产午夜精品一区理论片| 国产精品一区电影| 久久久精品人妻一区二区三区蜜桃 | 精品久久久久一区二区三区| 在线播放偷拍一区精品| 成人精品视频一区二区三区不卡 | 97久久精品无码一区二区天美| 国产高清在线精品一区二区| 精品少妇人妻AV一区二区| 国产av一区二区三区日韩| 人妻无码一区二区三区四区|