setTimeout,前端工程師一定會打交道的一個函數.它看上去很是的簡單,樸實.有着一個很不平凡的名字--定時器.讓年少的我天真的覺得本身能夠操縱將來.殊不知樸實之中隱含着驚天大密.我還記得我第一次用這個函數的時候,我天真的覺得它就是js實現多線程的工具.當時用它實現了一個坦克大戰的小遊戲,玩兒不亦樂乎.但是隨着在前端這條路上越走越遠,對它理解開始產生了變化.它彷佛開始蒙上了面紗,時常有一些奇怪的表現讓我捉摸不透.終於,個人耐心耗盡,下定決心,要撕開它的面具,一探究竟.javascript
要說setTimeout的淵源,就得從它的官方定義提及.w3c是這麼定義的html
setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。前端
看到這樣一個說明,咱們明白了它就是一個定時器,咱們設定的函數就是一個"鬧鐘",時間到了它就會去執行.然而聰明的你不由有這樣一個疑問,若是是settimeout(fn,0)呢?按照定義的說明,它是否會立馬執行?實踐是檢驗真理的惟一標準,讓咱們來看看下面的實驗java
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script> alert(1); setTimeout("alert(2)", 0); alert(3); </script> </body> </html>
這是一個很簡單的實驗,若是settimeout(0)會當即執行,那麼這裏的執行結果就應該是1->2>3 . 然而實際的結果倒是1->3->2. 這說明了settimeout(0)並非當即執行.同時讓咱們對settimeout的行爲感到很詭異.ajax
咱們先把上面的問題放一放.從js語言的設計上來看看是否能找到蛛絲馬跡.編程
咱們發現js語言設計的一個很重要的點是,js是沒有多線程的.js引擎的執行是單線程執行.這個特性曾經困擾我好久,我想不明白既然js是單線程的,那麼是誰來爲定時器計時的?是誰來發送ajax請求的?我陷入了一個盲區.即將js等同於瀏覽器.咱們習慣了在瀏覽器裏面執行代碼,卻忽略了瀏覽器自己.js引擎是單線程的,但是瀏覽器卻能夠是多線程的,js引擎只是瀏覽器的一個線程而已.定時器計時,網絡請求,瀏覽器渲染等等.都是由不一樣的線程去完成的. 口說無憑,我們依然看一個例子瀏覽器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> <script> var isEnd = true; window.setTimeout(function () { isEnd = false;//1s後,改變isEnd的值 }, 1000); while (isEnd); alert('end'); </script> </html>
isEnd默認是true的,在while中是死循環的.最後的alert是不會執行的. 我添加了一個定時器,1秒後將isEnd改成false. 若是說js引擎是多線程的,那麼在1秒後,alert就會被執行.然而實際狀況是,頁面會永遠死循環下去.alert並無執行.這很好的證實了,settimeout並不能做爲多線程使用.js引擎執行是單線程的.網絡
從上面的實驗中,咱們更加疑惑了,settimeout到底作了什麼事情呢?前端工程師
原來仍是得從js語言的設計上尋找答案.多線程
js引擎單線程執行的,它是基於事件驅動的語言.它的執行順序是遵循一個叫作事件隊列的機制.從圖中咱們能夠看出,瀏覽器有各類各樣的線程,好比事件觸發器,網絡請求,定時器等等.線程的聯繫都是基於事件的.js引擎處理到與其餘線程相關的代碼,就會分發給其餘線程,他們處理完以後,須要js引擎計算時就是在事件隊列裏面添加一個任務. 這個過程當中,js並不會阻塞代碼等待其餘線程執行完畢,並且其餘線程執行完畢後添加事件任務告訴js引擎執行相關操做.這就是js的異步編程模型.
如此咱們再回過頭來看settimeout(0)就會恍然大悟.js代碼執行到這裏時,會開啓一個定時器線程,而後繼續執行下面的代碼.該線程會在指定時間後往事件隊列裏面插入一個任務.由此可知settimeout(0)裏面的操做會放在全部主線程任務以後. 這也就解釋了爲何第一個實驗結果是1->3-2 .
因而可知官方對於settimeout的定義是有迷惑性的.應該給一個新的定義:
在指定時間內, 將任務放入事件隊列,等待js引擎空閒後被執行.
談到這裏,就不得不說瀏覽器的另一個引擎---GUI渲染引擎. 在js中渲染操做也是異步的.好比dom操做的代碼會在事件隊列中生成一個任務,js執行到這個任務時就會去調用GUI引擎渲染.
js語言設定js引擎與GUI引擎是互斥的,也就是說GUI引擎在渲染時會阻塞js引擎計算.緣由很簡單,若是在GUI渲染的時候,js改變了dom,那麼就會形成渲染不一樣步. 咱們須要深入理解js引擎與GUI引擎的關係,由於這與咱們平時開發息息相關,咱們時長會遇到一些很奇葩的渲染問題.看這個例子
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td><div id='status'>Not Calculating yet.</div></td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td><div id='status_ok'>Not Calculating yet.</div></td> </tr> </table> <script> function long_running(status_div) { var result = 0; for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } document.querySelector(status_div).innerHTML = 'calclation done' ; } document.querySelector('#do').onclick = function () { document.querySelector('#status').innerHTML = 'calculating....'; long_running('#status'); }; document.querySelector('#do_ok').onclick = function () { document.querySelector('#status_ok').innerHTML = 'calculating....'; window.setTimeout(function (){ long_running('#status_ok') }, 0); }; </script> </body> </html>
咱們但願能看到計算的每個過程,咱們在程序開始,計算,結束時,都執行了一個dom操做,插入了表明當前狀態的字符串,Not Calculating yet.和calculating....和calclation done.計算中是一個耗時的3重for循環. 在沒有使用settimeout的時候,執行結果是由Not Calculating yet 直接跳到了calclation done.這顯然不是咱們但願的.而形成這樣結果的緣由正是js的事件循環單線程機制.dom操做是異步的,for循環計算是同步的.異步操做都會被延遲到同步計算以後執行.也就是代碼的執行順序變了.calculating....和calclation done的dom操做都被放到事件隊列後面並且緊跟在一塊兒,形成了丟幀.沒法實時的反應.這個例子也告訴了咱們,在須要實時反饋的操做,如渲染等,和其餘相關同步的代碼,要麼一塊兒同步,要麼一塊兒異步才能保證代碼的執行順序.在js中,就只能讓同步代碼也異步.即給for計算加上settimeout.
不一樣瀏覽器的實現狀況不一樣,HTML5定義的最小時間間隔是4毫秒. 使用settimeout(0)會使用瀏覽器支持的最小時間間隔.因此當咱們須要把一些操做放到下一幀處理的時候,咱們一般使用settimeout(0)來hack.
這個函數與settimeout很類似,但它是專門爲動畫而生的.settimeout常常被用來作動畫.咱們知道動畫達到60幀,用戶就沒法感知畫面間隔.每一幀大約16毫秒.而requestAnimationFrame的幀率恰好是這個頻率.除此以外相比於settimeout,還有如下的一些優勢: