本文解析 JavaScript 的单线程执行模型与事件循环机制。详细区分宏任务(Macro-task)与微任务(Micro-task)的执行优先级,阐述主线程、调用栈与任务队列的交互过程。通过具体代码示例,说明 setTimeout、Promise 及 MutationObserver 在事件循环中的调度规则,并解释点击事件与脚本执行对任务队列的影响。
任务队列与分类
任务队列
主线程负责执行当前正在运行的代码,并维护一个函数调用栈。
- 宏任务(Macro-task,新版规范称为 task)包含:script(整体代码块)、setTimeout、setInterval、setImmediate、I/O 操作以及 UI 渲染。
- 微任务(Micro-task,新版规范称为 jobs)包含:process.nextTick、Promise 回调、Object.observe(已废弃)以及 MutationObserver(HTML5 新增特性,注意队列中同一时刻只能存在一个 MutationObserver 任务)。
任务分类
同步任务严格按照代码书写顺序依次执行,只有当前语句执行完毕,才会开始执行后续语句。
异步任务不遵循严格的先后顺序,当代码执行到异步逻辑时,该任务会被放入相应的任务队列中,等待后续时机执行。
单线程事件循环机制
单线程
主线程从执行 script(整体代码)开始第一次事件循环。随后全局上下文进入函数调用栈。当调用栈清空(仅剩全局上下文)时,立即执行所有可执行的 jobs(微任务)。在所有 jobs 执行完毕后,事件循环再次从 task(宏任务)队列中取出一个任务执行,执行完毕后再次清空所有 jobs。此过程不断循环,直至所有任务处理完毕。
执行注意事项
注意事项
- setTimeout 的最小时间间隔不能低于 4 毫秒,若设置值小于此限制,系统会自动调整为 4 毫秒。
- DOM 渲染频率通常与显示器刷新率同步,60 Hz 的显示器每 16 毫秒刷新一次,因此 DOM 渲染大约每 16 毫秒执行一次。
- process.nextTick 任务在 jobs 队列中拥有独立的优先级,会在其他 jobs 任务之前执行。
- 冒泡事件在子元素事件处理函数执行完成后,会被直接插入到主线程中。如果此时主线程不为空,冒泡事件的处理将优先于 jobs 队列中的任务执行。
经典示例分析
经典示例
示例详解:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
通过鼠标点击
<div class="outer">
<div class="inner"></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
// 输出结果
click
mutate
click
mutate
promise
promise
timeout
timeout
进阶--通过js执行
<div class="outer">
<div class="inner"></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
inner.click();
// 输出结果
click
click
mutate
promise
promise
timeout
timeout
由于点击事件由 JavaScript 触发,当 inner 的 onClick 函数执行完毕时,inner.click() 语句的作用域尚未退出调用栈,导致主线程调用栈不为空。因此,jobs 队列中的任务(mutate 和 promise)不会立即执行,而是继续执行 outer 的 onClick 函数。待 outer 的 onClick 执行完成后,inner.click() 语句才退出调用栈,随后才开始执行 jobs 队列中的任务。
之所以只有一个 mutate 输出,是因为在 jobs 队列中,同一时刻只能存在一个 MutationObserver 任务。当尝试创建第二个 MutationObserver 时,前一个任务尚未执行,因此不再创建新的实例。
总结
以上所述是小编给大家介绍的JS内部事件机制之单线程原理,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!