第 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() // 关闭
// 区别
- 简单的代码实现
function foo(){
console.log("执行foo");
setTimeout(foo, 1000)
}
foo();
function goo(){
console.log("执行goo");
}
setInterval(goo, 1000);
- 之前学习setInterval,设置的间隔时间过短的时候,如果代码块里的代码并没有执行完也会重新开始执行?(我一直都是这么理解的),但使用setTimeout实现setInterval的这个效果就没这个问题,必然会把代码块中的代码运行玩后,然后才会再次调用该函数
- requestAnimationFrame的兼容写法应该就是类似我这个setTimeout实现setInterval的代码
- 求大佬指正轻喷,本人萌新- -
- 简单的代码实现
function foo(){ console.log("执行foo"); setTimeout(foo, 1000) } foo(); function goo(){ console.log("执行goo"); } setInterval(goo, 1000);
- 之前学习setInterval,设置的间隔时间过短的时候,如果代码块里的代码并没有执行完也会重新开始执行?(我一直都是这么理解的),但使用setTimeout实现setInterval的这个效果就没这个问题,必然会把代码块中的代码运行玩后,然后才会再次调用该函数
- requestAnimationFrame的兼容写法应该就是类似我这个setTimeout实现setInterval的代码
- 求大佬指正轻喷,本人萌新- -
只有你提到了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 的方式实现
- 先执行 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)
- 先设置 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,我们可以得到以下结论:
- 浏览器执行结果与 nodejs 没有差异
- setInterval 与 setInterval2 效果相近,说明 setInterval 是先将自身 handle 放入timer堆,再执行回调函数
- 先执行回调函数再设置 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
- 标准中,setInterval()如果前一次代码没有执行完,则会跳过此次代码的执行。
- 浏览器中,setInterval()如果前一次代码没有执行完,不会跳过此次代码,而是将其插在队列中,等待前一次代码执行完后立即执行。
- 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);
setInterval
- 标准中,setInterval()如果前一次代码没有执行完,则会跳过此次代码的执行。
- 浏览器中,setInterval()如果前一次代码没有执行完,不会跳过此次代码,而是将其插在队列中,等待前一次代码执行完后立即执行。
- 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
}
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)
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()
function mySetInterval() {
const [handler, duration] = arguments;
mySetInterval.timer = setTimeout(() => {
handler();
arguments.callee(...arguments)
}, duration);
}
mySetInterval.clearInterval = function() {
clearTimeout(mySetInterval.timer)
}