JavaScript 是一种单线程的编程语言,这意味着它一次只能执行一个任务。为了能够处理异步操作,JavaScript 使用了一种称为事件循环(Event Loop)的机制。

本文将深入探讨事件循环的工作原理,并展示如何基于这一原理实现一个更为准确的 setTimeoutsetInterval

什么是事件循环?

事件循环是 JavaScript 运行时环境中处理异步操作的核心机制。它允许 JavaScript 在执行任务时不会阻塞主线程,从而实现非阻塞 I/O 操作。

为了理解事件循环,首先需要了解以下几个关键概念:

  1. 调用栈(Call Stack)

    • 调用栈是一个 LIFO(后进先出)结构,用于存储当前执行的函数调用。当一个函数被调用时,它会被推入调用栈,当函数执行完毕后,它会从调用栈中弹出。
  2. 任务队列(Task Queue)

    • 任务队列存储了所有等待执行的任务,这些任务通常是异步操作的回调函数,例如 setTimeoutsetInterval、I/O 操作等。当调用栈为空时,事件循环会从任务队列中取出一个任务并将其推入调用栈执行。
  3. 微任务队列(Microtask Queue)

    • 微任务队列存储了所有等待执行的微任务,这些微任务通常是 Promise 的回调函数、MutationObserver 等。微任务队列的优先级高于任务队列,当调用栈为空时,事件循环会优先处理微任务队列中的所有任务,然后再处理任务队列中的任务。

事件循环的工作原理

事件循环的工作原理可以简化为以下几个步骤:

  1. 执行调用栈中的任务

    • JavaScript 引擎会从调用栈中取出并执行最顶层的任务,直到调用栈为空。
  2. 处理微任务队列

    • 当调用栈为空时,事件循环会检查微任务队列。如果微任务队列中有任务,会依次取出并执行,直到微任务队列为空。
  3. 处理任务队列

    • 当调用栈和微任务队列都为空时,事件循环会检查任务队列。如果任务队列中有任务,会取出一个任务并将其推入调用栈执行。
  4. 重复上述步骤

    • 事件循环会不断重复上述步骤,确保所有任务都能被及时处理。

示例

以下是一个简单的示例,展示事件循环的工作原理:

console.log('Start');

setTimeout(() => {
console.log('Timeout callback');
}, 0); Promise.resolve().then(() => {
console.log('Promise callback');
}); console.log('End');

输出结果:

Start
End
Promise callback
Timeout callback

解释如下:

  1. 同步任务:首先执行同步任务,console.log('Start')console.log('End') 被推入调用栈并立即执行。
  2. 微任务Promise.resolve().then 创建了一个微任务,该微任务被推入微任务队列。
  3. 任务setTimeout 创建了一个任务,该任务被推入任务队列。
  4. 处理微任务:同步任务执行完毕后,调用栈为空,事件循环检查微任务队列并执行所有微任务,因此输出 Promise callback
  5. 处理任务:微任务队列为空后,事件循环检查任务队列并执行所有任务,因此输出 Timeout callback

为什么 setTimeout 不准确?

JavaScript 中的 setTimeoutsetInterval 是基于事件循环和任务队列的,因此它们的执行时间可能会受到以下几个因素的影响,从而导致不准确:

  1. 事件循环机制

    • JavaScript 是单线程的,所有代码的执行都是在一个事件循环中进行的。事件循环会依次处理任务队列中的任务。
    • 如果前面的任务执行时间较长,或者任务队列中有很多任务,定时器的回调函数就会被延迟执行。
  2. 任务队列的优先级

    • 浏览器的任务队列有不同的优先级,例如用户交互事件、渲染更新等任务的优先级通常高于 setTimeoutsetInterval
    • 这意味着即使定时器到期,如果有其他高优先级任务在执行,定时器的回调函数也会被延迟执行。
  3. JavaScript 引擎的限制

    • JavaScript 引擎通常会对最小时间间隔进行限制。例如,在浏览器环境中,嵌套的 setTimeout 调用的最小时间间隔通常是 4 毫秒。
    • 这意味着即使你设置了一个非常短的时间间隔,实际执行的时间间隔也可能会比你设置的时间更长。
  4. 系统性能和负载

    • 系统的性能和当前负载也会影响定时器的准确性。如果系统负载较高,任务的执行时间可能会被进一步延迟。

为了更直观地理解这一点,可以考虑以下示例:

console.log('Start');

setTimeout(() => {
console.log('Timeout callback');
}, 1000); const start = Date.now();
while (Date.now() - start < 2000) {
// 模拟一个耗时2秒的任务
} console.log('End');

在这个示例中,setTimeout 的回调函数设置为 1 秒后执行,但由于在主线程上有一个耗时 2 秒的任务,导致定时器的回调函数被延迟到这个任务执行完毕后才执行。

因此,实际执行时间会远远超过 1 秒。

实现一个更准确的 setTimeout

为了实现更精确的定时器,可以结合 Date 对象和递归的 setTimeout 来实现更高精度的定时器。

以下是一个实现准时 setTimeout 的例子:

function preciseTimeout(callback, delay) {
const start = Date.now(); function loop() {
const now = Date.now();
const elapsed = now - start;
const remaining = delay - elapsed; if (remaining <= 0) {
callback();
} else {
setTimeout(loop, remaining);
}
} setTimeout(loop, delay);
} // 使用示例
preciseTimeout(() => {
console.log('This is a precise timeout callback');
}, 1000); // 1秒

在这个实现中:

  1. 获取当前时间 start
  2. loop 函数中不断计算已经过去的时间 elapsed 和剩余时间 remaining
  3. 如果剩余时间 remaining 小于等于 0,就调用回调函数 callback
  4. 如果剩余时间 remaining 大于 0,就使用 setTimeout 递归调用 loop 函数。

这种方法能比直接使用 setTimeout 更精确地执行定时任务。

进一步优化

上面的代码还可以进一步优化,可以考虑使用 requestAnimationFrame 来实现更高精度的定时器。

requestAnimationFrame 是专门为动画设计的,它会在浏览器下一次重绘之前调用指定的回调函数。由于浏览器的重绘通常是每秒 60 次(即每 16.67 毫秒一次),所以使用 requestAnimationFrame 可以实现更高精度的定时器。

以下是使用 requestAnimationFrame 实现的高精度定时器:

function preciseTimeout(callback, delay) {
const start = Date.now(); function loop() {
const now = Date.now();
const elapsed = now - start; if (elapsed >= delay) {
callback();
} else {
requestAnimationFrame(loop);
}
} requestAnimationFrame(loop);
} // 使用示例
preciseTimeout(() => {
console.log('This is a precise timeout callback');
}, 1000); // 1秒

在这个实现中,requestAnimationFrame 会在每次浏览器重绘之前调用 loop 函数,从而实现更高精度的定时器。

实现一个更准确的 setInterval

同样地,我们可以通过结合 Date 对象和递归的 setTimeout 来实现更高精度的 setInterval。以下是一个实现准时 setInterval 的例子:

function preciseInterval(callback, interval) {
let expected = Date.now() + interval; function step() {
const now = Date.now();
const drift = now - expected; if (drift >= 0) {
callback();
expected += interval;
} setTimeout(step, interval - drift);
} setTimeout(step, interval);
} // 使用示例
preciseInterval(() => {
console.log('This is a precise interval callback');
}, 1000); // 每秒

在这个实现中:

  1. 设置预期的下一次执行时间 expected
  2. step 函数中不断计算当前时间 now 和预期时间 expected 之间的偏差 drift
  3. 如果偏差 drift 大于等于 0,就调用回调函数 callback,并更新预期时间 expected
  4. 使用 setTimeout 递归调用 step 函数,并根据偏差 drift 调整下一次调用的时间间隔。

进一步优化

为了进一步优化,可以考虑使用 requestAnimationFrame 来实现更高精度的定时器。requestAnimationFrame 是专门为动画设计的,它会在浏览器下一次重绘之前调用指定的回调函数。由于浏览器的重绘通常是每秒 60 次(即每 16.67 毫秒一次),所以使用 requestAnimationFrame 可以实现更高精度的定时器。

那我们使用 requestAnimationFrame 来实现的高精度 setInterval

function preciseSetInterval(callback, interval) {
let expected = performance.now() + interval;
function step() {
const drift = performance.now() - expected;
if (drift >= 0) {
callback();
expected += interval;
}
requestAnimationFrame(step);
}
requestAnimationFrame(step);
} // 使用示例
preciseSetInterval(() => {
console.log('This runs every 2 seconds with higher precision');
}, 2000);

总结

事件循环是 JavaScript 处理异步操作的核心机制,通过调用栈、任务队列和微任务队列的协调工作,实现了非阻塞 I/O 操作。

虽然 setTimeout 的定时精度受到事件循环的影响,但通过结合 Date 对象和递归的 setTimeout,或者使用 requestAnimationFrame,可以实现更为准确的定时器。

JavaScript 事件循环竟还能这样玩!的更多相关文章

  1. c#封装DBHelper类 c# 图片加水印 (摘)C#生成随机数的三种方法 使用LINQ、Lambda 表达式 、委托快速比较两个集合,找出需要新增、修改、删除的对象 c# 制作正方形图片 JavaScript 事件循环及异步原理(完全指北)

    c#封装DBHelper类   public enum EffentNextType { /// <summary> /// 对其他语句无任何影响 /// </summary> ...

  2. JavaScript 事件循环

    JavaScript 事件循环 事件循环 任务队列 async/await 又是如何处理的呢 ? 定时器问题 阻塞还是非阻塞 实际应用案例 拆分 CPU 过载任务 进度指示 在事件之后做一些事情 事件 ...

  3. 一篇文章图文并茂地带你轻松学完 JavaScript 事件循环机制(event loop)

    JavaScript 事件循环机制 (event loop) 本篇文章已经默认你有了基础的 ES6 和 javascript语法 知识. 本篇文章比较细致,如果已经对同步异步,单线程等概念比较熟悉的读 ...

  4. 深入理解 JavaScript 事件循环(一)— event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

  5. 深入理解 JavaScript 事件循环(二)— task and microtask

    引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...

  6. JavaScript事件循环(Event Loop)机制

    JavaScript 是单线程单并发语言 什么是单线程 主程序只有一个线程,即同一时间片断内其只能执行单个任务. 为什么选择单线程? JavaScript的主要用途是与用户互动,以及操作DOM.这决定 ...

  7. [译] 深入理解 JavaScript 事件循环(二)— task and microtask

    引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...

  8. 异步 JavaScript - 事件循环

    简评:如果你对 JavaScript 异步的原理感兴趣,这里有一篇不错的介绍. JavaScript 同步代码是如果工作的 在介绍 JavaScript 异步执行之前先来了解一下, JavaScrip ...

  9. JavaScript 事件循环 — event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

  10. JavaScript 事件循环及异步原理(完全指北)

    引言 最近面试被问到,JS 既然是单线程的,为什么可以执行异步操作? 当时脑子蒙了,思维一直被困在 单线程 这个问题上,一直在思考单线程为什么可以额外运行任务,其实在我很早以前写的博客里面有写相关的内 ...

随机推荐

  1. MapInfo 12.0 及 mapbasic 12.0 安装过程当中遇到的问题的汇总

    目录 MapInfo 12.0 及 mapbasic 12.0 安装过程当中遇到的问题的汇总 C++ 运行时库 Unable to load the CLR (-2147467263) 1) .NET ...

  2. 阿里云 腾讯云上搭建Samba服务

    对于这个主题,鄙人走了很久的坑,最后很抱歉的告诉你. 运营商把Samba服务的端口全部封掉了,所以你根本就没办法访问! 那怎么办,我Windows空间不够,又不想浪费云上的资源. 那就用FTP代替它吧 ...

  3. git clone 时拉取子模块

    git clone 时拉取子模块 对还未下载的项目: git clone --recursive 对已下载的项目: git submodule sync git submodule update -- ...

  4. 在Linux应用层使用POSIX定时器

    在Linux应用层使用POSIX定时器 ref : http://blog.chinaunix.net/uid-28458801-id-5035347.html http://blog.sina.co ...

  5. Linux 提权-SUID/SGID_2

    本文通过 Google 翻译 SUID | SGID Part-2 – Linux Privilege Escalation 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释 ...

  6. Blazor Server App Cannot find the fallback endpoint specified by route values

    github官方issues中提到的解决方案,CreateBuilder时指定项目绝对路径可以解决. 1 // 指定项目路径,也可以用Assembly.GetCallingAssembly获取 2 c ...

  7. Dawwin首位人工智能编程师,未来又会怎么样?

    Darwinai是一家快速发展的视觉质量检测公司,为制造商提供端到端解决方案,以提高产品质量并提高生产效率.该公司的专利可解释人工智能(XAI)平台已被众多财富500强公司采用,可以轻松集成值得信赖的 ...

  8. webpack4.15.1 学习笔记(四) — Tree shaking

    目录 Tree shaking 原理 标记效果 副作用代码不可被删除 如何实现 Tree shaking 的几种方法 总结 Tree shaking 本质上为了消除无用的js代码,减少加载文件体积的方 ...

  9. JavaScript小面试~什么是深拷贝,什么是浅拷贝,深拷贝和浅拷贝的区别,如何实现深拷贝

    深拷贝:就是在复制数据或者对象的时候,将其内存中值复制过来. 浅拷贝:就是在复制数据或者对象的时候,是将其引用复制过来. 深拷贝和浅拷贝的区别:深拷贝复制的是被复制数据或者对象的值,复制的数据或对象会 ...

  10. 入门到精通rsync和inotify

    rsync 作用: 实现文件的备份 备份位置可以是当前主机,也可以是远程主机 备份过程可以是完全备份,也可以是增量备份 功能: 1)类似于cp的复制功能 将本地主机的一个文件复制到另一个位置下 2)将 ...