setTimeout的实现

1浏览器页面是由消息队列和事件循环系统来实现的。

接下来将会通过setTimeout和XMLHttpRequest这两个WebAPI来介绍事件循环。

浏览器如何实现setTimeout?

渲染进程中所有运行在主线程上的任务都需要先添加在消息队列中,然后事件循环系统按照顺序执行消息队列中的任务。

典型事件的执行流程:

  1. 当接收到HTML文档数据,渲染引擎就会将"解析DOM"事件添加到消息队列中。
  2. 当用户改变了Web页面的窗口大小,渲染引擎就会将"重新布局"的事件添加到消息队列中。
  3. 当出发了Javascript引擎垃圾回收机制,渲染引擎就会将"垃圾回收"任务添加到消息队列中。
  4. 执行一段异步代码,也需要将执行任务添加到消息队列中。

当这些事件添加到消息队列中,事件循环系统就会按照消息队列中的顺序来执行事件。

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

Chrome浏览器中除了正常使用的消息队列外,还有另外一个消息队列,这个队列维护了需要延迟处理的任务列表,包括了定时器和chromium内部一些需要延迟执行的任务。所以创建一个定时器的时候,渲染进程就会将该定时器的回调函数添加到延时队列中。

JS 复制代码
class DelayedQueue {
    constructor() {
        this.queue = [];
    }

    // 添加任务到延迟队列
    addDelayedTask(task, delay) {
        const executionTime = Date.now() + delay;
        this.queue.push({ task, executionTime });
        this.sortQueue();
    }

    // 执行队列中已到期的任务
    executeExpiredTasks() {
        const currentTime = Date.now();
        while (this.queue.length > 0 && this.queue[0].executionTime <= currentTime) {
            const { task } = this.queue.shift();
            task();
        }
    }

    // 排序队列,确保队列按照执行时间递增排序
    sortQueue() {
        this.queue.sort((a, b) => a.executionTime - b.executionTime);
    }
}

// 创建延迟队列实例
const delayedQueue = new DelayedQueue();

// 模拟IO线程产生的任务
function produceTask() {
    setInterval(() => {
        const task = () => {
            console.log("执行IO线程产生的任务");
        };
        // 将任务加入延迟队列,并设置延迟时间为2000毫秒
        delayedQueue.addDelayedTask(task, 2000);
    }, 2000);
}

// 模拟渲染主进程的事件循环
function eventLoop() {
    setInterval(() => {
        // 执行延迟队列中已到期的任务
        delayedQueue.executeExpiredTasks();
    }, 1000);
}

// 启动IO线程模拟任务的产生
produceTask();

// 启动渲染主进程的事件循环
eventLoop();

这个例子中,我添加了一个名为 DelayedQueue 的类,用于维护一个延迟队列。每当需要添加一个延迟执行的任务时,调用 addDelayedTask 方法,并传入任务函数和延迟时间。executeExpiredTasks 方法负责检查队列中是否有已到期的任务,并执行它们。 sortQueue 方法用于确保队列按照执行时间递增排序。在 eventLoop 中,每秒钟检查一次延迟队列,执行已到期的任务。这样,可以确保 setTimeout 的精确性。

使用setTimeout的一些注意项

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

当当前任务执行时间过久时,可能会影响 setTimeout 任务的准时执行。这是因为 setTimeout 的执行并不是严格准时的,而是在指定的延迟时间后将任务添加到消息队列,然后等待主线程空闲时执行。如果当前任务执行时间较长,它可能会导致 setTimeout 的实际执行时间被推迟。

下面是一个简单的例子,演示了当前任务执行时间过久时对 setTimeout 任务的影响:

js 复制代码
// 模拟一个耗时较长的任务
function longRunningTask() {
    console.log('开始执行长时间运行的任务');
    // 模拟耗时操作,这里设置为 5 秒
    const endTime = Date.now() + 5000;
    while (Date.now() < endTime) {
        // 什么都不做,模拟耗时操作
    }
    console.log('长时间运行的任务执行完成');
}

// 启动 setTimeout 任务
function startSetTimeoutTask() {
    setTimeout(() => {
        console.log('setTimeout 任务执行');
    }, 1000); // 设置延迟时间为 1 秒
}

// 启动长时间运行的任务
longRunningTask();

// 启动 setTimeout 任务
startSetTimeoutTask();

在这个例子中,longRunningTask 模拟了一个执行时间较长的任务,它会占用主线程达到 5 秒。而 startSetTimeoutTask 启动了一个延迟 1 秒的 setTimeout 任务。 由于 longRunningTask 的执行时间较长,它会推迟 setTimeout 任务的实际执行时间。因此,虽然 setTimeout 的延迟时间设置为 1 秒,但实际上会在 longRunningTask 执行完成后的更长时间后才执行。 这种情况下,如果要确保 setTimeout 任务准时执行,可以考虑在长时间运行的任务中使用 requestAnimationFrame 或者 setImmediate(在一些环境中可用)来分割任务,以确保在每个任务块之间让出主线程,使得消息队列中的 setTimeout 任务有机会得到执行。

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

在浏览器环境中,setTimeout 的最短时间间隔是由浏览器实现决定的,并不是 JavaScript 标准规定的。通常来说,浏览器为了性能和效率的考虑,会将连续调用的 setTimeout 任务进行合并执行,并设置一个最小时间间隔来防止过于频繁的执行。

嵌套调用 setTimeout 的场景中,即使你设置了较短的时间间隔,浏览器也可能会进行合并执行。以下是一个简单的例子来演示:

js 复制代码
function nestedTimeoutExample() {
    // 第一层 setTimeout
    setTimeout(() => {
        console.log('第一层 setTimeout 执行');

        // 第二层 setTimeout,时间间隔为1毫秒
        setTimeout(() => {
            console.log('第二层 setTimeout 执行');
        }, 1);

    }, 1);
}

nestedTimeoutExample();

在这个例子中,我们有两个嵌套的 setTimeout,第一层和第二层都设置了 1 毫秒的时间间隔。但是,由于浏览器的实现机制,可能会将它们合并执行,并不保证准确的 1 毫秒的时间间隔。浏览器可能会设置一个最小的时间间隔(通常是4毫秒),以防止过于频繁的任务执行。如果需要更精确的定时,可以考虑使用 equestAnimationFrame,但这同样也受到浏览器的实现影响。

3.未激活的页面,setTimeout执行的最小间隔是1000毫秒

在未激活的页面中,浏览器通常会对定时器的执行间隔进行调整,以降低对系统资源的占用。这种行为是为了提高性能和降低功耗,因为未激活的页面对用户而言可能是不可见的,对其执行的任务可能会进行一些优化。

在大多数浏览器中,未激活的页面中的 setTimeout 最小间隔通常被调整为1000毫秒(1秒)。这意味着如果你在未激活的页面中使用setTimeout,设置的时间间隔小于1000毫秒时,浏览器可能会延长执行时间间隔。

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

JavaScript 中的延迟执行时间没有一个严格的最大值,但在实际应用中可能会受到一些限制。这些限制通常来自浏览器或宿主环境,并且可能因浏览器、设备或运行环境的不同而异。

定时器函数(例如 setTimeout)的延迟参数是一个32位整数,因此在理论上,最大的延迟值是2^31 - 1,即2147483647毫秒,约为24.86天。超过这个值的延迟时间可能会导致溢出,因此在实际应用中,延迟时间不应该超过这个范围。

js 复制代码
setTimeout(() => {
    // 2147483647 毫秒约为24.86天
    console.log('执行延迟任务');
}, 2147483647);

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

JavaScript 中,setTimeout 设置的回调函数中的 this 的值是在运行时确定的,并且通常取决于函数的调用方式。这种情况可能导致 this 的值与直觉不符,特别是在回调函数被对象方法调用时。

在使用 setTimeout 时,回调函数实际上成为了一个全局函数,并不再与原始的调用上下文(调用该函数的对象)相关联。因此,this 的值将不再指向调用对象。

js 复制代码
// 示例:使用 setTimeout 导致 this 不指向调用对象
const myObject = {
    value: 42,
    printValue: function () {
        console.log(this.value);
    }
};

// 在 myObject 上调用 printValue 方法
myObject.printValue(); // 输出: 42

// 使用 setTimeout 调用 printValue 方法
setTimeout(myObject.printValue, 1000); // 输出: undefined(this 不再指向 myObject)

解决 setTimeout 中 this 不指向调用对象的问题,有几种常见的方法:

  1. 使用箭头函数: 箭头函数不会创建自己的 this 上下文,而是从外部继承。因此,使用箭头函数作为 setTimeout 的回调函数可以确保 this 指向正确的对象。
js 复制代码
const myObject = {
    value: 42,
    printValue: function () {
        console.log(this.value);
    }
};

setTimeout(() => myObject.printValue(), 1000); // 输出: 42
  1. 使用 .bind() 方法: 可以使用 .bind() 方法将回调函数与指定的对象绑定,确保 this 指向该对象。
js 复制代码
const myObject = {
    value: 42,
    printValue: function () {
        console.log(this.value);
    }
};

setTimeout(myObject.printValue.bind(myObject), 1000); // 输出: 42
  1. 使用函数包装器: 使用一个函数包装器,将原始函数作为参数传递给 setTimeout
js 复制代码
const myObject = {
    value: 42,
    printValue: function () {
        console.log(this.value);
    }
};

setTimeout(function () {
    myObject.printValue();
}, 1000); // 输出: 42
相关推荐
小小李程序员10 分钟前
css边框修饰
前端·css
我爱画页面42 分钟前
使用dom-to-image截图html区域为一张图
前端·html
忧郁的西红柿1 小时前
HTML-DOM模型
前端·javascript·html
思茂信息1 小时前
CST电磁仿真77GHz汽车雷达保险杠
运维·javascript·人工智能·windows·5g·汽车
bin91531 小时前
【油猴脚本】00010 案例 Tampermonkey油猴脚本,动态渲染表格-添加提示信息框,HTML+Css+JavaScript编写
前端·javascript·css·bootstrap·html·jquery
Stanford_11061 小时前
C++入门基础知识79(实例)——实例 4【求商及余数】
开发语言·前端·javascript·c++·微信小程序·twitter·微信开放平台
Maer091 小时前
Cocos Creator3.x设置动态加载背景图并且循环移动
javascript·typescript
Good_Luck_Kevin20181 小时前
速通sass基础语法
前端·css·sass
大怪v2 小时前
前端恶趣味:我吸了juejin首页,好爽!
前端·javascript
反应热2 小时前
浏览器的本地存储技术:从 `localStorage` 到 `IndexedDB`
前端·javascript