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来看看其执行过程,如下图所示:
2.如果setTimeout存在嵌套调用,那么系统会设置最短时间间隔为4毫秒
function cb() { setTimeout(cb, 0); }
setTimeout(cb, 0);
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也会停止渲染,这样既可以保证⻚面的流畅性,又能节省主线程执行函数的开销