深入理解JavaScript定時(shí)機(jī)制
本文介紹了JavaScript定時(shí)機(jī)制,要理解JavaScript的定時(shí)機(jī)制,就要知道JavaScript的運(yùn)行機(jī)制。
首先聲明,JavaScript是單線程運(yùn)行(JavaScript引擎線程)事件驅(qū)動(dòng)。
一、瀏覽器中有多個(gè)線程
一款瀏覽器中包含的最基本的線程:
1、JavaScript引擎線程。
2、定時(shí)器線程,setInterval和setTimeout會(huì)觸發(fā)這個(gè)線程。
3、瀏覽器事件觸發(fā)線程,這個(gè)線程會(huì)觸發(fā)onclick、onmousemove和其它瀏覽器事件。
4、界面渲染線程,負(fù)責(zé)渲染瀏覽器界面HTML元素。注意:在JavaScript引擎運(yùn)行腳本期間,界面渲染線程都是處于掛起狀態(tài)的。也就是說(shuō)當(dāng)使用JavaScript對(duì)界面中的節(jié)點(diǎn)進(jìn)行操作時(shí),并不會(huì)立即體現(xiàn)出來(lái),要等到JavaScript引擎線程空閑時(shí),才會(huì)體現(xiàn)出來(lái)。(這個(gè)最后說(shuō))
5、HTTP請(qǐng)求線程(Ajax請(qǐng)求也在其中)。
以上這些線程在瀏覽器內(nèi)核的控制下,相互配合,完成工作(具體我也不知道)。
二、任務(wù)隊(duì)列
我們知道JavaScript是單線程的,所有JavaScript代碼都在JavaScript引擎線程中運(yùn)行。阮一峰老師的文章中叫這個(gè)線程為主線程,是一個(gè)執(zhí)行棧。(以下內(nèi)容也主要是根據(jù)阮一峰老師的文章理解總結(jié)。)
這些JavaScript代碼我們可以把他們看成一個(gè)個(gè)的任務(wù),這些任務(wù)有同步任務(wù)和異步任務(wù)之分。同步任務(wù)(比如變量賦值語(yǔ)句,alert語(yǔ)句,函數(shù)聲明語(yǔ)句等等)直接在主線程上按順序執(zhí)行,異步任務(wù)(比如瀏覽器事件觸發(fā)線程觸發(fā)的各種各樣的事件,使用Ajax返回的服務(wù)器響應(yīng)等)按照時(shí)間先后順序在任務(wù)隊(duì)列(也可以叫做事件隊(duì)列、消息隊(duì)列)中排隊(duì),等待被執(zhí)行。只要主線程上的任務(wù)執(zhí)行完了,就會(huì)去檢查任務(wù)隊(duì)列,看有沒(méi)有排隊(duì)等待的任務(wù),有就讓排隊(duì)的任務(wù)進(jìn)入主線程執(zhí)行。
比如下面的例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>定時(shí)機(jī)制</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div id="test"> </div> <script> var pro = document.getElementById('test'); pro.onclick = function() { alert('我沒(méi)有立即被執(zhí)行。'); }; function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } test(); </script> </body> </html>
在這個(gè)例子中test()函數(shù)執(zhí)行完大概要8~9秒,所以當(dāng)我們打開(kāi)這個(gè)頁(yè)面,在8秒之前點(diǎn)擊粉色方塊,不會(huì)立即彈出提示框,而要等到8秒之后才彈出,而且8秒之前點(diǎn)擊幾次粉色框,8秒之后就彈出幾次。
我們打開(kāi)這個(gè)頁(yè)面時(shí),主線程先聲明函數(shù)test,再聲明變量pro,然后把p節(jié)點(diǎn)賦值給pro,然后給p節(jié)點(diǎn)添加click事件,并指定回調(diào)函數(shù)(掛起),然后調(diào)用test函數(shù),執(zhí)行其中的代碼。在test函數(shù)中的代碼執(zhí)行期間,我們點(diǎn)擊了p節(jié)點(diǎn),瀏覽器事件觸發(fā)線程檢測(cè)到這個(gè)事件,就把這個(gè)事件放在了任務(wù)隊(duì)列中,以便主線程上的任務(wù)(這里是test函數(shù))執(zhí)行完后,檢查任務(wù)隊(duì)列時(shí)發(fā)現(xiàn)這個(gè)事件并執(zhí)行相應(yīng)的回調(diào)函數(shù)。如果我們多次點(diǎn)擊,這些多次觸發(fā)的事件就按觸發(fā)時(shí)間的先后在任務(wù)隊(duì)列中排隊(duì)(可以再給另外一個(gè)元素添加點(diǎn)擊事件,交替點(diǎn)擊不同的元素來(lái)驗(yàn)證)。
下面是總結(jié)的任務(wù)的運(yùn)行機(jī)制:
異步執(zhí)行的運(yùn)行機(jī)制如下。(同步執(zhí)行也是如此,因?yàn)樗梢员灰暈闆](méi)有異步任務(wù)的異步執(zhí)行。)
1、所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。
2、主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件。
3、一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開(kāi)始執(zhí)行。
4、主線程不斷重復(fù)上面的第三步。
三、事件和回調(diào)函數(shù)
我們?cè)诮oDOM元素指定事件時(shí),都會(huì)指定一個(gè)回調(diào)函數(shù),以便事件真的發(fā)生時(shí)執(zhí)行相應(yīng)的代碼。
主線程中事件的回調(diào)函數(shù)會(huì)被掛起,如果任務(wù)隊(duì)列中有正在排隊(duì)的相應(yīng)的事件,當(dāng)主線程檢測(cè)到時(shí)就會(huì)執(zhí)行相應(yīng)的回調(diào)函數(shù)。我們也可以說(shuō)主線程執(zhí)行異步任務(wù),就是在執(zhí)行相應(yīng)的回調(diào)函數(shù)。
四、事件循環(huán)
主線程檢查任務(wù)隊(duì)列中事件的過(guò)程是循環(huán)不斷的,因此我們可以畫(huà)一個(gè)事件循環(huán)的圖:
上圖中主線程產(chǎn)生堆和執(zhí)行棧,棧中的任務(wù)執(zhí)行完畢后,主線程檢查任務(wù)隊(duì)列中由其他線程傳入的發(fā)生過(guò)的事件,檢測(cè)到排在最前面的事件,就從掛起的回調(diào)函數(shù)中找出與該事件對(duì)應(yīng)的回調(diào)函數(shù),然后在執(zhí)行棧中執(zhí)行,這個(gè)過(guò)程一直重復(fù)。
五、定時(shí)器
結(jié)合以上知識(shí),下面探討JavaScript中的定時(shí)器:setTimeout()和setInterval()。
setTimeout(func, t)是超時(shí)調(diào)用,間隔一段時(shí)間后調(diào)用函數(shù)。這個(gè)過(guò)程在事件循環(huán)中的過(guò)程如下(我的理解):
主線程執(zhí)行完setTimeout(func, t);語(yǔ)句后,把回調(diào)函數(shù)func掛起,同時(shí)定時(shí)器線程開(kāi)始計(jì)時(shí),當(dāng)計(jì)時(shí)等于t時(shí),相當(dāng)于發(fā)生了一個(gè)事件,這個(gè)事件傳入任務(wù)隊(duì)列(結(jié)束計(jì)時(shí),只計(jì)時(shí)一次),當(dāng)主線程中的任務(wù)執(zhí)行完后,主線程檢查任務(wù)隊(duì)列發(fā)現(xiàn)了這個(gè)事件,就執(zhí)行掛起的回調(diào)函數(shù)func。我們指定的時(shí)間間隔t只是參考值,真正的時(shí)間間隔取決于執(zhí)行完setTimeout(func, t);語(yǔ)句后的代碼所花費(fèi)的時(shí)間,而且是只大不小。(即使我們把t設(shè)為0,也要經(jīng)歷這個(gè)過(guò)程)。
setInterval(func, t)是間歇調(diào)用,每隔一段時(shí)間后調(diào)用函數(shù)。這個(gè)過(guò)程在事件循環(huán)中的過(guò)程與上面的類似,但又有所不同。
setTimeout()是經(jīng)過(guò)時(shí)間t后定時(shí)器線程在任務(wù)隊(duì)列中添加一個(gè)事件(注意是一個(gè)),而setInterval()是每經(jīng)過(guò)時(shí)間t(一直在計(jì)時(shí),除非清除間歇調(diào)用)后定時(shí)器線程在任務(wù)隊(duì)列中添加一個(gè)事件,而不管之前添加的事件有沒(méi)有被主線程檢測(cè)到并執(zhí)行。(實(shí)際上瀏覽器是比較智能的,瀏覽器在處理setInterval的時(shí)候,如果發(fā)現(xiàn)已任務(wù)隊(duì)列中已經(jīng)有排隊(duì)的同一ID的setInterval的間歇調(diào)用事件,就直接把新來(lái)的事件 Kill 掉。也就是說(shuō)任務(wù)隊(duì)列中一次只能存在一個(gè)來(lái)自同一ID的間歇調(diào)用的事件。)
舉個(gè)例子,假如執(zhí)行完setInterval(func, t);后的代碼要花費(fèi)2t的時(shí)間,當(dāng)2t時(shí)間過(guò)后,主線程從任務(wù)隊(duì)列中檢測(cè)到定時(shí)器線程傳入的第一個(gè)間歇調(diào)用事件,func開(kāi)始執(zhí)行。當(dāng)?shù)谝淮蔚膄unc執(zhí)行完畢后,第二次的間歇調(diào)用事件早已傳入任務(wù)隊(duì)列,主線程馬上檢測(cè)到第二次的間歇調(diào)用事件,func函數(shù)又立即執(zhí)行。這種情況下func函數(shù)的兩次執(zhí)行是連續(xù)發(fā)生的,中間沒(méi)有時(shí)間間隔。
下面是個(gè)例子:
function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } function test2() { var d = new Date().valueOf(); //var e = d-a; console.log('我被調(diào)用的時(shí)刻是:'+d+'ms'); //alert(1); } setInterval(test2,3000); test();
結(jié)果:
為什么8.6秒過(guò)后沒(méi)有輸出兩個(gè)一樣的時(shí)刻,原因在上面的內(nèi)容中可以找到。
執(zhí)行例子中的for循環(huán)花費(fèi)了8601ms,在執(zhí)行for循環(huán)的過(guò)程中隊(duì)列中只有一個(gè)間歇調(diào)用事件在排隊(duì)(原因如上所述),當(dāng)8601ms過(guò)后,第一個(gè)間歇調(diào)用事件進(jìn)入主線程,對(duì)于這個(gè)例子來(lái)說(shuō)此時(shí)任務(wù)隊(duì)列空了,可以再次傳入間歇調(diào)用事件了,所以1477462632228ms這個(gè)時(shí)刻第二次間歇調(diào)用事件(實(shí)際上應(yīng)該是第三次)傳入任務(wù)隊(duì)列,由于主線程的執(zhí)行棧已經(jīng)空了,所以主線程立即把對(duì)應(yīng)的回調(diào)函數(shù)拿來(lái)執(zhí)行,第二次調(diào)用與第一次調(diào)用之間僅僅間隔了320ms(其實(shí)8601+320=8920,差不多就等于9秒了)。我們看到第三次調(diào)用已經(jīng)恢復(fù)正常了,因?yàn)榇藭r(shí)主線程中已經(jīng)沒(méi)有其他代碼了,只有一個(gè)任務(wù),就是隔一段時(shí)間執(zhí)行一次間歇調(diào)用的回調(diào)函數(shù)。
用setTimeout()實(shí)現(xiàn)間歇調(diào)用的例子:
function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } function test2(){ var d = new Date().valueOf(); console.log('我被調(diào)用的時(shí)刻是:'+d+'ms'); setTimeout(test2,3000); } setTimeout(test2,3000); test();
結(jié)果:
每?jī)纱握{(diào)用的時(shí)間間隔基本上是相同。想想為什么?
再看一個(gè)例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Flex布局練習(xí)</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div id="test"> </div> <script> var p = document.createElement('p'); p.style.width = '50px'; p.style.height = '50px'; p.style.border = '1px solid black'; document.body.appendChild(p); alert('ok'); </script> </body> </html>
這個(gè)例子的結(jié)果是提示框先彈出,然后黑色邊框的p元素才出現(xiàn)在頁(yè)面中。原因很簡(jiǎn)單,就一句話:
在JavaScript引擎運(yùn)行腳本期間,界面渲染線程都是處于掛起狀態(tài)的。也就是說(shuō)當(dāng)使用JavaScript對(duì)界面中的節(jié)點(diǎn)進(jìn)行操作時(shí),并不會(huì)立即體現(xiàn)出來(lái),要等到JavaScript引擎線程空閑時(shí),才會(huì)體現(xiàn)出來(lái)。
以上就是我對(duì)JavaScript定時(shí)機(jī)制的理解及總結(jié),如有錯(cuò)誤,希望看到的大神指正。也希望大家多多支持腳本之家。
相關(guān)文章
簡(jiǎn)介JavaScript中Math.cos()余弦方法的使用
這篇文章主要介紹了簡(jiǎn)介JavaScript中Math.cos()余弦方法的使用,是JS入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06JavaScript中使用concat()方法拼接字符串的教程
這篇文章主要介紹了JavaScript中使用concat()方法拼接字符串的教程,是JS入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06

javascript 內(nèi)置對(duì)象及常見(jiàn)API詳細(xì)介紹

javascript中判斷一個(gè)值是否在數(shù)組中并沒(méi)有直接使用

caller和callee的區(qū)別介紹及演示結(jié)果