setTimeout vs setInterval

0. Credits to:

http://www.laruence.com/2009/09/23/1089.html

https://www.jeffjade.com/2015/08/03/2015-08-03-javascript-this/

https://www.youtube.com/watch?v=8aGhZQkoFbQ

https://www.zhihu.com/question/55050034

https://codepen.io/discountry/pen/XRXdVX?editors=1011

I. setTimeout

  1. After delay period, put cb to taskQueue of main thread, rather than executing cb right away!
    • only guarantees after exactdelay, cb is scheduled,
    • cannot guarantee cb being executed after exact delay, because maybe at that time, the main thread is still dealing a time-consuming task, which will postpone the execution of cb
  2. Good use case: recursively schedule delayed task, like a node data structure

    • deal with CPU intensive task, set aside period for UI update to be executed to avoid freezing
    • eg. break down large task into smaller ones, schedule smaller task execution every delay period
      • the bigger delay period is, the longer time left for UI
      • the time left for UI is exactly delay period
      • NOTE: don’t use setInterval, since the execution time for cb can be longer thandelay , resulting in there’s no time left for UI update. When cb finishes execution, the UI update and next cb will race to be executed in next event loop
  3. setTimeout(cb, 0):

      1. 等到Call Stack为空,即下一轮 Event Loop的时候 执行 cb
      1. Render QueueCallback Queue 在browser中的优先级高,他们共用一个JS V8 Engine。因此,callback中的cb会等到DOM被render之后在调用,这就是为什么使用 setTimeout(cb, 0)
      1. 这样,即便有很多setTimeout(cb, 0)短时间大量被调用,也会因为render queue的高优先级,使得它能够在两个cb 调用之间,插入到 JS V8 thread中update DOM,从而使页面不会freeze
    • Waiting for elements mounted to DOM by GUI thread, then switch to JS thread, do the manipulation by cb

    • Every function put inside setTimeout with 0 delay, will respect all the DOM task, and will scheduled after all DOM tasck

    • will execute right after all synchronous task is finished on stack by main thread

    • i.e. has to wait for next event loop come

    • Great Use Case:

      • set the order of event handler:

        • 1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          // 1. parent event_handler will be executed first, 
          // 2. then the target element
          let input = document.getElementById('myButton');

          input.onclick = function A() {
          setTimeout(function B() {
          input.value +=' input';
          }, 0)
          };

          document.body.onclick = function C() {
          input.value += ' body'
          };
      • tackle the race problem of keypress

        • 1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          /*
          keypress has two results:
          1. give input the value of pressed key [asynchronous - hardware]
          2. call the keypress event handler [asynchronous - event]

          BAD version:
          - cannot get the current key value when callback is called
          - can only get the value of previous pressed key value
          GOOD version:
          - use setTimeout(fn, 0)
          - callback will be executed after the key value is updated in DOM
          */

          <input type="text" id="input-box">

          // 1. BAD Version
          document.getElementById('input-box').onkeypress = function (event) {
          this.value = this.value.toUpperCase();
          }

          // 2. GOOD Version
          document.getElementById('input-box').onkeypress = function() {
          let self = this;
          setTimeout(function() {
          self.value = self.value.toUpperCase();
          }, 0);
          }

II. setInterval

  1. Every delay period, put cb to taskQueue of main thread if there is no cb waiting to be executed on the taskQueue; Otherwise, won’t schedule cb even delay period is achieved
  2. Note:

    1. when delay is up, even there exists an CPU-intensive task , setInterval won’t wait for the finish of the current task, instead put the cb directly to taskQueue at the exact time spot;
    2. But if the main thread is still executing task, the cb has to waits to be executed
    3. There is NO accumulated cb scheduled on event queue, even if there is a CPU intensive task blocking the periodic cb scheduling.
    4. setInterval timer has a check mechnism to ensure, there will be only one cb on event queue
  3. Common use case: CSS animation

    • Note1:

      • it only focus scheduling cb after delay, don’t care how long vacant time left
      • it’s possible that execution of cb is longer than delay, then next cb will be executed right after first cb finishes
    • Note2:

      • mustn’t use time variable as terminating condition to clear interval,

      • because some time it cost longer to execute cb, making some scheduled task won’t be executed

      • use other variable as terminating condition to clear interval

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        let div = document.getElementById('someDiv');
        let opacity = 1;
        let fader = setInterval(function() {
        opacity -= 0.1;
        if (opacity >= 0) {
        div.style.opacity = opacity;
        } else {
        clearInterval(fader); // use `opacity` as terminating condition
        }
        }, 100);

III. Common Problems

  1. this problem => because of window.setTimeout or window.setInterval

    • setTimeout( function(){ this.value=50 }, 1000 )

    • Problem:

      • example above, exist this in the callback function, if not bind actual object, this will refer window
      • setTimeout setInterval will change the reference of this to window
    • Why?

      • this always refers to the object who calls the function where this resides
      • when cb executed, it is executed like cb(), which omits the prefix object: window
    • How to solve?

      • use bind() to produce a new cb function carrying this reference to right object

      • use apply or call

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        a = "string belongs to window"    // note: not " let a = ... ";
        obj = {
        a: "string belongs to obj",
        printA: function(){ console.log(this.a) }
        }

        // 1. "string belongs to window", after 1s
        setTimeout(obj.printA, 1000)

        // 2. "string belongs to window", after 1s
        let temp = obj.printA
        setTimeout(temp, 1000)

        // 3. "string belongs to obj"
        setTimeout(function(){ obj.printA.apply(obj) }, 2000)

        // 4. "string belongs to obj"
        setTimeout(function(){ obj.printA() }, 3000)
  2. Implement a setInterval using setTimeout

    NOTE:

    1. Approach 1: the time_handle should be an object if declared inside the function, rather than an number, because the timeId is updated every delay period
    2. Approach 2: the time_handle should be a global var outside the function , because the timeId is updated every delay period

    3. 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      // ----------------Approach 1----------------
      function customSetInterval( cb, delay ){
      let time_handle = {id: null}
      function wrapper(){
      cb.apply(null)
      time_handle.id= setTimeout(wrapper, delay)
      }
      time_handle.id= setTimeout(wrapper, delay)
      return time_handle
      }
      // Test
      let i = 0
      function cb(){ i+=1; console.log(i) }
      let interval_handle = customSetInterval(cb, 1000) // print 1 , 2 , 3 , 4 , ...
      // clearTimeout(interval_handle.id) // use to clear interval


      // ----------------Approach 2----------------
      let time_handle = null
      function customSetInterval( cb, delay ){
      function wrapper(){
      cb.apply(null)
      time_handle= setTimeout(wrapper, delay)
      }
      time_handle= setTimeout(wrapper, delay)
      }
      // Test
      let i = 0
      function cb(){ i+=1; console.log(i) }
      customSetInterval(cb, 1000) // print 1 , 2 , 3 , 4 , ...
      // clearTimeout(time_handle) // use to clear interval
  3. how to handle encoding task for a file (CPU intensive)?

    • Problem: it might freeze the UI when encode the whole file

    • Solution:

      • Divide the file into several part, using a symbol, or using size

      • for each small part, just take over CPU, since it’s small task

      • between each encoding, leave time for UI interaction

      • Note: use setTimeout instead of setInterval

      • Since the execution time for cb can be longer thandelay , resulting in there’s no time left for UI update. When cb finishes execution, the UI update and next cb will race to be executed in next event loop

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        /*
        input: the file to be encoded
        output: the encoded goal file
        totalSize: the size of the input file
        encode: the function used to encode
        */

        function encodeWrapper( input, output, totalSize, encode ){
        let output = ""
        let accumSize = 0
        let i = 0
        let timeoutHandle = null
        /* delayed recursion
        i: ith step
        stepSize: how big to encod for a step
        */
        function encodeHelper(i, stepSize ){
        output += encode( input.substring(i*stepSize,stepSize) )
        if( i*stepSize > totalSize ) {
        clearTimeout(timeoutHandle)
        return output
        }
        timeoutHandle = setTimeout(encodeHelper(i+1, stepSize), 100)
        }
        return encodeHelper(0, 1024)
        }
  4. How to write a delayed recursion ?

    • Problem: Calculate the sum from 0 to n

    • Solution: f(n) = f(n-1) + n; f(0) = 0

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /*
    f(n) = f(n-1) + n
    f(0) = 0
    */

    function wrapper(n){
    let timeHandle = 0
    let accum = 0

    // recursive call itself every 1000ms
    function sum(n){
    timeHandle=setTimeout(function(){
    if (n === 0) {
    clearTimeout(timeHandle)
    alert(accum) // output the result
    return
    }
    accum += n
    sum(n-1)
    },1000)
    }
    sum(n)
    }
  1. Debouncify a function

    • Problem: Some Ajax functions can be frequently called using events, which will cause severe performance problem on backend

    • Solution:

      • we want to call Ajax function only after a period delay time (so that we can make sure the user is really waiting to for to make Ajax call, rather than in the middle of typing)
      • when there is another intention to call the Ajax function in the middle of last delay, we want to reschedule the ajax call after another delay
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      function callAjaxRightAway(){ 
      console.log("Ajax call starting, with arguments: " + [...arguments] )
      }


      function Debouncify( fn, delay ){
      let time_handle = null
      return function(){
      clearTimeout(time_handle)
      let args = arguments
      time_handle = setTimeout( function(){ fn.apply(null,args) }, delay)
      }
      }

      let delayedFun = Debouncify(callAjaxRightAway, 2000)

      delayedFun({arr:[1, 2, 3], heigh: 10 }) // will be ignored
      delayedFun(1,2) // will be ignored
      delayedFun("a", "b", "c") // "Ajax call starting, with arguments: a,b,c"