javascript執行緒及與執行緒有關的效能優化

風靈使發表於2019-01-05
友情連結

一篇很好的幫你理解javascript events loop的文章

宣告

這些內容都是按我自己的理解來組織和寫的,可能術語什麼的有些不是很嚴謹,所以有些概念模糊的應也專業的術語為準,這裡介紹的有些“術語”並不等同於你已經知道的那些“術語”,所以不要硬套概念在這裡去理解!當然了,我也會經常複習檢視這裡的文件,對一些錯誤額觀點會及時更正,儘量保證嚴謹性!

javascript執行緒

我覺得在開始描述相關問題之前,需要理解一下javascript裡面的執行緒概念,首先需要知道:

  • javascript是單執行緒的,也就是說,一段程式碼,js執行的時候是從上往下一句一句的執行,前面的程式碼永遠要先於後面的程式碼執行,如:
	var a = 15;
	var b = 16;
	//這裡js程式碼在執行的時候,肯定先執行把15賦值給a的操作,再來執行把16賦值給b的操作
  • 同步操作、非同步操作
  • 首先得知道什麼是同步操作!就好比兩個人去食堂排隊打飯,排在前面的人打完之後才輪到後面的人打飯!這就是同步操作,大家按先來後到的順序做事!同步的好處就是簡單有規則,所以除錯起來相對輕鬆,因為大家都是按“規則”辦事的,不會出現“插隊”的情況,所以要“調查誰”,只要找到“它前面的相關人”,就能“逮住他”。同樣,同步也是有不好的地方的,比如資源不能充分利用,因為“排隊”的時候不能做其他事情!只能等待,不能合理安排自己的任務等!

簡單來說,瀏覽器javascript同步任務指的是在執行棧排隊執行的任務,這個執行棧也就是所謂的javascript執行程式碼的主執行緒!

  • 非同步操作,同樣以去食堂打飯來說明!有一群人去食堂打飯,小明發現在他前面有好多好多人在排隊,可是剛好他現在有一件急需要做的事情去處理,還好,他有一個很好的朋友在食堂吃飯,於是他跑過去跟他朋友說道:“哥們,我現在有很重要的事需要做,你能不能在人少的時候給我打電話告訴我一下,我再過來打飯!”於是,小明就去做他自己的事情去了,等沒有人排隊的時候,他的朋友打電話告訴他,可以過來打飯了!於是小明就很舒服地去打飯了。其實可以把這個過程就叫做非同步,我們可以看到,非同步很亮眼的一個好處就是,小明可以打飯和做其他事情兩不誤,所以能合理利用資源!當然了,這是需要付出代價的,至少在程式碼實現上肯定比同步難!非同步的不好的地方也有很多,很難除錯和斷言,比如下面的程式碼:
	var a = '';
	getData();//前面裡面包含一個非同步操作,實現對a = 15的賦值操作
	console.log(a);//我們發現在這個地方列印a是個空字串,因為在這個地方,非同步操作並沒有執行
	//解決方法就是使用回撥,callback,如
	getData(function(){
		console.log(a);//print  15
	});

一句話說明,瀏覽器中javascript非同步任務是沒有進入執行棧的javascript任務,而是進入了一個稱為事件佇列的地方去排隊等待執行,排隊的規則是先到的排在前面,後到的排在後面。這些非同步任務會在自己準備好之後,通過觸發一些事件來告知主執行緒,自己已經把該做的都做完 了,而且我還給你了一個函式你(主執行緒)去處理吧!這個函式也就是所謂的回撥函式,到現在為止,我才明白為什麼回撥函式為什麼是非同步的呢!(注:此回撥函式不同於你在同步任務裡面寫的回撥函式,反正記住一條,回撥本身不是非同步的,而是因為回撥是非同步任務準備好之後給的函式是非同步的!) 然後當主執行緒中的任務全部執行完成之後,也就是主執行緒空閒之後,會對事件佇列進行一個輪詢,從而執行了非同步任務!

  • 按我的理解來說,javascript只是“同步”的,沒有“非同步”一說!只不過因為javascript程式碼藉助了程式碼所在的宿主環境,由宿主來管理這些“非同步”的程式碼,從而讓javascript得以實現“非同步”一說!那麼宿主是怎麼管理“非同步程式碼”的呢?簡單來說就是通過一種排隊機制實現的!可以這樣子來理解:假設當前有一段程式碼正在執行,而且大概需要執行20ms,當執行到10ms時候突然觸發了一個點選事件,這裡如果是多執行緒的話,那麼不用等待,監聽器直接觸發,可是js單執行緒的,所以事件監聽器不能執行,那怎麼辦呢?此時,宿主的管理作用就出來了,宿主並沒有讓事件監聽器立即執行,而是把監聽器的程式碼用排隊的方式放在當前執行程式碼的後面,噹噹前程式碼在20ms之後執行完成之後,再來執行事件監聽器程式碼!可以用一張圖片把這個過程描述如下:
    在這裡插入圖片描述

setTimeoutsetInterval

  • setTimeout定時器

setTimeout描述的操作就是程式在多少時間之後再執行某操作,如:

	
	var a = 1;
	function fun(){
		a += 1;
		console.log(a);
	};

	setTimeout(fun,5000);
	//5秒之後列印2

setTimeout API


  	var id = setTimeout(fn,timer);
  	//fn是簽名函式
  	//timer間隔時間
  	//返回一個id值,在fn未觸發之前,可以通過clearTimeout(id)清除,從而不執行fn
  	clearTimeout(id);

  • setInterval 間隔定時器

setInterval描述的是每隔多少時間執行某操作,如:

	var cc = 1;
	function fn(){
		cc += 1;
		console.log(cc);
	};


	setInterval(fn,1000);

setInterval API

	var id = setInterval(fn,timer);
	//fn是要執行簽名名字,
	//timer是間隔時間
	//返回一個id,用於將來某個時間用clearInterval清除間隔定時器
	clearInterval(id);

setTimeoutsetInterval的區別

  • 首先從概念上來說明,setTimeout多少時間之後執行某操作,只執行一次,而setInterval每隔多少時間之後執行某操作,如果不用clearInterval清除的話,將會一直執行下去。其實兩個方法都返回一個id值,用於清除定時器,分別是clearTimeoutclearInterval,還有說明一下這兩個操作都是非同步的,其實這也是javascript在瀏覽器中最最最簡單的非同步操作了!

  • 再次從效能上來說,setTimeout的效能是要優於setInterval的,這一點將會在後面的文件中說明,需要聯絡上面所說的排隊機制!

  • setTimeoutsetInterval都不能保證到了時間點一定會執行,如:setTimeout(fn,5000),並不能保證5s之後一定能執行fn。這得取決於當前js執行緒佇列裡面還有沒有其他待處理佇列,如果剛好沒有的話,那麼就能剛好執行,如果當前執行緒裡面已經有了其它待處理佇列正在執行,那麼需要排隊,等到javascript執行緒空閒的時候才會執行定時器!還有需要記住一點,能用setInterval實現的操作,一定能用setTimeout來實現,如下面的例子:

	
	//實現對一個數字定時加1操作 
	//setTimeout
	(function(){
		var a = 0;
		setTimeout(function fun(){
			a += 1;
			console.log(a);
			setTimeout(fun,1000);
		},1000);
	})();

	//setInterval

	(function(){
		var a = 0;
		setInterval(function(){
			a += 1;
			console.log(a);
		},1000);
	})();


  • setTimeoutsetInterval最重要的區別就是:如果用setTimeoutsetInterval來實現一個重複的操作,切記!setTimeout是等待迴圈的操作執行完成之後,才繼續在間隔時間之後再把迴圈操作新增到javascript的執行緒裡面,而setInterval是不等待的,它從來不管放線上程裡面迴圈操作有沒有執行完成,反正到點就會把迴圈操作新增到javascript執行緒佇列裡面。但是這裡有一點需要說明一下,js執行緒不會維護setInterval裡面已經過期的了的迴圈操作,所以同一個setInterval線上程裡面只會有一個輪次。理解這一點很重要,這是setTimeout效能優於setInterval的根源!現在用一張草圖說明一下這個過程,如下:

setTimeout

setTimeout

注意:上面的圖實際上有點不準確,正常情況應該是在10ms處時才新增第一個佇列,然後在30ms處新增第二個佇列,以此類推!這裡只是為方便說明,所以圖片上是在0ms時新增了第一個佇列,望注意!

setInterval

setInterval

由此可見,setTimeout可以讓瀏覽器喘口氣,因為setTimeout是等他新增的佇列執行完成之後才在間隔時間後新增佇列,而setInterval是不管瀏覽器死活的,它自己爽了就好,它定時就新增佇列,但是嚴重影響效能!至於為什麼這樣會影響效能,後面的文件會仔細說明!(合理的利用setTimeout,能把一個耗時大的操作,變成一些耗時短小的操作,從而提升畫面互動體驗,比如頁面卡頓什麼的!

耗時大的操作影響互動和效能

  • 為了說明這個問題,我們需要一個例項來說明一下,下面是例項的節選程式碼,全部程式碼可到demo1.html!我們這裡實現一個操作:用js實現向頁面新增20000*6的一個表格,並且每個單元格需要顯示當前的序號,我們知道反覆對html進行dom操作、渲染是一個很影響效能的過程,檢視頁面就知道很卡,而且還可能當機等情況!話不多說,程式碼如下:
	<table>
		<tbody></tbody>
	</table>



	 <script type="text/javascript">
	 	
	 window.onload = function(){
	 	(function(){

	 		var table = document.getElementsByTagName('table')[0];
		 	var tbody = table.getElementsByTagName('tbody')[0];
		 	var num = 0
		 	for(var i = 0,len = 20000;i<len;i++){
		 		var tr = document.createElement("tr");
		 		for(var j = 0,len1 = 6;j<len1;j++){
		 			var td = document.createElement('td');
		 			num += 1;
		 			var txt = document.createTextNode(num);
		 			td.appendChild(txt);
		 			tr.appendChild(td);
		 		};
		 		tbody.appendChild(tr);
		 	};


	 	})();
	 };



	 </script>
	

我們發現上面的頁面載入的時候空白了一段時間,雖然這裡效能損耗還不足以讓瀏覽器當機。但現在改進一下js程式碼,是可以讓這個空白時間縮短的,好的,程式碼如下(檢視全部程式碼):



	<table>
		<tbody></tbody>
	</table>
	
	<script type="text/javascript">
	 	
	 window.onload = function(){
	 	(function(){
	 		/*這裡我們把原本一步完成的事情,在這裡分成5小步,從而達到把耗時大的程式碼劃分為耗時小的程式碼
	 		有利於html頁面快速構建*/
	 		var table = document.getElementsByTagName('table')[0];
	 		var tbody = table.getElementsByTagName('tbody')[0];
	 		var stepNum = 4000;
	 		var isComplete = false;//表格是否渲染完成
	 		var num = 0;//單元格序號
	 		var timeoutId = setTimeout(function fn(){
	 			if(isComplete){
	 				clearTimeout(timeoutId);
	 				return;
	 			};
	 			for(var i = 0,len = 4000;i<len;i++){
	 				var tr = document.createElement('tr');
	 				for(var j = 0,len1 = 6;j<len1;j++){
	 					var td = document.createElement('td');
	 					num += 1;
	 					var currentNum = num;//因為i是從零開始的,所以需要加1
	 					td.appendChild(document.createTextNode(currentNum));
	 					tr.appendChild(td);
	 				};
	 				tbody.appendChild(tr);
	 			};
	 			stepNum += 4000;
	 			if(stepNum > 20000){
	 				isComplete = true;//說明已經超過20000行了
	 			};
	 			setTimeout(fn,0);//0ms之後繼續呼叫fn
	 			//這裡說明一下,setTimeout和setInterval並不能準確保證短時粒度的執行
	 			//也就是說,這裡雖然要求是0ms之後把程式碼推送到事件佇列裡面
	 			//但是可能實際上是真正執行的是在比0ms長的時間之後推送到時間佇列裡面
	 			//關於這一點可以再開一個單元來說明
	 		},0);
	 	})();
	 };



	 </script>


我們發現使用了setTimeout來的程式碼開啟頁面會快了許多,當然了可能視覺上看不是很明顯,原因也是有的,其一就是我們這裡的程式碼量還算在合理量之間,其二,可能跟瀏覽器的效能什麼的有一些關係。但這的確是加快了頁面響應時間的,不信,我們可以在程式碼中加一些東西,來看看當頁面剛記載的時候到頁面有內容呈現花了多少時間,所以對以上程式碼分別做如下更改

未用setTimeout版,點這裡檢視全部程式碼

	

	<table>
		<tbody></tbody>
	</table>




	<script type="text/javascript">
	 	
	 window.onload = function(){
	 	var startTime = new Date().getTime();
	 	(function(){

	 		var table = document.getElementsByTagName('table')[0];
		 	var tbody = table.getElementsByTagName('tbody')[0];
		 	var num = 0
		 	for(var i = 0,len = 20000;i<len;i++){
		 		var tr = document.createElement("tr");
		 		for(var j = 0,len1 = 6;j<len1;j++){
		 			var td = document.createElement('td');
		 			num += 1;
		 			var txt = document.createTextNode(num);
		 			td.appendChild(txt);
		 			tr.appendChild(td);
		 		};
		 		tbody.appendChild(tr);
		 	};


	 	})();
	 	var endTime = new Date().getTime();
	 	var diffTime = endTime - startTime;
	 	console.log("頁面渲染這個表格花費了"+diffTime+"毫秒");
	 };



	 </script>

瀏覽器控制檯的截圖(chrome瀏覽器)在這裡插入圖片描述

使用setTimeout版,點這裡檢視全部程式碼


	 <table>
	 	<tbody></tbody>	
	 </table>




	 <script type="text/javascript">
	 	
	 window.onload = function(){
	 	var startTime = new Date().getTime();
	 	(function(){
	 		/*這裡我們把原本一步完成的事情,在這裡分成5小步,從而達到把耗時大的程式碼劃分為耗時小的程式碼
	 		有利於html頁面快速構建*/
	 		var table = document.getElementsByTagName('table')[0];
	 		var tbody = table.getElementsByTagName('tbody')[0];
	 		var stepNum = 4000;
	 		var isComplete = false;//表格是否渲染完成
	 		var num = 0;//單元格序號
	 		var isDisplayTime = true;//是否列印時間
	 		var timeoutId = setTimeout(function fn(){
	 			if(isComplete){
	 				clearTimeout(timeoutId);
	 				return;
	 			};
	 			for(var i = 0,len = 4000;i<len;i++){
	 				var tr = document.createElement('tr');
	 				for(var j = 0,len1 = 6;j<len1;j++){
	 					var td = document.createElement('td');
	 					num += 1;
	 					var currentNum = num;//因為i是從零開始的,所以需要加1
	 					td.appendChild(document.createTextNode(currentNum));
	 					tr.appendChild(td);
	 				};
	 				tbody.appendChild(tr);
	 			};
	 			stepNum += 4000;
	 			if(stepNum > 20000){
	 				isComplete = true;//說明已經超過20000行了
	 			};
	 			if(isDisplayTime){
	 				isDisplayTime = false;
	 				var endTime = new Date().getTime();
	 				var diffTime = endTime - startTime;
	 				console.log("渲染這個表格共花了"+diffTime+"毫秒");
	 			};
	 			setTimeout(fn,0);//0ms之後繼續呼叫fn
	 			//這裡說明一下,setTimeout和setInterval並不能準確保證短時粒度的執行
	 			//也就是說,這裡雖然要求是0ms之後把程式碼推送到事件佇列裡面
	 			//但是可能實際上是真正執行的是在比0ms長的時間之後推送到時間佇列裡面
	 			//關於這一點可以再開一個單元來說明
	 		},0);
	 	})();
	 };



	 </script>

瀏覽器控制檯的截圖(chrome瀏覽器)
在這裡插入圖片描述

setTimeout是怎麼提升頁面響應時間的?

實際上這得歸功於瀏覽器的內部渲染機制,這裡不做過多介紹,因為要講明白這些東西,完全是就是寫一個長篇大論了,奈何自己能力有限,有些知識的掌握程度還欠火候,所以不能在這裡亂說一些,只能把自己所能掌握的說明一下!

其實瀏覽器有一個機制,那就是如果某段程式碼的執行時間過長,那麼就會造成頁面卡頓,因為在某段程式碼執行的過程中,它不能做其它事情,不能渲染頁面。甚至有些程式碼的執行時間實在過長,瀏覽器會直接當機,當然了有的瀏覽器對執行時間大於某個閥值的,會直接給出彈出提示,並拒絕程式碼的執行!

setTimeout的奧妙就是把一個執行時間很長的程式碼分成執行時間很小的程式碼段,這樣瀏覽器就能逐步渲染頁面了,從而解決了頁面遲遲顯示不出來的問題,以及因為程式碼執行時間過長瀏覽器當機的問題。

事件輪詢

這部分內容待完善

setTimeoutsetInterval間隔時間粒度討論(僅作討論,以說明在小粒度的時候誤差很大)

目前來說,鑑於各大瀏覽器的js引擎等原因,這兩種定時器都很難實現時間間隔粒度精確到1ms或比這個時間更小的時間粒度的處理,當然了,瀏覽器各大廠商正在努力想這個方向靠攏!我們來做一個測試,程式碼如下:

setTimeout版這裡檢視全部程式碼

		

		var startTime = new Date().getTime();
		for(var i = 0;i<100;i++){
			setTimeout(function fn(){
				var endTime = new Date().getTime();
				var diffTime = endTime - startTime;
				console.log("中間相差了"+diffTime+"毫秒");
				startTime = endTime;//結束時間作開始時間
			},1);
		};

  • 瀏覽器控制檯截圖(firefox瀏覽器)
    在這裡插入圖片描述

setInterval版,點這裡


	var startTime = new Date().getTime();
	var num = 0;
	var id = setInterval(function fn(){			
		if(num>=100){
			clearInterval(id);
			return;
		};
		var endTime = new Date().getTime();
		var diffTime = endTime - startTime;
		startTime = endTime;//結束時間賦值給開始時間
		console.log("間隔了"+diffTime+"毫秒");
		num += 1;
	},1);

瀏覽器控制檯截圖(firefox瀏覽器)

在這裡插入圖片描述

我們從截圖可以知道:setTimeoutsetInterval都有誤差,但是setTimeout波動沒有setInterval那麼大!同時如果我們把間隔時間設定為較大的一個時間粒度,同樣也會有誤差,但是相對說來說,影響不是很大,可以忽略不計,但是小粒度就得注意了,因為對於5000ms有個0~10ms左右的誤差都可以忽略不計的,但是對於1ms有個幾毫秒的誤差就得商榷了!

相關文章