Advanced-Frontend / Daily-Interview-Question

我是依扬(木易杨),公众号「高级前端进阶」作者,每天搞定一道前端大厂面试题,祝大家天天进步,一年后会看到不一样的自己。

Home Page:https://muyiy.cn/question/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

第 133 题:用 setTimeout 实现 setInterval,阐述实现的效果与setInterval的差异

impeiran opened this issue · comments

当中应该涉及setInterval的特性,node环境与浏览器的又会作何表现

function timeout(fn,time){ var timer; if(timer){ clearTimeout(timer) } return function() { timer = setTimeout(function(){ fn(); timeout(fn,time)() },time); } } function interval (fn,time) { timeout(fn,time)() }

function mySetInterval() {
  var args = arguments
  var timer = setTimeout(() => {
    args[0]()
    args.callee(...args)
  }, args[1])
  return timer
}

var timer = mySetInterval(() => {
  console.log(111)
}, 1000)

// clearInterval 清除计时器的方法还不知道怎么实现

受下面那位老哥@weiweixuan的启发 ,改了下代码,实现了清理功能

    function mySetInterval() {
        mySetInterval.timer = setTimeout(() => {
            arguments[0]()
            mySetInterval(...arguments)
        }, arguments[1])
    }

    mySetInterval.clear = function() {
        clearTimeout(mySetInterval.timer)
    }

    mySetInterval(() => {
        console.log(11111)
    }, 1000)

    setTimeout(() => {
        // 5s 后清理
        mySetInterval.clear()
    }, 5000)
   const mySetInterval = (fn,duration)=>{
        const timer = {}
        function timeout(fn,duration){
            return setTimeout(()=>{
                fn();
                timer.timeout = timeout(fn,duration)
            },duration)
        }
        timer.timeout = timeout(fn,duration)
        return timer
    }
    const clearMySetInterval = (timer)=>{
        clearTimeout(timer.timeout)
    }
    //test
    console.time('timer')
    console.time('interval')
    const timer = mySetInterval(()=>console.timeLog('timer'),300)
    const interval = setInterval(()=>console.timeLog('interval'),300)
    setTimeout(()=>{
        clearMySetInterval(timer)
        clearInterval(interval)
    },3000)

自己实现的的setTimerout回调版只能打印9次,会越来越慢。原版setInterval能打印10次

    var timer = null;
    var a = function(t){
        console.log(new Date().valueOf())
        timer= setTimeout(a,t)

    }
    a.close=function(){
        clearTimeout(timer)
    }
    a(1000) // 模拟一秒一次
   a.close() // 关闭

// 区别

commented

code2

commented

带清理功能的

code4

  1. 简单的代码实现
    function foo(){
        console.log("执行foo");
        setTimeout(foo, 1000)
    }
    foo();
    function goo(){
        console.log("执行goo");
    }
    setInterval(goo, 1000);
  1. 之前学习setInterval,设置的间隔时间过短的时候,如果代码块里的代码并没有执行完也会重新开始执行?(我一直都是这么理解的),但使用setTimeout实现setInterval的这个效果就没这个问题,必然会把代码块中的代码运行玩后,然后才会再次调用该函数
  2. requestAnimationFrame的兼容写法应该就是类似我这个setTimeout实现setInterval的代码
  3. 求大佬指正轻喷,本人萌新- -
  1. 简单的代码实现
    function foo(){
        console.log("执行foo");
        setTimeout(foo, 1000)
    }
    foo();
    function goo(){
        console.log("执行goo");
    }
    setInterval(goo, 1000);
  1. 之前学习setInterval,设置的间隔时间过短的时候,如果代码块里的代码并没有执行完也会重新开始执行?(我一直都是这么理解的),但使用setTimeout实现setInterval的这个效果就没这个问题,必然会把代码块中的代码运行玩后,然后才会再次调用该函数
  2. requestAnimationFrame的兼容写法应该就是类似我这个setTimeout实现setInterval的代码
  3. 求大佬指正轻喷,本人萌新- -

只有你提到了setInterval的那个痛点..

大概实现了一下?返回值和原生略有不同使用了对象方便访问:

function myInterval(func, duration) {
  let tag = {
    flag: +new Date
  }
  const f = () => setTimeout(() => {
    if(tag.flag){
      func()
      f()
    }
  }, duration)
  f()
  return tag
}

function myClear(tag) {
  tag.flag = 0
}

let t = myInterval(() => console.log(1), 3000)

myClear(t)

timerFun();
function timerFun() {
console.log('1');
var timer = setTimeout(function() {
timerFun();
clearTimeout(timer)
}, 1000)
}

@hugeorange 这种实现与setInterval的差异呢?

function _setinterval(callback,time){
  let timer = {}
  function run(){
    clearTimeout(timer)
    timer = setTimeout(()=>{
      callback()
      run()
    },time)
  }
  run()
  return {
    clear(){
      clearTimeout(timer)
    }
  }
}

function _clearInterval(timer){
  timer.clear()
}

let callback = ()=>{
  console.count()
}

let timer = _setinterval(callback,2000)

setTimeout(function(){
  _clearInterval(timer)
},2000*10)

function mySetInterval() {
mySetInterval.id = setTimeout(() => {
arguments0
mySetInterval(...arguments)
}, arguments[1])
}

mySetInterval.clearInterval = function (intervalId) {
clearTimeout(intervalId)
}

mySetInterval( () => {
console.log('1')
}, 1000)

setTimeout(() => {
mySetInterval.clearInterval(mySetInterval.id)
}, 5000)

预备知识

在浏览器中,setInterval的方法定义为:

long setInterval(in any handler, in optional any timeout, in any... args);

可以看出,该方法返回的句柄是不变的 long 值,我们需要通过该句柄去取消定时器

另一个要注意的点是:该方法的执行上下文必须为 window,WorkerUtils ,或者 实现 WindowTimers interface的对象(这个目前不知道怎么实现)

interface WindowTimers {
  long setTimeout(in any handler, in optional any timeout, in any... args);
  void clearTimeout(in long handle);
  long setInterval(in any handler, in optional any timeout, in any... args);
  void clearInterval(in long handle);
};
Window implements WindowTimers;

注意clearInterval(lone handler) 会对 handler 做隐式类型转换,下文有用到该特性

而在node环境中, setInterval 返回的是一个 Timeout 对象,

clearInterval(object timer), 故 clearInterval 不会对其中的参数做隐式类型转换(https://github.com/nodejs/node/blob/master/lib/timers.js#L194)

setTimeout 模拟

setTimeout 模拟 setInterval(handler,?timeout,...args) ,有两种实现:

注意这里我们要返回一个 timer的引用,但是timer又只能是Int,只能采取重写 valueOf 的方式实现

  1. 先执行 fn 再 重新设置 setTimeout
function setInterval1 (handler,timeout,...args) {
  let isBrowser = typeof window !=='undefined'
  if(isBrowser && this!==window){
    throw 'TypeError: Illegal invocation'
  }
  let timer = {}
  if(isBrowser){
    // 浏览器上处理
    timer = {
      value:-1,
      valueOf: function (){
        return this.value
      }
    }
    let callback = ()=>{
      handler.apply(this,args)
      timer.value = setTimeout(callback,timeout)
    }
    timer.value = setTimeout(callback,timeout)
  } else {
    // nodejs的处理
    let callback = ()=>{
      handler.apply(this,args)
      Object.assign(timer,setTimeout(callback,timeout))
    }
    Object.assign(timer,setTimeout(callback,timeout))
  }
  return timer
}

测试用例:

// 基础功能:不断的打印3
setInterval1 ((a,b)=>console.log(a+b),1000,1,2) 
// 清除定时器: 打印10次3后停止
let t = setInterval1 ((a,b)=>console.log(a+b),1000,1,2)
setTimeout(()=>{
  window.clearInterval(t)
},10.5*1000)
// this:前两个均提示非法调用错误,最后一个可以成功调用 定时输出 undefined
// node 下都可以调用
let tmp = {
  a:1,
  test:setInterval
}
let tmp1 = {
  a:1,
  test:setInterval1
}
tmp.test(function(){
  console.log(this.a)
},1000)
tmp1.test(function(){
  console.log(this.a)
},1000)
tmp1.test.call(window,() =>{
  console.log(this.a)
},1000)
  1. 先设置 setTimeout 再执行 fn
function setInterval2 (handler,timeout,...args) {
  let isBrowser = typeof window !=='undefined'
  if(isBrowser && this!==window){
    throw 'TypeError: Illegal invocation'
  }
  let timer = {}
  if(isBrowser){
    // 浏览器上处理
    timer = {
      value:-1,
      valueOf: function (){
        return this.value
      }
    }
    let callback = ()=>{
      // 区别在这
      timer.value = setTimeout(callback,timeout)
      handler.apply(this,args)
    }
    timer.value = setTimeout(callback,timeout)
  } else {
    // nodejs的处理
    let callback = ()=>{
      // 区别在这
      Object.assign(timer,setTimeout(callback,timeout))
      handler.apply(this,args)
    }
    Object.assign(timer,setTimeout(callback,timeout))
  }
  return timer
}

setInterval 、 setInterval1 、 setInterva2 三者差异对比

先编写预处理函数

function setInterval1 (handler,timeout,...args) {
  let isBrowser = typeof window !=='undefined'
  if(isBrowser && this!==window){
    throw 'TypeError: Illegal invocation'
  }
  let timer = {}
  if(isBrowser){
    // 浏览器上处理
    timer = {
      value:-1,
      valueOf: function (){
        return this.value
      }
    }
    let callback = ()=>{
      handler.apply(this,args)
      timer.value = setTimeout(callback,timeout)
    }
    timer.value = setTimeout(callback,timeout)
  } else {
    // nodejs的处理
    let callback = ()=>{
      handler.apply(this,args)
      Object.assign(timer,setTimeout(callback,timeout))
    }
    Object.assign(timer,setTimeout(callback,timeout))
  }
  return timer
}
function setInterval2 (handler,timeout,...args) {
  let isBrowser = typeof window !=='undefined'
  if(isBrowser && this!==window){
    throw 'TypeError: Illegal invocation'
  }
  let timer = {}
  if(isBrowser){
    // 浏览器上处理
    timer = {
      value:-1,
      valueOf: function (){
        return this.value
      }
    }
    let callback = ()=>{
      // 区别在这
      timer.value = setTimeout(callback,timeout)
      handler.apply(this,args)
    }
    timer.value = setTimeout(callback,timeout)
  } else {
    // nodejs的处理
    let callback = ()=>{
      // 区别在这
      Object.assign(timer,setTimeout(callback,timeout))
      handler.apply(this,args)
    }
    Object.assign(timer,setTimeout(callback,timeout))
  }
  return timer
}
// 同步处理函数
function syncHandler(ms) {
  let d = Date.now()
  while (Date.now() - d < ms) { }
}
// 异步处理函数
function asyncHandler(callback,ms){
  setTimeout(callback,ms)
}
let scope = typeof window !=='undefined'?window:global
// 主测试函数
function test(setInterval,count){
  return (handler,timeout,...args) => {
    let t = setInterval (handler,timeout,...args)
    setTimeout(()=>{
      scope.clearInterval(t)
    },(count+0.5)*timeout)
  }
}

1. handler 为同步处理函数

  • setInterval
var start = Date.now()
var icounter = 0
test(setInterval,5)(function(){
  var time = (Date.now() - start) / 1000
  console.log('setInterval=>次数:' + (++icounter) + '    所用时间:' + time.toFixed(3))
  syncHandler(100)
},1000)
/*
# chrome76
setInterval=>次数:1    所用时间:1.002
setInterval=>次数:2    所用时间:2.001
setInterval=>次数:3    所用时间:3.000
setInterval=>次数:4    所用时间:4.002
setInterval=>次数:5    所用时间:5.002
# node v10
setInterval=>次数:1    所用时间:1.003
setInterval=>次数:2    所用时间:2.004
setInterval=>次数:3    所用时间:3.005
setInterval=>次数:4    所用时间:4.005
setInterval=>次数:5    所用时间:5.005
*/
  • setInterval1
var start = Date.now()
var icounter = 0
test(setInterval1,5)(function(){
  var time = (Date.now() - start) / 1000
  console.log('setInterval1=>次数:' + (++icounter) + '    所用时间:' + time.toFixed(3))
  syncHandler(100)
},1000)
/*
# chrome76
setInterval1=>次数:1    所用时间:1.001
setInterval1=>次数:2    所用时间:2.103
setInterval1=>次数:3    所用时间:3.204
setInterval1=>次数:4    所用时间:4.305
setInterval1=>次数:5    所用时间:5.406
# node v10
setInterval1=>次数:1    所用时间:1.005
setInterval1=>次数:2    所用时间:2.121
setInterval1=>次数:3    所用时间:3.224
setInterval1=>次数:4    所用时间:4.324
setInterval1=>次数:5    所用时间:5.428
*/
  • setInterval2
var start = Date.now()
var icounter = 0
test(setInterval2,5)(function(){
  var time = (Date.now() - start) / 1000
  console.log('setInterval2=>次数:' + (++icounter) + '    所用时间:' + time.toFixed(3))
  syncHandler(100)
},1000)
/*
# chrome76
setInterval2=>次数:1    所用时间:1.001
setInterval2=>次数:2    所用时间:2.004
setInterval2=>次数:3    所用时间:3.005
setInterval2=>次数:4    所用时间:4.007
setInterval2=>次数:5    所用时间:5.008
# node v10
setInterval2=>次数:1    所用时间:1.006
setInterval2=>次数:2    所用时间:2.005
setInterval2=>次数:3    所用时间:3.005
setInterval2=>次数:4    所用时间:4.005
setInterval2=>次数:5    所用时间:5.005
*/

当 handler 为同步处理函数且执行时间小于 timeout,我们可以得到以下结论:

  1. 浏览器执行结果与 nodejs 没有差异
  2. setInterval 与 setInterval2 效果相近,说明 setInterval 是先将自身 handle 放入timer堆,再执行回调函数
  3. 先执行回调函数再设置 settimeout 会导致下次执行实现等待时间大于 timeout+同步代码执行时间

通过 node-libuv 源码可以证明

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
    heap_node = heap_min(timer_heap(loop));//取出timer堆上超时时间最小的元素
    if (heap_node == NULL)
      break;
    //根据上面的元素,计算出handle的地址,head_node结构体和container_of的结合非常巧妙,值得学习
    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)//如果最小的超时时间比循环运行的时间还要小,则表示没有到期的callback需要执行,此时退出timer阶段
      break;

    uv_timer_stop(handle);//将这个handle移除
    uv_timer_again(handle);//如果handle是repeat类型的,重新插入堆里
    handle->timer_cb(handle);//执行handle上的callback
  }
}
  setTimeout(dunction(){
    //处理代码
    setTimeout(arguments.callee,ms)
  },ms)

setInterval

  1. 标准中,setInterval()如果前一次代码没有执行完,则会跳过此次代码的执行。
  2. 浏览器中,setInterval()如果前一次代码没有执行完,不会跳过此次代码,而是将其插在队列中,等待前一次代码执行完后立即执行。
  3. Node中,setInterval()会严格按照间隔时间执行:一直等待完成上一次代码函数后,再经过时间间隔,才会进行下一次调用。

有测试过或者能提供相关文献么?
我这里测试 node v10和 chrome 76效果是一样的,即等待前一次代码执行完后立即执行

function mySetInterval(fn,time){
function inner(){
fn();
setTimeout(inner,time);
}
inner()
}

mySetInterval(() => {console.log("sss")}, 1000)

@francecil 我在浏览器 里试验的是 setInterval()如果前一次代码没有执行完,不会跳过此次代码,等待前一次代码执行完后执行。(是不是立即,还有待测试

        function customerTimeInterval(index) {
            for (let i = 0; i < 1000; i ++) {
                console.log(index);
            }
        }
        let i = 0;
        this.timer = setInterval(() => {
            i ++;
            if (i === 5) {
                this.timer && clearTimeout(this.timer);
            }
            customerTimeInterval(i);
        }, 1);

结果:
result

setInterval

  1. 标准中,setInterval()如果前一次代码没有执行完,则会跳过此次代码的执行。
  2. 浏览器中,setInterval()如果前一次代码没有执行完,不会跳过此次代码,而是将其插在队列中,等待前一次代码执行完后立即执行。
  3. Node中,setInterval()会严格按照间隔时间执行:一直等待完成上一次代码函数后,再经过时间间隔,才会进行下一次调用。

有测试过或者能提供相关文献么?
我这里测试 node v10和 chrome 76效果是一样的,即等待前一次代码执行完后立即执行
@francecil 我经测试后结果也跟你一样,抱歉没注意那篇文献的日期,大意了,已删除issue comment

 setTimeout(function(){
            //todo
            setTimeout(arguments.callee,time)
        },time)
let n=0
let id = setInterval(() => {
    n+=1
    console.log(n)
    if ( n>=10 ){
        window.clearInterval(id)
    }
})
  • setTimeout模拟
let n=0
let id = setTimeout(function fn(){
    n+=1
    console.log(n)
    if(n<10){
       setTimeout(fn,500)
    }
},500)
function mySetInterval() {
    let timeout;
    let args = arguments;

    (function run() {
        let self = this
        timeout = setTimeout(() => {
            args[0]();
            run.apply(self,[...args]);
        },args[1])
    })()
    return {
        clearInterval: function() {
            clearTimeout(timeout)
        }
    }
}
function clearMyInterval(time){
    time.clearInterval()
}
let test = mySetInterval(() => {
    console.log('111')
},2000)

setTimeout(() => {
    clearMyInterval(test)
},5000)
// 自启动,自关闭
function mySetInterval(intervalSign, cb, delay) {
    intervalSign ? mySetInterval.timer = setTimeout(() => {
        typeof cb === 'function' && cb();
        mySetInterval(intervalSign, cb, delay)
    }, delay) : clearTimeout(mySetInterval.timer);
    
}
// 传参为true开启定时器
mySetInterval(true, () => {
    console.log('setInterval log')
}, 1000);
// 传参为false,关闭定时器
mySetInterval(false);

我感觉这样的模拟实现会让定时器误差变大,每次调用setTimeout开启定时器也有一定的时间消耗.
看issue别的大佬的回复, 设置的间隔时间过短的时候,如果代码块里的代码并没有执行完也会重新开始执行,但使用setTimeout实现setInterval的这个效果就没这个问题,必然会把代码块中的代码运行完后,然后才会再次调用该函数.

function mySetInterval(fn, time) {
	let timer = null;
	// 定义内部函数
	function interval() {
		clearTimeout(timer)
		timer = setTimeout(()=> {
			fn()
			interval()   // fn执行完后再次执行interval
		}, time)
	}
	interval()
	// 取消函数
	interval.cancel = function() {
		clearTimeout(timer)
	}
	return interval;
}

// 使用方法
let timer = mySetInterval(() => {
	   console.log(11111)
}, 1000)

setTimeout(() => {
    // 5s 后清理
   timer.cancel()
 }, 5000)
!function timer() {
    return !function () {
        var timeid = setTimeout(function () {
            console.log("hahaha...")
            clearTimeout(timeid)
            timer()
        }, 1000)
    }()
}()

hahaha...(6个时候)
[Done] exited with code=1 in 7.024 seconds

hahaha...(19个时候)
[Done] exited with code=1 in 19.326 seconds

(不过这个测试不够准确.......)

      function setIntervalBySetTimeout (fn, timeout) {
        function initTimeout () {
          clearTimeout(fn._tid);
          fn._tid = setTimeout(() => {
            fn();
            initTimeout();
          }, timeout);
        }

        initTimeout();
      }

      const callback = () => {
        console.log(11111);
      };
      // 开始定时器
      setIntervalBySetTimeout(callback, 1000);

      // 5秒后关闭定时器
      setTimeout(() => {
        clearTimeout(callback._tid);
      }, 5000);

hugeorange 的写法有问题,函数应该允许反复调用,如果在函数上定义timer变量,当我多次调用方法时,timer永远是最后一次的,这会导致之前的定时器不能正确使用clear方法。

我的改写:

function simuInterval(fn, mills) {
  let timer = null;
  (function loop() {
    timer = setTimeout(() => {
      loop();
      fn();
    }, mills);
  })();

  return () => {
    clearTimeout(timer);
  };
}

另外很多人写的有点问题,如果我们在fn里面执行clear计时器操作,那么必须将重启timer操作前置,不然将导致clear失败;比如上面代码如果写成:

fn();
loop();

当我在fn内做了clear,但是loop紧接着会重启,这导致clear操作失败。

测试用例:

let clear = simuInterval(() => {
  console.log('should only run one time');
  clear();
}, 100);
function mySetInterval(fn, ms) {
    return {
        start_id: null,
        start: function() {
            var that = this;
            // that.clear();
            that.start_id = setTimeout(function() {
                fn();
                that.start();
            }, ms);
        },
        clear: function() {
            console.log('timer clear');
            var that = this;
            setTimeout(function() { // 解决再fn内部取消定时会失效的情况
                clearTimeout(that.start_id);
            }, 0)
        }
    };
}
var count = 0;
var t = mySetInterval(function() {
    console.log(count++);
    if (count > 10) {
        console.log('clear');
        t.clear();
    }
    // t.clear();
}, 100);

t.start();

模拟的setInterval 和原生setInterval 没有差异?

function _setInterval(fn, interval) {
    var timer = {}
    var timerID = 0
    if (typeof window !== "undefined") {
        timer = {
            valueOf() {
                return timerID
            }
        }
    }

    var oneTime = function () {
        if (typeof window !== "undefined") {
            timerID = setTimeout(() => {
                oneTime()
                fn()
            }, interval)
        } else {
            Object.assign(timer, setTimeout(() => {
                oneTime()
                fn()
            }, interval))
        }
    }
    oneTime()
    return timer;
}
setTimeout(() => {
    console.log('插入耗时计算get(8000000)')
    get(8000000)
}, 2000);

var nu = 0
var timer = _setInterval(function () {
    console.log('interval', nu)
    var data = new Date();
    var str = data.getMinutes() + ":" + data.getSeconds() + ":" + data.getMilliseconds();
    console.log(str);
    if (nu > 3) {
        console.log('clearInterval')
        clearInterval(timer)
    }
    nu++
    // get(8000000)
}, 1000)

// 耗时计算
function get(n) {
    var count = 0
    for (var i = 0; i <= n; i++) {
        var temp = String(i).match(/1/g)
        if (temp) {
            count += temp.length
        }
    }
    return count
}
commented
function mySetinterval(fn, delay = 300) {
	let timer = function () {
		setTimeout(() => {
			fn()
			timer()
		}, delay)
	}
	timer()
	return function stop() {
		timer = () => {}
	}
}

let stop = mySetinterval(() => {
	console.log(1)
}, 2000)

setTimeout(() => {
	stop()
}, 10 * 1000)

commented
function mySetInterval(cb, delay) {
  const timerRef = {};

  function genTimeout() {
    clearTimeout(timerRef.value);

    return setTimeout(() => {
      cb();
      timerRef.value = genTimeout();
    }, delay);
  }

  timerRef.value = genTimeout();

  return  timerRef;
}

function myClearInterval(timerRef) {
  clearTimeout(timerRef.value);
}
const myInterval = (cb, span) => {
  let isRun = true
  const func = async () => {
    while (isRun) {
      await new Promise(resolve => {
        setTimeout(() => {
          cb()
          resolve()
        }, span)
      })
    }
  }
  func()
  return () => {
    isRun = false
  }
}

const clearFunc = myInterval(() => {
  console.log('hello')
}, 2000)

setTimeout(() => {
  clearFunc()
}, 6000)

setInterval能够保证以固定频率向事件队列放入回调,setTimeout不能保证。两个都不能保证固定的回调执行频率,因为存在主线程阻塞的可能

差异就是假设js执行线程阻塞的话会使自己模拟的间隔增加,而setInterval在页面卡顿时依然会从计时器线程中创建任务增加至任务队列中

function myInterval(cb, time) {
    let id = setTimeout(() => {
      cb();
      myInterval(cb, time);
    }, time);
    myInterval.cancel = () => {
      clearTimeout(id);
    }
  }
function time() {
            let timer;
            timer =  setTimeout(() => {
                clearTimeout(timer)
                console.log(1);
                time()
            }, 1000);
        }
        time()
commented
function mySetInterval() {
    const [handler, duration] = arguments;

    mySetInterval.timer = setTimeout(() => {
        handler();
        arguments.callee(...arguments)
    }, duration);
}

mySetInterval.clearInterval = function() {
  clearTimeout(mySetInterval.timer)
}