setTimeout 與 Event Loop 淺析

来源:http://www.cnblogs.com/muge10/archive/2017/08/13/7349704.html
-Advertisement-
Play Games

先從一個小題目開始: 以下代碼的輸出結果是? 下麵還有加強版: // 2 function test2(value) { value = value || 'default 2'; console.log(value); } setTimeout(test2, 1000, 2.1); // T2-1 ...


先從一個小題目開始: 以下代碼的輸出結果是?

// 1
function test1 () {
  console.log(1)
};

setTimeout(test1, 1000);                // T1-1setTimeout(test1(), 1000);              // T1-2
setTimeout(console.log(1.1), 1000);     // T1-3

 下麵還有加強版:

// 2
function test2(value) {
  value = value || 'default 2';
  console.log(value);
}

setTimeout(test2, 1000, 2.1);           // T2-1
setTimeout(test2(), 1000, 2.2);         // T2-2
setTimeout(test2(2.3), 1000, 2.31);     // T2-3

// 3
function test3(value) {
  value = value || 'default 3';
  console.log(value);

  return test3;
}

setTimeout(test3, 1000, 3.1);           // T3-1
setTimeout(test3(), 1000, 3.2);         // T3-2
setTimeout(test3(3.3), 1000, 3.31)      // T3-3


// 4
for(var i = 0; i < 5; i++) {            // T4-1
  console.log(i);
}

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

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

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

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

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


// 5
function someLoop(){
  var tag = true;
  var temp = 0;

  setTimeout(function(){ 
    tag = !tag; 
    console.log(tag)
  }, 1000);

  while(tag) {
    temp++;
  }
}

someLoop();                           // T5-1
setTimeout 示例2

 

如果這些對你都是小菜一碟,咳咳,你可以自己回去玩了,這裡已經沒有啥對你有價值的東西了,我們他日再敘。

如果你對這些代碼有些疑惑,那可以坐下來一起探討一下人生。

 

先看第一段:

// 1
function test1 () {
  console.log(1)
};

setTimeout(test1, 1000);                // T1-1
setTimeout(test1(), 1000);              // T1-2
setTimeout(console.log(1.1), 1000);     // T1-3

 

T1-1 這裡是最常見的使用方法了, 這個就不用多說了,1s 以後輸出 1;

===我是分割線===

 T1-2 與 T1-1 的區別就是第一個參數不再是函數,而是函數調用。那是不是1s 以後再調用這個函數呢?在 chrome console 里看一下運行的結果

立即輸出了1,又輸出了33,1s 以後啥都沒有。

這裡有3個小問題:1是咋輸出的;33是咋輸出的;1s 以後啥都沒有又是咋回事。

先說33,註意最上邊 “// 1” 左邊有一個向右的箭頭,這代表你在 console 里輸入的代碼;33左邊有一個向左的箭頭,這個意思是代碼執行後返回的值;33上面兩個橫線中間的值沒有左箭頭也沒有右箭頭,是代碼里 console.log() 列印的結果。

這裡33代表著 setTimeout() 方法的返回值,返回的是一個計時器,可以用來以後取消定時用的,這個33就是定時器的 id。這裡對我們代碼的執行並沒有啥意義,下麵就忽略這個計時器的 id 了。

要想說清楚輸出1和1s 以後啥都沒有的問題,必須先說說 JavaScript 里的事件迴圈機制了。

setTimeout(fn, delay) 是在指定時間以後,將 fn 推入到事件迴圈的消息隊列當中去,然後 js 執行引擎跑完執行棧里的代碼以後,就立即從這個消息隊列里依次取出函數來執行。

這個將 fn 推入消息隊列的函數用 js 模擬類似這樣:

function pushToMessageQueue(fn){
  console.log(fn);
  if(fn && Object.prototype.toString.call(fn).toLowerCase() === '[object function]') {
    // add fn to Message Queue
    // ......
    
  }
}

// 我並沒有研究過瀏覽器底層代碼實現,如有錯誤,還請留言指出,謝謝。

執行 setTimeout(fn, delay) 的時候,底層調用這個方法,將 fn 作為參數傳遞進去。

於是,當我們執行 setTimeout(test1(), 1000) 的時候,底層實際上將 test1() 作為參數傳遞給 pushToMessageQueue(fn) 函數。

這實際上發生了兩件事情:

1,將 test1() 作為參數賦值給 fn;類似於這樣:

var fn = test1();

2,運行 pushToMessageQueue 函數邏輯。

 

在第一步里進行參數傳遞的時候,實際上是立即執行了 test1(),並將返回值賦值給fn。所以才會立即列印 1。test1 函數並沒有返回值,預設返回 undefined,於是 pushToMessageQueue 函數的實際參數是 undefined,於是就沒有任何東西被推進執行棧里,所以1s 以後就啥都沒有執行。

 

至此,T1-2的疑問得到解決。瞭解了這些,再理解接下里的幾種情況就容易許多。

===我是分割線===

 T1-3 與 T1-2 很像,不過一個是函數調用,一個是語句,但它們運行的過程都是類似的。先把 console.log(1.1) 賦值給 fn,於是立即列印出 1.1,fn 的值為 undefined,於是1s 以後啥都沒有執行。

 

第一段代碼輸出結果如下:(忽略計時器 id)

 ===我是分割線===

下麵看第二段:

// 2
function test2(value) {
  value = value || 'default 2';
  console.log(value);
}

setTimeout(test2, 1000, 2.1);           // T2-1
setTimeout(test2(), 1000, 2.2);         // T2-2
setTimeout(test2(2.3), 1000, 2.31);     // T2-3

 

T2-1 與 T1-1 的區別是多了一個參數 2.2,setTimeout(fn, delay) 常見的寫法就是兩個參數,一個是要執行的函數,一個是延遲時間,多餘的參數什麼意思呢?看一下 MDN 上的資料(中文資料在此

var timeoutID = scope.setTimeout(function[, delay, param1, param2, ...]);
var timeoutID = scope.setTimeout(function[, delay]);
var timeoutID = scope.setTimeout(code[, delay]);

這裡的 scope 預設是 window。

這家伙有3種用法:

第二種用法,就是最常見的用法;

第三種用法,第一個參數是一個字元串,執行的時候解析為 js 語句再執行。這個用法已經不推薦了,完全可以用第二種代替。

第一種用法,就是在第二種之上加了更多的參數,這些參數會在第一個參數函數 fn 執行的時候作為參數傳遞,即 fn(param1, param2, param3) 這樣。

 

於是 T2-1 意思就是 1s 以後執行 test2(2.1),結果就是1s 以後列印 2.1。

===我是分割線===

T2-2 與 T1-2 類似,立即執行 test2(),列印 default 2,由於 test2 函數並沒有返回值,1s 以後也不會有任何函數被推入消息隊列,多餘的參數也沒有意義。

===我是分割線===

T2-3 里 test2 帶了一個參數2.3,立即列印2.3,1s 以後啥都沒有。

 

第二段代碼輸出結果如下:

===我是分割線===

 第三段:

// 3
function test3(value) {
  value = value || 'default 3';
  console.log(value);

  return test3;
}

setTimeout(test3, 1000, 3.1);           // T3-1
setTimeout(test3(), 1000, 3.2);         // T3-2
setTimeout(test3(3.3), 1000, 3.31)      // T3-3

 

T3-1 比較好理解,1s 以後執行 test3,並傳入參數 3.1,實際執行的是 test3(3.1),所以1s 以後列印3.1;執行以後返回的 test3 沒有被任何變數接收。

===我是分割線===

T3-2 立即執行 test3(),立即列印 default 3,並將返回值 test3 推入消息隊列,1s 以後執行 test3(3.2),列印 3.2;

===我是分割線===

T3-3 立即執行 test3(3.3),立即列印 3.3,並將返回值 test3 推入消息隊列,1s 以後執行 test3(3.31),列印 3.31;

 

第三段代碼輸出結果如下:

===我是分割線===

 

第四段:

// 4
for(var i = 0; i < 5; i++) {            // T4-1
  console.log(i);
}

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

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

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

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

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

 

T4-1:

輸出 0 1 2 3 4

===我是分界線===

T4-2:

前面說到,setTimeout(fn, delay) 是在 delay 後將 fn 推入消息隊列,等主進程執行完執行棧中的代碼以後再執行執行消息隊列里的回調。這裡 for 迴圈就是執行棧中的代碼,setTimeout 第一個參數里的回調都在指定時機被推入消息隊列,迴圈完畢以後再執行。而此時的 i 已經變成了5,因此,列印結果是,立即列印一個5,然後接下來每秒再列印一個5。一共5個5。

5 (1s 後)5(2s 後)5 (3s 後)5 (4s 後)5

===我是分界線===

T4-3:

和上一個的區別是,回調里多了一個 i 參數。但在執行的時候並沒有實參傳遞給這個回調,因此 i 就是 undefined,輸出結果跟上一個類似,只是把 5 換成了 undefined。

undefined (1s 後)undefined(2s 後)undefined (3s 後)undefined (4s 後)undefined

===我是分界線===

T4-4:

迴圈體內部變成了一個立即執行函數,並把 i 作為參數傳遞。於是就利用閉包保存了對每一個 i 的引用。

0 (1s 後)1(2s 後)2 (3s 後)3 (4s 後)4

===我是分界線===

T4-5

與上一個的區別是,迴圈體內的立即執行函數沒有形參,於是傳遞的參數 i 就沒有參數來接收,於是匿名函數內部的 i 引用的其實都是外面迴圈里的 i。本質上和 T4-2並沒有區別,輸出也是一樣的。

5 (1s 後)5(2s 後)5 (3s 後)5 (4s 後)5

===我是分界線===

T4-6

setTimeout 的第一個參數變成了一個立即執行函數,這和 T1-2 是類似的,只不過外部多了一個迴圈而已。

0 1 2 3 4 (1s 後)無輸出

 

第四段 輸出如下

===我是分界線===

第五段

// 5
function someLoop(){
  var tag = true;
  var temp = 0;

  setTimeout(function(){ 
    tag = !tag; 
    console.log(tag)
  }, 1000);

  while(tag) {
    temp++;
  }
}

someLoop();                           // T5-1

 

根據上面的分析,這個應該很好理解。這段代碼是個死迴圈,永遠不會輸出。

結果:

 

參考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 超多經典 canvas 實例 普及:<canvas> 元素用於在網頁上繪製圖形。這是一個圖形容器,您可以控制其每一像素,必須使用腳本來繪製圖形。 註意:IE 8 以及更早的版本不支持 <canvas> 元素。 貼士:全部例子都分享在我的 GayHub - https://github.com/bxm ...
  • 一、Flex的簡介 Flex 是 Flexible Box 的縮寫,意為"彈性佈局",用來為盒狀模型提供最大的靈活性。用六個字概括彈性佈局就是簡單、方便、快速。 flex( flexible box:彈性佈局盒模型),是2009年w3c提出的一種可以簡潔、快速彈性佈局的屬性。主要思想是給予容器控制內 ...
  • 在前端項目開發中,px,em,以及rem都是頁面佈局常用的單位,雖然它們是長度單位,但是所含的意義不一樣。通過複習和查閱,總結了以下知識。 px像素(Pixel) 定義:相對長度單位。像素px是相對於顯示器屏幕解析度而言的。(引自CSS2.0手冊) 特點: 1:px代表的是像素,用它設置字體大小時, ...
  • 普通的路人,普通地瞧。簡單粗暴地分析了 Zepto 的 Ajax 模塊,分析時使用的是目前最新 1.2.0 版本。 ...
  • 本文將介紹如何親手來完成一個yeoman的generator,以實現快速構建最適合自己的項目。 本文將實現的generator起名為ngtimo,依照yeoman的命名規矩就叫做generator ngtimo,是筆者這周末一晚上加一上午參考著yeoman官方給出的幾個generator( "gen ...
  • 若是沒有對編輯器做任何配置直接添加圖片的話,顯示的html內容如下圖所示:它會顯示出原圖片尺寸 所以必須要對圖片的初始顯示尺寸做控制:ueditor文件中找到image.js文件 在image.js中找到如下圖所示: 在此處添加上所要想顯示的尺寸! http://ueditor.baidu.com/ ...
  • 假如面試回答js的運行機制時,你可能說出這麼一段話:“Javascript的事件分同步任務和非同步任務,遇到同步任務就放在執行棧中執行,而碰到非同步任務就放到任務隊列之中,等到執行棧執行完畢之後再去執行任務隊列之中的事件。”但你能說出背後的原因嗎? 先理解相關概念 線程與進程 進程:是系統資源分配和調度 ...
  • 剛學習angularJS,於是練習寫了一個類似於購物車的全選/取消全選的功能,主要實現的功能有: 1、勾選全選checkbox,列表數據全部被勾選,取消同理,用ng-model實現雙向綁定; 2、選中列表中的所有checkbox,全選也會被勾選;(這裡我想到的方法是給每一個對象增加checked欄位 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...