16 setTimeout如何实现的

97 阅读5分钟

setTimeoutsetTimeout和XMLHttpRequestXMLHttpRequest这两个WebAPI来介绍事件循环的应用。

setTimeout定时器用来指定某个函数在多少毫秒之后执行。

它会返回一个整数,表示定时器的编号,同时你还可以通过该编号来取消这个定时器。

    function showName(){  
        console.log("say hello")  
    }
    var timerID = setTimeout(showName,200);
    // timerID 用这个编号可以取消定时器。clearTimeout.

浏览器怎么实现setTimeout

事件循环的几种事件类型 1.当接收到HTML文档数据,渲染引擎就会将“解析DOM”事件添加到消息队列中。 2.当用戶改变了Web⻚面的窗口大小,渲染引擎就会将“重新布局”的事件添加到消息队列中。 3.当触发了JavaScript引擎垃圾回收机制,渲染引擎会将“垃圾回收”任务添加到消息队列中。 4.同样,如果要执行一段异步JavaScript代码,也是需要将执行任务添加到消息队列中。

不过通过定时器设置回调函数有点特别,它们需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,所以为了保证回调函数能在指定时间内执行,你不能将定时器的回调函数直接添加到消息队列中。

在Chrome中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表包括了定时器和Chromium内部一些需要延迟执行的任务。所以当通过JavaScript创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中

Chromium cs.chromium.org/chromium/sr… 队列代码

DelayedIncomingQueue delayed_incoming_queue;
// 代码中延迟队列的代码在这

C++延迟队列的定义

struct DelayTask{          // 延迟结构体
    int64 id;             // 延迟编号
    CallBackFunction cbf; // 回调函数 
    int start_time;       // 定时器开始时间
    int delay_time;       // 延迟时间
};  
DelayTask timerTask;      // 延迟对象
timerTask.cbf = showName; // 指定延迟对象  
timerTask.start_time = getCurrentTime(); //获取当前时间  
timerTask.delay_time = 200;//设置延迟执行时间

// 创建好回调任务之后,再将该任务添加到延迟执行队列中

delayed_incoming_queue.push(timerTask); // 添加到延迟队列里面

    // 现在通过定时器发起的任务就被保存到延迟队列中了,那接下来我们再来看看消息循环系统是怎么触发延迟队列的。

接着上一章节的内容补充从延迟队列里面取出延迟任务。

void ProcessTimerTask(){  
    //从delayed_incoming_queue中取出已经到期的定时器任务  
    //依次执行这些任务  
}

TaskQueue task_queue;  
void ProcessTask();  
bool keep_running = true; // 是否退出主线程

void MainTherad(){  
for(;;){  
    //执行消息队列中的任务  
    Task task = task_queue.takeTask();  
    ProcessTask(task);  
    //执行延迟队列中的任务  
    ProcessDelayTask()
    if(!keep_running) //如果设置了退出标志,那么直接退出线程循环  
        break;  
    }  
}

从上面代码可以看出来,我们添加了一个ProcessDelayTask函数ProcessDelayTask函数,该函数是专⻔用来处理延迟执行任务的。这里我们要重点关注它的执行时机,在上段代码中,处理完消息队列中的一个任务之后,就开始执行ProcessDelayTask函数。ProcessDelayTask函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。

延迟队列 timerTask4 | timerTask3 | timerTask2 | timerTask1

 <-          <-      <-(到期了吗?到期了就执行)          <-(到期吗?到期了就执行)
clearTimeout(timerId)
// 相当于在这个延迟队列里面找到对应的ID直接删掉就行了在遍历就行了。
// delayed_incoming_queue 在这个队列找到直接干掉完事儿

使用setTimeout的一些注意事项
现在你应该知道在浏览器内部定时器是如何工作的了。不过在使用定时器的过程中,如果你不了解定时器的一些细节,那么很有可能掉进定时器的一些陷阱里。所以接下来,我们就来讲解一下在使用定时器过程中存在的那些陷阱。

1.如果当前任务执行时间过久,会影延迟到期定时器任务的执行

function bar() {  
    console.log('bar')  
}
function foo() {
    setTimeout(bar, 0);
    // 这里的任务执行的过久导致 上面延迟队列的任务被推迟了虽然是0,佐证了即时性很差劲。
    for (let i = 0; i < 999000; i++) {
        let i = 5+8+8+8
        console.log(i);
    }
}

你也可以打开Performance来看看其执行过程,如下图所示:

截屏2023-05-11 下午8.26.38.png

2.如果setTimeout存在嵌套调用,那么系统会设置最短时间间隔为4毫秒

function cb() { setTimeout(cb, 0); }  
setTimeout(cb, 0);

截屏2023-05-11 下午8.33.14.png

4毫秒延迟的代码 cs.chromium.org/chromium/sr…

上图中的竖线就是定时器的函数回调过程,从图中可以看出,前面五次调用的时间间隔比较小,嵌套调用超过五次以上,后面每次的调用最小时间间隔是4毫秒。之所以出现这样的情况,是因为在Chrome中,定时器被嵌套调用5次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于4毫秒,那么浏览器会将每次调用的时间间隔设置为4毫秒。

static const int kMaxTimerNestingLevel = 5;  
// Chromium uses a minimum timer interval of 4ms. We'd like to go  
// lower; however, there are poorly coded websites out there which do  
// create CPU-spinning loops. Using 4ms prevents the CPU from  
// spinning too busily and provides a balance between CPU spinning and  
// the smallest possible interval timer.  
static constexpr base::TimeDelta kMinimumInterval = base::TimeDelta::FromMilliseconds(4);

base::TimeDelta interval_milliseconds =  
std::max(base::TimeDelta::FromMilliseconds(1), interval);  
if (interval_milliseconds < kMinimumInterval &&  
nesting_level_ >= kMaxTimerNestingLevel)  
interval_milliseconds = kMinimumInterval;  
if (single_shot)  
StartOneShot(interval_milliseconds, FROM_HERE);  
else  
StartRepeating(interval_milliseconds, FROM_HERE);

3.未激活的⻚面,setTimeout执行最小间隔是1000毫秒 除了前面的4毫秒延迟,还有一个很容易被忽略的地方,那就是未被激活的⻚面中定时器最小值大于1000毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是1000毫秒,目的是为了优化后台⻚面的加载损耗以及降低耗电量。这一点你在使用定时器的时候要注意。

4.延迟器最大时间2^32次方如果超出就是即可执行。

function showName(){  
    console.log("test")  
}  
var timerID = setTimeout(showName,2147483648);//会被理解调用执行
// 小于这个值就木得问题了

5.使用setTimeout设置的回调函数中的this不符合直觉 如果被setTimeout推迟执行的回调函数是某个对象的方法,那么该方法中的this关键字将指向全局环境,而不是定义时所在的那个对象。这点在前面介绍this的时候也提过,你可以看下面这段代码的执行结果:

var name= 1;  
var MyObj = {  
    name: 2,  
    showName: function(){  
        console.log(this.name);  
    }  
}  
setTimeout(MyObj.showName,1000)

// 解决方案  2. bind 3、()=>{} 4、function(){MyObj.showName();}

总结 1、首先,为了支持定时器的实现,浏览器增加了延时队列。 2、其次,由于消息队列排队和一些系统级别的限制,通过setTimeout设置的回调任务并非总是可以实时地被执行,这样就不能满足一些实时性要求较高的需求了 3、最后,在定时器中使用过程中,还存在一些陷阱,需要你多加注意。

动画用requestAnimationFrame

使用requestAnimationFrame不需要设置具体的时间,由系统来决定回调函数的执行时间,requestAnimationFrame里面的回调函数是在⻚面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只 执行一次,内如果⻚面未激活的话,requestAnimationFrame也会停止渲染,这样既可以保证⻚面的流畅性,又能节省主线程执行函数的开销