JavaScript 事件循环
- 作者: 五速梦信息网
- 时间: 2026年04月04日 13:37
事件循环
JavaScript
引擎的一般算法:
- 当有任务时:从最先进入的任务开始执行。
- 休眠直到出现任务,然后转到第 1 步。
JavaScript
任务示例:
设置任务 —— 引擎处理它们 —— 然后等待更多任务(即休眠,几乎不消耗 CPU 资源)。
一个任务到来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。
多个任务组成了一个队列,即所谓的“宏任务队列”(v8 术语):
scriptmousemovesetTimeout
scriptmousemovesetTimeout
例如:
引擎执行任务时永远不会进行渲染(render)。如果任务执行需要很长一段时间也没关系。仅在任务完成后才会绘制对 DOM 的更改。
如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。因此,在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报,建议你终止这个任务。这种情况常发生在有大量复杂的计算或导致死循环的程序错误时。
任务队列
JavaScriptMacrotask Queue(Task Queue) 宏任务Microtask Queue 微任务
Macrotask
setTimeoutsetIntervalsetImmediateI/O
Microtask
Promiseprocess.nextTickObject.observe
那么,两者有什么具体的区别呢?或者说,如果两种任务同时出现的话,应该选择哪一个呢?
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。
其实事件循环执行流程如下:
MacrotaskMacrotaskMicrotaskMicrotaskmicrotaskmacrotask
MacrotaskMicrotask
我们先来看一段代码:
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
你觉得结果应该是什么呢?
chrome
1
9
7
8
2
3
10
11
12
13
在上面的例子中
- 第一次事件循环:
console.log(1)1settimeout1macrotasksetinterval1macrotassettimeout2macrotaskpromise2thenmicrotaskconsole.log(9)9microtaskconsole.log(7)console.log(8)78microtaskmacrotasksettimeout1setinterval1settimeout2
- 第二次事件循环:
macrotasksettimeout12microtaskmacrotasksetinterval1settimeout2
- 第三次事件循环:
macrotasksetinterval13setinterval1macrotaskmicrotaskmacrotasksettimeout2setinterval1
- 第四次事件循环:
macrotasksettimeout210new Promisenew Promise11thenmicrotaskmicrotaskmicrotask1213setinterval1
microtaskmacrotaskmacrotaskmacrotaskmicrotask
因为一开始js主线程中跑的任务就是macrotask任务macrotaskmicrotask
microtaskmicrotaskmicrotaskmicrotaskmacrotaskUI渲染/IO操作/ajax请求nodejsprocess.nexttick
async/await 又是如何处理的呢 ?
大家看看这段代码在浏览器上的输出是什么?
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
async/await
async/awaitPromiseGeneratorPromise
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
// 其实就是
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(()=>console.log('async1 end'))
}
那我们再看看转换后的整体代码
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(()=>console.log('async1 end'))
}
async function async2() {
console.log('async2');
}
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
这下就很明白了吧,输出的结果如下:
/**
* async1 start
* async2
* promise1
* script end
* async1 end
* promise2
* */
定时器问题
setTimeout(task, 100)100
我们看这个栗子
const s = new Date().getSeconds();
setTimeout(function() {
// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
setTimeout50022macrotasksetTimeout
setTimeout(task,100)100macrotaskmicrotasksetTimeout
阻塞还是非阻塞
关于 js 阻塞还是非阻塞的问题我们先理解下同步、异步、阻塞还是非阻塞的解释,在网上看到一段描述的非常好,引用下
我要看足球比赛,但是妈妈叫我烧水,电视机在客厅,烧水要在厨房。家里有2个水壶,一个是普通的水壶,另一个是水开了会叫的那种水壶。我可以:
- 用普通的水壶烧,人在边上看着,水开了再去看球。**(同步,阻塞)**这个是常规做法,但是我看球不爽了。
- 用普通水壶烧,人去看球,隔几分钟去厨房看看。**(同步,非阻塞)**这个又大问题,万一在我离开的几分钟水开了,我就麻烦了。
- 用会叫的水壶,人在边上看着。**(异步,阻塞)**这个没有问题,但是我太傻了。
- 用会叫的水壶,人去看球,听见水壶叫了再去看。**(异步,非阻塞)**这个应该是最好的。
等着看球的我:阻塞
看着电视的我:非阻塞
普通水壶:同步
会叫的水壶:异步
所以,异步往往配合非阻塞,才能发挥出威力。
js 核心还是同步阻塞的,比如看这段代码
while (true) {
if (new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
console.log('end')
console.log('end')while
jsnodejs事件循环任务队列libuvjs
实际应用案例
拆分 CPU 过载任务
假设我们有一个 CPU 过载任务。
CPU
DOM
100setTimeout100
11000000000
JS
let i = 0;
let start = Date.now();
function count() {
// 做一个繁重的任务
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
浏览器甚至可能会显示一个“脚本执行时间过长”的警告。
setTimeout
let i = 0;
let start = Date.now();
function count() {
// 做繁重的任务的一部分 (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // 安排(schedule)新的调用 (**)
}
}
count();
现在,浏览器界面在“计数”过程中可以正常使用。
count(*)(**)
i=1...1000000i=1000001..2000000
onclickcountJavaScript
setTimeout
为了使两者耗时更接近,让我们来做一个改进。
schedulingcount()
let i = 0;
let start = Date.now();
function count() {
// 将调度(scheduling)移动到开头
if (i < 1e9 - 1e6) {
setTimeout(count); // 安排(schedule)新的调用
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
}
count();
count()count()
如果你运行它,你很容易注意到它花费的时间明显减少了。
为什么?
setTimeout4ms04ms
最后,我们将一个繁重的任务拆分成了几部分,现在它不会阻塞用户界面了。而且其总耗时并不会长很多。
进度指示
对浏览器脚本中的过载型任务进行拆分的另一个好处是,我们可以显示进度指示。
DOM
从一方面讲,这非常好,因为我们的函数可能会创建很多元素,将它们一个接一个地插入到文档中,并更改其样式 —— 访问者不会看到任何未完成的“中间态”内容。很重要,对吧?
i
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
……但是我们也可能想在任务执行期间展示一些东西,例如进度条。
setTimeout
这看起来更好看:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// 做繁重的任务的一部分 (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>
divi
在事件之后做一些事情
setTimeout
menu-opensetTimeoutclick
menu.onclick = function() {
// ...
// 创建一个具有被点击的菜单项的数据的自定义事件
let customEvent = new CustomEvent("menu-open", {
bubbles: true
});
// 异步分派(dispatch)自定义事件
setTimeout(() => menu.dispatchEvent(customEvent));
};
- 上一篇: JavaScript 事件循环及异步原理(完全指北)
- 下一篇: JavaScript 富文本编辑器
相关文章
-
JavaScript 事件循环及异步原理(完全指北)
JavaScript 事件循环及异步原理(完全指北)
- 互联网
- 2026年04月04日
-
javascript 之执行环境
javascript 之执行环境
- 互联网
- 2026年04月04日
-
JavaScript 中 4 种常见的内存泄露陷阱
JavaScript 中 4 种常见的内存泄露陷阱
- 互联网
- 2026年04月04日
-
JavaScript 富文本编辑器
JavaScript 富文本编辑器
- 互联网
- 2026年04月04日
-
JavaScript 对象与数组参考大全
JavaScript 对象与数组参考大全
- 互联网
- 2026年04月04日
-
javaScript 对象学习笔记
javaScript 对象学习笔记
- 互联网
- 2026年04月04日






