setTimeout與setInterval的區別和nodejs中的差別

博客文章地址javascript

setTimeout與setInterval

setTimeout setInterval 是咱們在 javaScript 中常常用到的定時器,setTimeout 方法用於在指定的毫秒數後調用函數或計算表達式,setInterval 可按照指定的週期不停的調用函數或計算表達式。java

可是當咱們要循環調用某任務時候,處了用 setInterval 指定週期外,咱們也能夠用函數中嵌套setTimeout 回掉本身來實現, 能夠看下面一段代碼node

// A
    function myTimeout() {
        doStuff()
        setTimeout(myTimeout, 1000)
    }
    myTimeout()
    
    // B
    function myTimeout() {
        doStuff()
    }
    myTimeout()
    setInterval(myTimeout, 1000)

上面A, B 兩個方法都是在循環執行 myTimeout 函數,但是它們之間有什麼不一樣呢。咱們大部分都知道這其實取決與 doStuff 所消耗的時間, 以下圖所示若是 doStuff 消耗時間很短(實際中大部分消耗時間都很短很難有所察覺),兩個方法效果近似git

setTimeout與setInterval的區別和nodejs中的差別

doStuff是一個很複雜的計算,須要消耗很長時間時候,咱們就能夠分析出A 方法(用setTimeout回掉)可以保障每一次任務結束到下一次任務開始的時間間隔爲咱們預期的值,可是B(setInterval)卻能保證任務開始到下一次任務開始之間的間隔爲咱們預期的值,(固然若是doStuff執行時間比咱們預期間隔還長,setInterval 還有可能會直接放棄某次任務,這種罕見狀況咱們暫不考慮)github

爲了感覺其中的差別,這裏定義一個模擬任務執行的函數segmentfault

function wait(time) {
        var start = Date.now()
        while(Date.now() - start < time){}
    }

wait什麼也沒作,可是卻能夠阻塞進程time毫秒的時間,而後咱們定義 doStuff,讓它每次執行阻塞進程500ms,並且能夠輸出間隔時間信息,以及本次執行結束到下次執行開始的時間間隔promise

function doStuff() {
        console.log('doStuff___start', new Date().getSeconds()) //每次輸出當前的秒數
            
        console.timeEnd('timeout') //每次輸出此次執行與上一次執行結束的時間間隔
        wait(500)
        console.time('timeout')
    }

而後咱們分別運行A, B兩種方法app

/*
     * A方法 setTimeout
     */
    // doStuff___start 36
    // timeout: 1002.865966796875ms
    // doStuff___start 37
    // timeout: 1004.380859375ms
    // doStuff___start 39
    // timeout: 1001.550048828125ms
    // doStuff___start 40
    // timeout: 1001.051025390625ms
    // doStuff___start 42
    // timeout: 1001.637939453125ms
    
    /*
     * B方法 setInterval
     */
    // doStuff___start 50
    // timeout: 500.412109375ms
    // doStuff___start 51
    // timeout: 500.51806640625ms
    // doStuff___start 52
    // timeout: 500.099853515625ms
    // doStuff___start 53
    // timeout: 499.873291015625ms
    // doStuff___start 54
    // timeout: 500.439697265625ms

能夠看到 A 方法(用setTimeout回掉),咱們保證了每次進程結束到下一次進程開始的間隔爲預期值,可是從每次進程開始的時間間隔(咱們這裏精確到了秒)是會改變的,而B 方法(setInterval)表現的和咱們預期的相同,正好與A相反。函數

nodejs中的差別

目前爲止因此的表現都合理,至少很符合預期。但是當我在 nodejs(v8.1.4) 中測試時候,卻發現無論我用 setTimeout 仍是 setInterval ,他們老是能表現出一樣的效果(都是上面A方法的效果【用setTimeout回掉】)。這一點讓我很困惑,通過一番探究,在 nodejs 關於 timers 的代碼中找到了答案。測試

nodejs 關於定時器的源碼在 node/lib/timer 文件中,進入就關於定時器的一些設計解釋,由於 node 是作服務端代碼,在內部 TCP, I/O.. 等大部分事件都會建立一個定時器,任什麼時候間均可能存在大量的定時器任務,因此設計一個高效的定時器是頗有必要的。

nodejs實現定時器也很巧妙, 爲了能夠輕鬆取消添加事件,nodejs使用了雙向鏈表將 timer 插入和移除操做複雜度下降,具體實如今 node/lib/internal/linkedlist.js 文件中, 鏈表缺點天然是去查找元素,可是node ,把同一個時間間隔的 timer 維護在同一個雙向鏈表中,這樣就不須要去查找,由於先插入的老是先執行,具體的分析能夠參考這篇文章 經過源碼解析 Node.js 中高效的 timer.

迴歸主題,在 nodejs 關於 timer 的源碼下,咱們能夠找到執行定時器的代碼

// setInterval 會返回 createRepeatTimeout 的返回值
    exports.setInterval = function(callback, repeat, arg1, arg2, arg3) {
      ...
      return createRepeatTimeout(callback, repeat, args);
    }
    
    //  createRepeatTimeout函數生成timer
    function createRepeatTimeout(callback, repeat, args) {
      repeat *= 1; // coalesce to number or NaN
      if (!(repeat >= 1 && repeat <= TIMEOUT_MAX))
        repeat = 1; // 這裏間隔若是小於1或者大於TIMEOUT_MAX(2^31-1)都會按照1計算
      var timer = new Timeout(repeat, callback, args);
      timer._repeat = repeat; // 追加了_repeat屬性表示要循環調用
      ...
    
      return timer;
    }
    

    // 函數回掉時,能夠看到執行時在ontimeout函數中
    function tryOnTimeout(timer, list) {
        ...
             try {
                ontimeout(timer);
                threw = false;
              } finally {
                if (timerAsyncId !== null) {
                  if (!threw)
                  ...
                }
        ...
    }
    
    // ontimeout執行
    function ontimeout(timer) {
      var args = timer._timerArgs;
      var callback = timer._onTimeout;
      if (typeof callback !== 'function')
        return promiseResolve(callback, args[0]);
      if (!args)
        timer._onTimeout();
      else {
        switch (args.length) {
          case 1:
            timer._onTimeout(args[0]);
            break;
          case 2:
            timer._onTimeout(args[0], args[1]);
            break;
          case 3:
            timer._onTimeout(args[0], args[1], args[2]);
            break;
          default:
            Function.prototype.apply.call(callback, timer, args);
        }
      }
      if (timer._repeat) // 追加timer
        rearm(timer);
    }

上面代碼分析,能夠看到追加循環調用是在 ontimeout 函數中,它裏面一大堆判斷參數個數的內容能夠無論,最後的if(timer._repeat) rearm(timer)判斷是否要循環調用,能夠看到它是在上面 timer._onTimeout 執行完以後纔去執行的。這和咱們開始寫的A方法(用setTimeout回掉)基本相似,至此在 nodejs 表現出的不一樣就能夠理解了。

issues , 關於這個問題也有不少討論,仍是有很多人想把它改會咱們熟悉的方式的

具體最後要怎樣仍是要看後面的版本修改了。

參考資料

相關文章
相關標籤/搜索