深入理解JavaScript事件循环机制:从单线程到异步执行

作者:代码拾荒时间:2026-10-26 13:54:15浏览:207

JavaScript作为单线程非阻塞语言,通过事件循环机制处理异步任务。本文详细解析同步代码、宏任务与微任务的执行优先级,剖析Event Loop工作流程,并结合Promise、async/await等案例说明输出顺序,帮助开发者掌握异步编程核心原理。

JavaScript单线程与非阻塞特性

JavaScript是一种采用单线程模型且具备非阻塞特性的脚本语言。

单线程

这意味着JavaScript引擎仅依靠唯一的主线程来负责处理所有任务。

非阻塞

在JavaScript引擎执行异步任务时,主线程不会停滞等待结果返回,而是将该任务挂起(pending),转而继续执行后续的其他任务。一旦异步任务完成并返回结果,JavaScript会将该任务的回调函数放入任务队列中。当主线程执行栈中的当前同步任务全部执行完毕,处于空闲状态时,主线程会按照队列顺序将队首的回调函数压入执行栈,执行其内部的同步代码。若执行过程中再次遇到异步任务,则将其回调函数继续加入队列,这一过程即构成了事件循环机制。

通常情况下,JavaScript是非阻塞的,但在某些特定场景下,代码执行会暂时停止,例如调用alert、confirm或prompt等阻塞式API时。

同步与异步任务分类

JavaScript的任务队列主要划分为同步任务和异步任务两类。

同步任务

同步任务直接在主线程上执行。在浏览器首次解析HTML文件的过程中,这些任务会被依次执行完毕。这包括在当前作用域内直接执行的所有代码,例如直接调用的方法或new创建的对象。

异步任务

这类任务通常耗时较长或涉及性能消耗。当浏览器执行流遇到异步任务时,会将其移交至异步任务队列,不会立即执行,而是等待后续时机。

异步任务根据执行优先级的不同,进一步细分为宏任务(macrotask)和微任务(microtask)。

宏任务包括:script(整体代码)、setTimeout、setInterval、setImmediate、I/O操作以及UI渲染等。

微任务包括:process.nextTick、Promise.then()、Object.observe、MutationObserver以及async/await相关的逻辑等。

回调函数的归属取决于其类型:若为微任务回调,则加入微任务队列;若为宏任务回调,则加入宏任务队列。在优先级上,微任务高于宏任务。

事件循环(Event Loop)执行流程

Event Loop(事件循环)的具体执行过程如下:

  • 主线程开始执行一段代码,通常从执行script标签内的整体代码开始,将代码压入执行栈。同步代码拥有最高优先级并优先执行。在执行过程中,若遇到异步任务源,系统会判断其属于宏任务还是微任务。
  • 根据判断结果,如果是宏任务,则将其放入宏任务队列;如果是微任务,则将其放入微任务队列。
  • 当同步代码执行完毕,执行栈变为空时,主线程会检查微任务队列中是否存在可执行的任务。如果存在,则依次执行微任务队列中的所有任务。若微任务队列为空,则当前这一轮的任务执行结束。
  • 随后进行DOM渲染。
  • 接着,主线程检查宏任务队列中是否有可执行的宏任务。如果有,则取出队列中最前面的那个宏任务,压入执行栈开始执行,并重复上述步骤,直到宏任务队列中的所有任务执行完毕。

需要注意的是,微任务通常在DOM渲染之前触发执行,而宏任务则在DOM渲染之后触发。

代码执行顺序示例分析

代码示例1

// 语句一
console.log(1);
// 语句二
setTimeout(()=>{
    console.log(2);
},0);
//语句三
Promise.resolve().then(()=>{
    console.log(3);
})
// 语句四
console.log(4);

//输出顺序 //1,4,3,2

代码示例2

console.log("script start");
setTimeout(function () {
  console.log("setTimeout");
}, 0);
Promise.resolve()
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });
console.log("script end");

JavaScript单线程与非阻塞特性

代码示例3

Promise与async/await机制

在new Promise(...)构造函数内部执行的代码属于同步代码,会立即执行。只有then方法之后注册的回调函数,才是作为异步任务执行的。

console.log("script start");
setTimeout(function () {
  console.log("timeout1");
}, 10);
new Promise((resolve) => {
  console.log("promise1");
  resolve();
  setTimeout(() => console.log("timeout2"), 10);
}).then(function () {
  console.log("then1");
});
console.log("script end");

JavaScript单线程与非阻塞特性

代码示例4

async和await是Generator函数与Promise的语法糖。async函数在行为上与普通函数一致,但其标识表明该函数内部包含异步操作,并始终返回一个Promise对象。

当await后面的函数执行完毕后,await操作会生成一个微任务(类似于Promise.then的行为)。

// 等价
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
// Promise 写法
async function async1() {
  console.log("async1 start");
  Promise.resolve(async2()).then(() => console.log("async1 end"));
}

JavaScript单线程与非阻塞特性

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();
setTimeout(() => {
  console.log("timeout");
}, 0);
new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});
console.log("script end");

代码示例5

// 1. 开始执行
console.log(1)     // 2. 打印 1
setTimeout(function () {    // 6. 浏览器在 0ms 后,将该函数推入任务队列
    console.log(2)    // 7. 打印 2
    Promise.resolve(1).then(function () {    // 8. 将 resolve(1) 推入任务队列  9. 将 function函数推入任务队列
        console.log('ok')    // 10. 打印 ok
    })
})    // 3.调用 setTimeout 函数,并定义其完成后执行的回调函数
setTimeout(function () {        // 11. 浏览器 0ms 后,将该函数推入任务队列
    console.log(3)    // 12. 打印 3
})    // 4. 调用 setTimeout 函数,并定义其完成后执行的回调函数
// 5. 主线程执行栈清空,开始读取 任务队列 中的任务
// output: 1  2 ok 3

代码示例6

// 1. 开始执行
console.log(1)     // 2. 打印 1
setTimeout(function () {    // 6. 浏览器在 0ms 后,将该函数推入任务队列
    console.log(2)    // 7. 打印 2
    Promise.resolve(1).then(function () {    // 8. 将 resolve(1) 推入任务队列  9. 将 function函数推入任务队列
        console.log('ok')    // 10. 打印 ok
    })
})    // 3.调用 setTimeout 函数,并定义其完成后执行的回调函数
setTimeout(function () {        // 11. 浏览器 0ms 后,将该函数推入任务队列
    console.log(3)    // 12. 打印 3
})    // 4. 调用 setTimeout 函数,并定义其完成后执行的回调函数
// 5. 主线程执行栈清空,开始读取 任务队列 中的任务
// output: 1  2 ok 3

代码示例7

setTimeout(function(){
  console.log('1')
});
new Promise(function(resolve){
  console.log('2');
  resolve();
}).then(function(){
  console.log('3')
});
var timer;
timer = setInterval(function(){
 console.log('5');
  clearInterval(timer);
});
new Promise(function(resolve){
  resolve();
}).then(function(){
  console.log('6')
});
console.log('4');
// 2,4,3,6,1,5

代码示例8

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
});
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0);
console.log('start');
// start -> Promise1 -> setTimeout1 -> Promise2 - > setTimeout2

实例

<body>
  <div id="box"></div>
  <script src="./app.js"></script>s
</body>
//js
const box = document.getElementById('box')
box.innerHTML = '<P>我是后来插进去的内容</P>'
console.log("1");
setTimeout( ()=>{
    console.log("2");
    alert("定时器执行了")
},0 )
Promise.resolve().then(()=>{
    console.log("3")
    alert("Promise执行了")
})
console.log("4");

JavaScript单线程与非阻塞特性

JavaScript单线程与非阻塞特性

tip:从规范层面来看,microtask(微任务)的执行优先级高于macrotask(宏任务)。因此,若有需要优先执行的逻辑,将其放入microtask队列会比放入task队列更早被执行。

到此这篇关于JavaScript中的事件循环机制及其运行原理的文章就介绍到这了,更多相关JavaScript事件循环机制内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章