javascript線程及與線程有關的性能優化

友情鏈接

一篇很好的幫你理解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有個幾毫秒的誤差就得商榷了!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章