JavaScript 事件循环竟还能这样玩!
JavaScript 是一种单线程的编程语言,这意味着它一次只能执行一个任务。为了能够处理异步操作,JavaScript 使用了一种称为事件循环(Event Loop)的机制。
本文将深入探讨事件循环的工作原理,并展示如何基于这一原理实现一个更为准确的 setTimeout
、setInterval
什么是事件循环?
事件循环是 JavaScript 运行时环境中处理异步操作的核心机制。它允许 JavaScript 在执行任务时不会阻塞主线程,从而实现非阻塞 I/O 操作。
为了理解事件循环,首先需要了解以下几个关键概念:
调用栈(Call Stack):
- 调用栈是一个 LIFO(后进先出)结构,用于存储当前执行的函数调用。当一个函数被调用时,它会被推入调用栈,当函数执行完毕后,它会从调用栈中弹出。
任务队列(Task Queue):
- 任务队列存储了所有等待执行的任务,这些任务通常是异步操作的回调函数,例如
setTimeout
、setInterval
、I/O 操作等。当调用栈为空时,事件循环会从任务队列中取出一个任务并将其推入调用栈执行。
- 任务队列存储了所有等待执行的任务,这些任务通常是异步操作的回调函数,例如
微任务队列(Microtask Queue):
- 微任务队列存储了所有等待执行的微任务,这些微任务通常是
Promise
的回调函数、MutationObserver
等。微任务队列的优先级高于任务队列,当调用栈为空时,事件循环会优先处理微任务队列中的所有任务,然后再处理任务队列中的任务。
- 微任务队列存储了所有等待执行的微任务,这些微任务通常是
事件循环的工作原理
事件循环的工作原理可以简化为以下几个步骤:
执行调用栈中的任务:
- JavaScript 引擎会从调用栈中取出并执行最顶层的任务,直到调用栈为空。
处理微任务队列:
- 当调用栈为空时,事件循环会检查微任务队列。如果微任务队列中有任务,会依次取出并执行,直到微任务队列为空。
处理任务队列:
- 当调用栈和微任务队列都为空时,事件循环会检查任务队列。如果任务队列中有任务,会取出一个任务并将其推入调用栈执行。
重复上述步骤:
- 事件循环会不断重复上述步骤,确保所有任务都能被及时处理。
示例
以下是一个简单的示例,展示事件循环的工作原理:
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
解释如下:
- 同步任务:首先执行同步任务,
console.log('Start')
和console.log('End')
被推入调用栈并立即执行。 - 微任务:
Promise.resolve().then
创建了一个微任务,该微任务被推入微任务队列。 - 任务:
setTimeout
创建了一个任务,该任务被推入任务队列。 - 处理微任务:同步任务执行完毕后,调用栈为空,事件循环检查微任务队列并执行所有微任务,因此输出
Promise callback
。 - 处理任务:微任务队列为空后,事件循环检查任务队列并执行所有任务,因此输出
Timeout callback
。
为什么 setTimeout
不准确?
JavaScript 中的 setTimeout
和 setInterval
是基于事件循环和任务队列的,因此它们的执行时间可能会受到以下几个因素的影响,从而导致不准确:
事件循环机制:
- JavaScript 是单线程的,所有代码的执行都是在一个事件循环中进行的。事件循环会依次处理任务队列中的任务。
- 如果前面的任务执行时间较长,或者任务队列中有很多任务,定时器的回调函数就会被延迟执行。
任务队列的优先级:
- 浏览器的任务队列有不同的优先级,例如用户交互事件、渲染更新等任务的优先级通常高于
setTimeout
和setInterval
。 - 这意味着即使定时器到期,如果有其他高优先级任务在执行,定时器的回调函数也会被延迟执行。
- 浏览器的任务队列有不同的优先级,例如用户交互事件、渲染更新等任务的优先级通常高于
JavaScript 引擎的限制:
- JavaScript 引擎通常会对最小时间间隔进行限制。例如,在浏览器环境中,嵌套的
setTimeout
调用的最小时间间隔通常是 4 毫秒。 - 这意味着即使你设置了一个非常短的时间间隔,实际执行的时间间隔也可能会比你设置的时间更长。
- JavaScript 引擎通常会对最小时间间隔进行限制。例如,在浏览器环境中,嵌套的
系统性能和负载:
- 系统的性能和当前负载也会影响定时器的准确性。如果系统负载较高,任务的执行时间可能会被进一步延迟。
为了更直观地理解这一点,可以考虑以下示例:
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秒
在这个实现中:
- 获取当前时间
start
。 - 在
loop
函数中不断计算已经过去的时间elapsed
和剩余时间remaining
。 - 如果剩余时间
remaining
小于等于 0,就调用回调函数callback
。 - 如果剩余时间
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); // 每秒
在这个实现中:
- 设置预期的下一次执行时间
expected
。 - 在
step
函数中不断计算当前时间now
和预期时间expected
之间的偏差drift
。 - 如果偏差
drift
大于等于 0,就调用回调函数callback
,并更新预期时间expected
。 - 使用
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 事件循环竟还能这样玩!的更多相关文章
- c#封装DBHelper类 c# 图片加水印 (摘)C#生成随机数的三种方法 使用LINQ、Lambda 表达式 、委托快速比较两个集合,找出需要新增、修改、删除的对象 c# 制作正方形图片 JavaScript 事件循环及异步原理(完全指北)
c#封装DBHelper类 public enum EffentNextType { /// <summary> /// 对其他语句无任何影响 /// </summary> ...
- JavaScript 事件循环
JavaScript 事件循环 事件循环 任务队列 async/await 又是如何处理的呢 ? 定时器问题 阻塞还是非阻塞 实际应用案例 拆分 CPU 过载任务 进度指示 在事件之后做一些事情 事件 ...
- 一篇文章图文并茂地带你轻松学完 JavaScript 事件循环机制(event loop)
JavaScript 事件循环机制 (event loop) 本篇文章已经默认你有了基础的 ES6 和 javascript语法 知识. 本篇文章比较细致,如果已经对同步异步,单线程等概念比较熟悉的读 ...
- 深入理解 JavaScript 事件循环(一)— event loop
引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...
- 深入理解 JavaScript 事件循环(二)— task and microtask
引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...
- JavaScript事件循环(Event Loop)机制
JavaScript 是单线程单并发语言 什么是单线程 主程序只有一个线程,即同一时间片断内其只能执行单个任务. 为什么选择单线程? JavaScript的主要用途是与用户互动,以及操作DOM.这决定 ...
- [译] 深入理解 JavaScript 事件循环(二)— task and microtask
引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...
- 异步 JavaScript - 事件循环
简评:如果你对 JavaScript 异步的原理感兴趣,这里有一篇不错的介绍. JavaScript 同步代码是如果工作的 在介绍 JavaScript 异步执行之前先来了解一下, JavaScrip ...
- JavaScript 事件循环 — event loop
引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...
- JavaScript 事件循环及异步原理(完全指北)
引言 最近面试被问到,JS 既然是单线程的,为什么可以执行异步操作? 当时脑子蒙了,思维一直被困在 单线程 这个问题上,一直在思考单线程为什么可以额外运行任务,其实在我很早以前写的博客里面有写相关的内 ...
随机推荐
- MapInfo 12.0 及 mapbasic 12.0 安装过程当中遇到的问题的汇总
目录 MapInfo 12.0 及 mapbasic 12.0 安装过程当中遇到的问题的汇总 C++ 运行时库 Unable to load the CLR (-2147467263) 1) .NET ...
- 阿里云 腾讯云上搭建Samba服务
对于这个主题,鄙人走了很久的坑,最后很抱歉的告诉你. 运营商把Samba服务的端口全部封掉了,所以你根本就没办法访问! 那怎么办,我Windows空间不够,又不想浪费云上的资源. 那就用FTP代替它吧 ...
- git clone 时拉取子模块
git clone 时拉取子模块 对还未下载的项目: git clone --recursive 对已下载的项目: git submodule sync git submodule update -- ...
- 在Linux应用层使用POSIX定时器
在Linux应用层使用POSIX定时器 ref : http://blog.chinaunix.net/uid-28458801-id-5035347.html http://blog.sina.co ...
- Linux 提权-SUID/SGID_2
本文通过 Google 翻译 SUID | SGID Part-2 – Linux Privilege Escalation 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释 ...
- Blazor Server App Cannot find the fallback endpoint specified by route values
github官方issues中提到的解决方案,CreateBuilder时指定项目绝对路径可以解决. 1 // 指定项目路径,也可以用Assembly.GetCallingAssembly获取 2 c ...
- Dawwin首位人工智能编程师,未来又会怎么样?
Darwinai是一家快速发展的视觉质量检测公司,为制造商提供端到端解决方案,以提高产品质量并提高生产效率.该公司的专利可解释人工智能(XAI)平台已被众多财富500强公司采用,可以轻松集成值得信赖的 ...
- webpack4.15.1 学习笔记(四) — Tree shaking
目录 Tree shaking 原理 标记效果 副作用代码不可被删除 如何实现 Tree shaking 的几种方法 总结 Tree shaking 本质上为了消除无用的js代码,减少加载文件体积的方 ...
- JavaScript小面试~什么是深拷贝,什么是浅拷贝,深拷贝和浅拷贝的区别,如何实现深拷贝
深拷贝:就是在复制数据或者对象的时候,将其内存中值复制过来. 浅拷贝:就是在复制数据或者对象的时候,是将其引用复制过来. 深拷贝和浅拷贝的区别:深拷贝复制的是被复制数据或者对象的值,复制的数据或对象会 ...
- 入门到精通rsync和inotify
rsync 作用: 实现文件的备份 备份位置可以是当前主机,也可以是远程主机 备份过程可以是完全备份,也可以是增量备份 功能: 1)类似于cp的复制功能 将本地主机的一个文件复制到另一个位置下 2)将 ...