0
点赞
收藏
分享

微信扫一扫

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?


说明

浏览器工作原理与实践专栏学习笔记

用法

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

function showName(){
console.log("凯小默")
}
var timerID = setTimeout(showName, 200);

浏览器怎么实现 setTimeout

渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。不过通过定时器设置回调函数有点特别,它们需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,所以为了保证回调函数能在指定时间内执行,不能将定时器的回调函数直接添加到消息队列中。

在规定时间内怎么执行定时器设置的回调事件?

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

参考资料:​​Chromium 中关于队列部分的源码​​

源码中延迟执行队列的定义:

;

  1. 当通过 JavaScript 调用 setTimeout 设置回调函数的时候,渲染进程将会创建一个回调任务

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;//设置延迟执行时间

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

delayed_incoming_queue.push(timerTask)

消息循环系统是怎么触发延迟队列的?

在消息循环的代码中加入执行延迟队列的代码

ProcessDelayTask 函数:专门用来处理延迟执行任务的,它会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务,等到期的任务执行完成之后,再继续下一个循环过程。

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;
}
}

设置一个定时器,JavaScript 引擎会返回一个定时器的 ID。

取消定时器:

clearTimeout(timer_id)

浏览器内部实现取消定时器的操作:从 ​​delayed_incoming_queue​​ 延迟队列中,通过 ID 查找到对应的任务,然后再将其从队列中删除掉就可以了。

使用 setTimeout 的注意事项

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

例子:

function bar() {
console.log('bar')
}
function foo() {
setTimeout(bar, 0);
for (let i = 0; i < 5000; i++) {
let i = 5+8+8+8
console.log(i)
}
}
foo()

长任务导致定时器被延后执行:

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?_定时器

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

例子:

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

循环嵌套调用 setTimeout:

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?_定时器_02

因为在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。

参考资料:​​Chromium 实现 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 毫秒

如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量

4. 延时执行时间有最大值

Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,那么相当于延时值被设置为 0 了,这导致定时器会被立即执行。

例子:

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?_消息队列_03

5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉

如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。

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

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?_定时器_04

第一种是将MyObj.showName放在匿名函数中执行:

//箭头函数
setTimeout(() => {
MyObj.showName()
}, 1000);
//或者function函数
setTimeout(function() {
MyObj.showName();
}, 1000)

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?_消息队列_05

第二种是使用 bind 方法

setTimeout(MyObj.showName.bind(MyObj), 1000)

浏览器原理 15 # WebAPI:setTimeout 是如何实现的?_回调函数_06


举报

相关推荐

0 条评论