前端校招準備系列--js中的setTimeout究竟是什麼?

前言

  在刷筆試題的時候,常常會碰到setTimeout的問題,只知道這個是設置定時器;可是考察的重點通常是在一個方法中包含了定時器,定時器中的打印和方法中打印的執行順序問題,也許我說的有點兒難懂,下面就來看看setTimeout究竟是什麼吧!javascript


定時器的介紹

js中有哪些定時器?

週期定時器:setInterval()

介紹

  setInterval()是按照指定的週期來調用定時器,方法會不斷的調用定時器,直到使用clearInterval()中止或者窗口關閉html

語法

  setInterval(code,millisec,lang)java

  • code:要執行的方法體(必選)
  • millisec:每隔多少毫秒執行一次(單位是毫秒,若是設置爲5000,即每5秒執行一次)(必選)
  • lang:指使用的語言(可選)
實例

  經過setInterval實現時鐘效果es6

<html>
<body>

<input type="text" id="clock" />
<script type="text/javascript">
//每隔1秒執行一次clock方法
var int=self.setInterval("clock()",1000);
function clock()
{
var d=new Date();
var t=d.toLocaleTimeString();
document.getElementById("clock").value=t;
}
</script>
<!-- 設置一個按鈕,點擊按鈕即中止定時器 -->
<button onclick="int=window.clearInterval(int)">中止</button>
</body>
</html>

  效果圖:面試

圖片描述

‘一次性’定時器:setTimeout()

介紹

  顧名思義,這個定時器只會執行一次,和setInterval()的區別就在這兒了,正是由於如此,setInterval()才須要使用clearInterval方法去取消定時器chrome

語法

  setTimeout(code,millisec,lang)    ps:每一個參數的含義和setInterval()的均相同segmentfault

實例

  點擊按鈕3秒後彈出「Hello」promise

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鳥教程(runoob.com)</title>
</head>
<body>

<p>點擊按鈕,在等待 3 秒後彈出 "Hello"。</p>
<button onclick="myFunction()">點我</button>

<script>
function myFunction()
{
    setTimeout(function(){alert("Hello")},3000);
}
</script>

</body>
</html>

  效果圖:
圖片描述瀏覽器

取消定時器

介紹

  使用計時器ID來取消計時器回調的發生,每一個計時器都會返回一個id,是爲了取消定時器的方法能夠獲取到相應的計數器。多線程

  • clearInterval(id)
  • clearTimeout(id)
實例
//設置超時調用
var timeoutId = setTimeout(function (){
    alert("hello World");
    },1000);
//取消掉用的代碼
clearTimeout(timeoutId);

setTimeout的執行順序究竟是怎樣的?

  咱們都知道,js是單線程語言,全部的多線程都是假象,都是單線程模擬出來的。瀏覽器是多進程的,而瀏覽器的內核(渲染進程)是多線程的。不理解這句話的能夠去看看這篇文章
  渲染進程中有一個js引擎線程,這個線程是用來處理javaScript腳本的(例如chrome的V8引擎),而咱們一直說的javaScript是單線程的就是由於這個。
  那麼問題來了,既然js是單線程的,那setTimeout的異步是怎麼實現的呢?js在解析腳本的時候,會將任務分爲兩大類,同步任務和異步任務,它們在解析時會進入不一樣的場所執行。

  • 同步任務:會進入主線程的執行棧,也就是js引擎線程管理的地方,按照順序執行
  • 異步任務:進入Event Table中,並註冊函數,當回調函數的條件知足時,就會將回調函數放進Event Queue中,也就是任務隊列中。

  當主線程中的任務執行完畢後,也就是執行棧爲空時,就會去任務隊列中看有沒有事件,若是有的話,就進入主線程執行,一直這樣循環下去,這就是事件循環機制了,能夠參照下面的圖理解一下:

圖片描述

  也許你對事件循環機制的過程仍是不太明白,那麼我再解釋清楚一點。例以下面這個例子:

console.log('start')
   setTimeout(function(){
     console.log('setTimeout')
   },5000)
   console.log('end')

執行過程:

  • 開始解析,遇到console.log,是同步任務,進入主線程,直接執行,打印start
  • 往下走,遇到setTimeout,是異步任務,進入Event Table,並註冊回調函數;
  • 再往下走,遇到console.log,直接執行,打印end
  • 5s後,將回調函數放進Event Queue,此時執行棧恰好爲空,主線程會去任務隊列中取出這個回調函數,執行,打印setTimeout

  ps:

  • 第1,3步都是js引擎線程乾的事情,主線程執行任務;
  • 第2步是渲染進程中的事件觸發線程(專門管理任務隊列的)管理;
  • 第4步是定時器線程控制的(也就是setTiemout和setInterval所在的進程),定時器線程專門用來控制何時將回調函數放進任務隊列。

  若是看懂了上面的例子,就知道其實setTimeout的第二個參數其實並不能準確的控制多少秒後執行裏面的函數,而是控制多少秒後將這個函數放進任務隊列中;這樣也就一樣能夠解釋,爲何有時候明明設置的是2秒以後執行,卻要等不止2秒(由於頗有可能定時線程將回調函數放進任務隊列後,主線程還在執行執行棧中的任務,須要執行棧中的任務所有執行完後纔會去任務隊列中取任務)。
  這樣就會引起一個問題,咱們知道setInterval是隔必定的時間執行一次,如今理解了原理後,就知道實際上是隔必定的時間定時器線程將回調函數放進任務隊列中。若是已經將回調函數放進任務隊列,可是主線程正在執行一個很是耗時的任務,當這個任務執行完畢後,主線程去任務隊列中取任務,這個時候,就會出現連續執行的狀況,也就是說setInterval至關於失效了。

setTimeout基礎篇

  這一部分主要是針對在事件循環機制中setTimeout調順序進行舉例子,若是可以輕鬆的將例子看懂,就說明你是真的懂了事件循環機制的一部分,爲何說是一部分呢,由於還有一個宏任務和微任務的知識點尚未涉及到,後面的進階篇就會涉及到啦!

例1

console.log('start')
  setTimeout(function(){
    console.log('setTimeout')
  },0)
  console.log('end')

打印結果:(若是前面看懂了的同窗應該就會明白爲何)

圖片描述

分析:其實和上面那個例子時同樣的,只是這個0會給咱們一種會當即執行的假象,這個0是說明定時器線程會當即將回調函數放進任務隊列而已,主線程仍是會將執行棧中的兩個同步任務執行完成後再去任務隊列中取任務,因此執行順序和這裏的秒數無關。並且即便執行棧爲空,也不會0秒就執行,由於HTML的標準規定,setTimeout不超過4ms按照4ms來計算。

例2

console.log('start')
  setTimeout(function(){
    console.log('setTimeout')
  }(),0)
  console.log('end')

打印結果:(仔細對比與例1的區別)
圖片描述

分析:細心的同窗會發現,我將回調函數改爲了當即執行函數,就改變了執行的順序。首先咱們須要明確的是setTimeout的第一個參數是指函數的返回值,這裏回調函數爲當即執行函數時,返回值就是undefined了,因此會直接執行當即執行函數,也就是當即打印setTimeout,而真正的setTimeout函數就至關於沒起做用。

例3

setTimeout(() => {
   console.log('setTimeout')
},3000)

sleep(10000000)//僞代碼,表示這個函數要執行好久好久

打印結果:
這個結果不說也知道,確定會打印出setTimeout的,可是重點卻不在這兒~
重點在於,這個setTimeout是隔好久好久打印出來的,遠遠超過了3秒,這個例子也是很明確的體現了js的事件循環機制。

setTimeout進階篇

  這一部分相對於基礎篇,加上了做用域以及其餘也是比較難以理解的東西,可能還須要補充一些其餘知識才會明白,我會盡可能講清楚,也會把我看的參考文章放在下面。
  受到一篇文章的啓發,咱們以按部就班的方式來闡述

難度:O

問題:如下代碼輸出的是什麼?

for(var i = 0;i < 5;i++){
    console.log(i)
  }

答案:沒錯,你沒有看錯,就是一個簡單的循環,就像你想的那樣,連續輸出0,1,2,3,4

難度:OO

問題:如下代碼輸出的是什麼?若是把時間改成1000*i輸出的又是什麼?

for(var i = 0;i < 5;i++){
    setTimeout(function(){
      console.log(i)
    },1000)
  }

答案:
  時間爲1000時,1秒後會連續輸出5個5;時間爲1000*i時,會每隔一秒輸出一個5,一共5個5
分析:
  由上面的事件循環機制咱們知道,setTimeout是異步事件,會放在事件隊列中等着主線程來執行,這個時候for循環中的i已經變成了5,因爲定時器線程是在1秒後直接將5個setTimeout事件放進事件隊列中,因此主線程在執行的時候就沒有間隔了;當時間乘上一個i時,定時器會隔1秒將setTimeout事件放入隊列,就會出現每隔一秒輸出一個5的狀況。

難度:OOO

問題:若是想輸出0,1,2,3,4應該怎麼改?
分析:
  出現上一題的狀況主要是由於在setTimeout的回調函數中並無保存每次循環i的值,最後執行的時候,獲得的i就是最後更新的i了(即爲5),因此要解決這個問題,思路是要在回調函數中保存每次for循環中的i值。

解決方案1:使用es6中let代替var
分析:let是es6中新增的內容,做用和var同樣,都是用來定義變量,可是最大的差異就是let會造成塊級做用域,在本例中,就是每次循環,都會產生一個做用域,在該做用域中的變量是一個固定值,下次i變化時不會對這個i產生影響,也就是達到了咱們的目標。

for(let i = 0;i < 5;i++){
    setTimeout(function(){
      console.log(i)
    },1000*i)
  }

解決方案2:使用閉包
分析:就是直接在setTimeout函數的外面套一層當即執行函數,並將i值做爲參數傳到匿名函數中(這裏的匿名函數也能夠是命名函數),而後因爲setTimeout中回調函數用到了匿名函數中的i,就會造成閉包。

for(var i = 0;i < 5;i++){
    (function(i){
      setTimeout(function(){
        console.log(i)
      }, 1000 * i)
    }) (i) 
  }

延伸:將代碼變成下面這樣會輸出什麼?(去掉匿名函數中的i)
分析:這裏會輸出5個5,也就是閉包沒有起做用,根本緣由是i並無傳進去,打印的仍是最後的i

for (var i = 0; i < 5; i++) {
      (function () {
        setTimeout(function () {
          console.log(i)
        }, i * 1000)
      })(i);
    }

解決方案3:將回調函數改爲當即執行函數
分析:這個解決方案其實不是太好,若是要求是每隔1秒輸出一個數字,這個方法就不適用了;這個方法會立馬輸出0,1,2,3,4,緣由結合基礎篇應該就明白了

for (var i = 0; i < 5; i++) {
        setTimeout((function (i){
          console.log(i);
        })(i), i * 1000)
    }

難度:OOOO

  這一部分會涉及到promise,事件循環機制,宏任務和微任務的內容,算是比較難的部分了,若是以爲比較難看懂,最好先去補一下基礎知識,我這裏就簡單介紹一下。

promise對象

我這裏就不詳細講了,能夠看這篇文章

宏任務和微任務

  • 宏任務:能夠理解成將代碼塊走一遍的過程,setTimeout和promise都是宏任務,如今不理解不要緊,後面會經過例子幫助理解
  • 微任務:是在宏任務執行完成以後執行的,也是有相應的微任務隊列存放微任務,好比promise中的then就是微任務

圖片描述

問題:如下代碼輸出的是什麼?

setTimeout(function () {
      console.log(1)
    }, 0);
    new Promise(function executor(resolve) {
      console.log(2);
      for (var i = 0; i < 10000; i++) {
        i == 9999 && resolve();
      }
      console.log(3);
    }).then(function () {
      console.log(4);
    });
    console.log(5);

答案:(是否是很懵,爲何會是這樣,下面看個人分析你就知道了)

圖片描述

分析:

  1. 進入宏任務(從第一行到最後一行執行一遍的過程),碰到setTimeout,將setTimeout放進事件隊列中;
  2. 碰到promise,執行console,打印2
  3. 通過循環後,執行console,打印3
  4. 到了then,因爲then是微任務,會在宏任務執行完成後執行,放進微任務隊列;
  5. 遇到console,打印5
  6. 至此,第一次的宏任務執行完成,接下來執行微任務隊列中的then,打印4
  7. 如今執行棧中的任務都執行完了,如今就要去事件隊列中取事件,此時執行setTimeout這個宏任務,打印1

宏任務微任務與同步事件異步事件的關係:
  這些詞都是用來描述事件的,只是從不一樣的角度來描述,就像是胖子矮子與男生女生之間的聯繫。

總結

  關於setTimeout還有不少能夠去研究的東西,我這裏只是將我目前看到的相關內容進行總結,因爲涉及的內容過多,若是沒有相關內容的基礎可能會比較難看懂,我也是爲了這篇文章看了好多資料,這篇文章拖了大概一週才完工,有什麼問題,能夠留言告訴我呀!

  若是你以爲還不錯,就請給個贊吧~

參考文章

關於setTimeout的面試題
js事件執行機制
從瀏覽器進程到js線程的詳解
強烈推薦js進階系列

相關文章
相關標籤/搜索