友情鏈接
一篇很好的幫你理解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之後執行完成之後,再來執行事件監聽器代碼!可以用一張圖片把這個過程描述如下:
setTimeout
和setInterval
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);
setTimeout
和setInterval
的區別
-
首先從概念上來說明,
setTimeout
多少時間之後執行某操作,只執行一次,而setInterval
每隔多少時間之後執行某操作,如果不用clearInterval
清除的話,將會一直執行下去。其實兩個方法都返回一個id值,用於清除定時器,分別是clearTimeout
和clearInterval
,還有說明一下這兩個操作都是異步的,其實這也是javascript
在瀏覽器中最最最簡單的異步操作了! -
再次從性能上來說,
setTimeout
的性能是要優於setInterval
的,這一點將會在後面的文檔中說明,需要聯繫上面所說的排隊機制! -
setTimeout
和setInterval
都不能保證到了時間點一定會執行,如: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);
})();
setTimeout
和setInterval
最重要的區別就是:如果用setTimeout
和setInterval
來實現一個重複的操作,切記!setTimeout
是等待循環的操作執行完成之後,才繼續在間隔時間之後再把循環操作添加到javascript
的線程裏面,而setInterval
是不等待的,它從來不管放在線程裏面循環操作有沒有執行完成,反正到點就會把循環操作添加到javascript
線程隊列裏面。但是這裏有一點需要說明一下,js線程不會維護setInterval
裏面已經過期的了的循環操作,所以同一個setInterval
在線程裏面只會有一個輪次。理解這一點很重要,這是setTimeout
性能優於setInterval
的根源!現在用一張草圖說明一下這個過程,如下:
setTimeout
注意:上面的圖實際上有點不準確,正常情況應該是在10ms
處時才添加第一個隊列,然後在30ms
處添加第二個隊列,以此類推!這裏只是爲方便說明,所以圖片上是在0ms時添加了第一個隊列,望注意!
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的奧妙就是把一個執行時間很長的代碼分成執行時間很小的代碼段,這樣瀏覽器就能逐步渲染頁面了,從而解決了頁面遲遲顯示不出來的問題,以及因爲代碼執行時間過長瀏覽器死機的問題。
事件輪詢
這部分內容待完善
setTimeout
和setInterval
間隔時間粒度討論(僅作討論,以說明在小粒度的時候誤差很大)
目前來說,鑑於各大瀏覽器的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瀏覽器)