说明
浏览器工作原理与实践专栏学习笔记
用法
setTimeout 定时器,用来指定某个函数在多少毫秒之后执行。
function showName(){
console.log("凯小默")
}
var timerID = setTimeout(showName, 200);
浏览器怎么实现 setTimeout
渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。不过通过定时器设置回调函数有点特别,它们需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,所以为了保证回调函数能在指定时间内执行,不能将定时器的回调函数直接添加到消息队列中。
在规定时间内怎么执行定时器设置的回调事件?
在 Chrome 中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。
参考资料:Chromium 中关于队列部分的源码
源码中延迟执行队列的定义:
DelayedIncomingQueue delayed_incoming_queue;
- 当通过 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;//设置延迟执行时间
- 创建好回调任务之后,再将该任务添加到延迟执行队列中
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()
长任务导致定时器被延后执行:
2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
例子:
function cb() { setTimeout(cb, 0); }
setTimeout(cb, 0);
循环嵌套调用 setTimeout:
因为在 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 毫秒
如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。
4. 延时执行时间有最大值
Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,那么相当于延时值被设置为 0 了,这导致定时器会被立即执行。
例子:
5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉
如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。
var name= 1;
var MyObj = {
name: 2,
showName: function(){
console.log(this.name);
}
}
setTimeout(MyObj.showName,1000)
第一种是将MyObj.showName放在匿名函数中执行:
//箭头函数
setTimeout(() => {
MyObj.showName()
}, 1000);
//或者function函数
setTimeout(function() {
MyObj.showName();
}, 1000)
第二种是使用 bind 方法
setTimeout(MyObj.showName.bind(MyObj), 1000)