实例详解JS中的事件循环机制

作者:小圆点时间:2026-10-25 09:15:24浏览:478

本文深入解析JavaScript的事件循环机制,详细区分同步任务、异步任务、宏任务与微任务的执行优先级。通过setTimeout、Promise及Async/Await的具体案例,逐步演示Tick执行流程,帮助开发者掌握代码执行顺序与异步编程核心原理。

一、前言

在回顾React相关钩子函数并结束该系列后,作者感到些许迷茫,不确定后续更新方向。经过思考,决定先回归JavaScript基础,梳理重要知识点,随后继续深入框架开发(可能涉及源码阅读或全家桶补全)。正如古语所言,不积跬步无以至千里,构建宏伟建筑需从基础做起,因此本文先从JavaScript的事件循环机制入手,正式展开讨论。

在JavaScript中,所有任务可划分为同步任务和异步任务两类。那么,如何准确区分这两者呢?

同步任务是指在主线程执行栈中按顺序排队执行的任务。只有当前一个任务执行完毕,后一个任务才会开始执行。常见的同步任务包括console.log输出语句和变量赋值语句等。

异步任务不会直接进入主线程,而是进入任务队列。只有当主线程中的所有任务执行完毕后,系统才会通知任务队列,请求执行其中的任务,此时该异步任务才会被推入主线程执行。典型的异步任务包括Ajax网络请求和setTimeout定时函数等。异步任务的协调依赖于任务队列机制,该机制遵循先进先出(FIFO)原则。

当执行一段代码时,若主线程执行栈遇到同步任务,会立即执行;若遇到异步任务,则暂时挂起,并将其推入任务队列(队列遵循先进先出原则)。当主线程执行栈中的同步任务全部执行完毕后,JavaScript引擎会去任务队列中读取挂起的异步任务,并将其推入执行栈中执行。这个“执行栈执行—判断同异步—同步执行或异步挂起推入事件队列—栈空后取事件队列任务并推入执行栈—继续判断同异步……”的不断重复过程,即构成了本文所介绍的事件循环机制。

一、前言

二、宏任务与微任务

每一次事件循环的操作被称为一个tick。在详细介绍一次tick的执行步骤之前,我们需要补充两个核心概念:宏任务(Macrotask)和微任务(Microtask)。

宏任务和微任务是严格意义上的ES6之后才出现的概念,原因在于ES6引入了Promise。在ES6之后,JavaScript的任务被更细致地划分为宏任务和微任务两类。

宏任务主要包括:script(整体代码)、setTimeout、setInterval、I/O操作、UI交互事件、postMessage、requestAnimationFrame(帧动画)、MessageChannel以及setImmediate(Node.js环境)。

微任务主要包括:Promise.then、MutationObserver以及process.nextTick(Node.js环境)。

了解了宏任务与微任务的概念后,我们来详细分析每次tick的执行顺序。首先参考下图:

一、前言

三、Tick 执行顺序

1、首先执行一个宏任务(如果栈中没有,则从事件队列中获取);

2、在执行过程中,如果遇到微任务,将其添加到微任务队列中;如果有宏任务,则推入相应的事件队列;

3、宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);

4、当前宏任务执行完毕后,开始进行页面渲染;5、开始下一个宏任务(从事件队列中获取),开启下一次的tick;

需要特别注意:如果宏任务执行过程中又添加了一个新的宏任务到任务队列中,这个新的宏任务会等到下一次事件循环再执行。而微任务则不同,如果在微任务执行过程中又添加了新的微任务,则新的微任务也会在本次微任务执行过程中被执行,直到微任务队列为空。每次宏任务执行完毕并开启下一次宏任务时,会将微任务队列中所有的微任务执行完毕!

四、案例详解

概念性的内容介绍完毕,下面通过具体的demo来练习巩固!

1.掺杂setTimeout

console.log('开始');

setTimeout(()=>{
    console.log('同级的定时器');
      setTimeout(() => {
          console.log('内层的定时器');
      }, 0);
},0)

console.log('结束');

输出结果为

开始 -> 结束 -> 同级的定时器 ->内层的定时器

解释上述代码:

  • 整体代码作为一个宏任务进入主线程执行栈中;
  • 遇到console.log('开始'),控制台输出 开始;
  • 遇到有一个宏任务setTimeout,JS引擎将之挂起,并推入任务队列;
  • 遇到console.log('结束'),控制台输出 结束;本次宏任务执行完毕,发现本次并无微任务,GUI进行render渲染完毕开启下一次宏任务执行,本次tick结束。
  • JS引擎从任务队列拿出第一个setTimeout宏任务,将至推入主线程执行栈, 开始进行第二个宏任务;
  • 执行setTimeout回调,遇到 console.log('同级的定时器'),控制台输出 同级的定时器;
  • 遇到第二个setTimeout ,这是个本次宏任务产生的新的宏任务,将此宏任务挂起,并推入任务队列;
  • 同样此时发现没有微任务,则GUI接管开始进行渲染,渲染完毕又开启下一次宏任务,tick结束;
  • JS引擎又从任务队列拿出第二个setTimeout宏任务,将之推入主线程执行栈, 开始进行第三个宏任务;
  • 执行第二个setTimeout回调,遇到 console.log('内层的定时器'),控制台输出 内层的定时器;
  • 本次宏任务执行完毕发现没有微任务,结束。

2.掺杂微任务,此处主要是Promise.then

console.log('script start');

setTimeout(function() {
  new Promise(resolve=>{
        console.log('000');
      resolve()
    }).then(res=>{
        console.log('这是微任务');
    })
  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');

输出结果为:

script start -> promise1 -> script end -> then1 -> 000 -> timeout1 -> 这是微任务 -> timeout2

解释上述代码:

  • 整体script作为一个宏任务进入主线程执行栈;
  • 遇到 console.log('script start') 输出 script start ;
  • 遇到setTimeout作为新的一个宏任务连同其回调内容一同推入任务队列 ;
  • 遇到和script start 同级的new Promise 进行执行,此处需注意:Promise内容是同步任务,它的.then才是微任务会被推入微任务队列。所有此处JS引擎的处理逻辑是:遇到 console.log('promise1') 输出 promise1 ,遇到resolve() 会将 Promise的.then函数推入微任务队列(注意,我们常说微任务时宏任务的小尾巴,指的是本次宏任务产生的微任务都会在本次宏任务执行完之后进行执行清空。);遇到resolve下面的setTimeout这是个新的宏任务,会被挂起并推入任务队列。
  • 继续顺序执行,执行到 console.log('script end') ,输出script end;此时第一个宏任务执行完毕,JS引擎开始清理小尾巴(执行并清空微任务队列)。
  • 此时由本次执行宏任务的过程中产生了 .then(function() { console.log('then1')}) 这个微任务,JS引擎会将此任务内的回调推入执行栈进行执行,输出 then1;
  • 微任务队列为空,开启下一个宏任务,第一轮tick结束;
  • JS引擎从任务队列中拿script start下面那个setTimeout宏任务将回调推入主线程执行栈中进行执行;
  • 遇到了Promise,执行其内容:遇到 console.log('000') 输出 000;
  • 执行 resolve() 将.then函数推入微任务队列(是此次宏任务的小尾巴);
  • 继续执行,遇到 console.log('timeout1') 输出 timeout1;本次宏任务执行完毕;
  • 宏任务执行完毕后紧接着处理小尾巴: .then(res=>{ console.log('这是微任务'); }) 输出 这是微任务;
  • 微任务队列清空后,继续开启下一个宏任务,第二轮tick结束;
  • 将任务队列中的 setTimeout(() => console.log('timeout2'), 10); 回调推入执行栈中执行,输出 timeout2 ; 无微任务,第三轮tick结束,任务队列也为空。

好了,相信经过这两个例子,小伙伴们对事件循环有了初步的认识。接下来我们再顽皮一下:对上面这个demo做一丢丢微调

微调一 : 其他地方不变,then里塞定时器

setTimeout(function() {
  new Promise(resolve=>{
        console.log('000');
      resolve()
    }).then(res=>{
         setTimeout(()=>{
         console.log('这次的执行顺序呢?') -----> 如果这里再塞个定时器呢?执行顺序是什么?
        },10)
        console.log('这是微任务');
    })
  console.log('timeout1');
}, 10);

微调二:其他地方不变,对Promise进行链式调用

new Promise(resolve => {
    console.log('promise1');       
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
}).then(()=>{
    console.log('then2')
}).then(()=>{
    console.log('then3')
})

此Promise进行链式调用,其他地方不动,此时的执行顺序是什么?

提示:在一次tick结束时,此tick内微任务队列中的微任务一定会执行完并清空,如果在执行过程中又产生了微任务,那么同样会在此tick过程中执行完毕;而宏任务的执行则可以看成是下一次tick的开始。

3.掺杂async/await

在进行demo解析之前,我们需要补充一下async/await的相关知识点。

async

async相当于隐式返回Promise:当我们在函数前使用async的时候,使得该函数返回的是一个Promise对象,async的函数会在这里帮我们隐式使用Promise.resolve();

下面看个小demo来理解下async函数是怎么隐式转换的:

async function test() {
    console.log('这是async函数')
    return '测试隐式转换' 
}

上面这个async就相当于如下代码:

function test(){
    return new Promise(function(resolve) {
      console.log('这是async函数')
       resolve('测试隐式转换')
   })
}

await

await表示等待,是右侧表达式的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且await只能在带有async的内部使用;使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的 代码 , 当外部代码执行完毕,再回到该函数内部执行await后面剩余的代码;

好了,补充完前置知识我们来做个demo助助兴:

掺杂async/await的事件循环

async function async2() {				
    console.log('async2');     
}

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
    resolve()
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

输出顺序为

script start --> async1 start --> async2 --> promise1 --> script end --> async1 end --> promise2 --> setTimeout

首先为方便理解我们先将async函数转为return Promise的那种形式:

①:
async function async2() {				
    console.log('async2');     
}
转换后如下:
function async2() {			
    return  new Promise(resolve=>{
       console.log('async2');     
    })
}


②:
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
转换后如下:
function async1() {
  return new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码
    console.log('async1 end');
  })
}

所以,最后我们包含async函数的代码块就相当于如下代码:

function async2() {			
    return  new Promise(resolve=>{
       console.log('async2');     
    })
}

function async1() {
  return new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码,在此处是阻塞了console.log('async1 end')的执行
    console.log('async1 end');
  })
}
=============上面为声明部分===========
console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码,在此处是阻塞了console.log(async1end)的执行;这里相当于awaitasync2()
   
    console.log('async1 end');
  })
}

new Promise((resolve) => {
    resolve()
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

经过一系列骚操作之后,我们终于可以来分析这个代码块的执行顺序了,废话不多说,开冲。

解释上述代码:

  • 首先整体代码作为第一个宏任务进入主线程执行栈;
  • 首先顺序执行,遇到了async2、async1 函数的声明,不进行任何输出;
  • 执行到console.log('script start') 输出 script start ;
  • 继续执行,遇到setTimeout宏任务,挂起并推入任务队列;
  • 接着执行Promise内容部分,遇到console.log('async1 start'),输出async1 start ;
  • 这一步重点来了,遇到了await,这该怎么办呢?别急,咱们再来看看使用await会发生什么:使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的 代码 , 当外部代码执行完毕,再回到该函数内部执行await后面剩余的代码;
  • 好了,下面开始解释await async2():由于是是从右往左执行,所以我们首先执行了async2()输出了一个Promise,我们执行了Promise的内容输出了async2;async2执行完了之后,遇到await,完全不出意外,后面的代码被阻塞;我们去执行外面的代码;
  • 因为console.log('async1 end')被await阻塞掉了,我们先执行外面的代码:执行了外面Promise的内容,遇到了resolve(),将.then函数推入微任务队列;然后执行console.log('promise1'),输出 promise1;
  • 最后执行到console.log('script end'),输出 script end;
  • 到此,我们外层的代码就执行完毕,现在想想好像少了什么?往前一看,我们console.log('async1 end')还在等待,此时,JS引擎执行log输出 async1 end 。
  • 由此,我们本次的宏任务就执行完毕,下面看看是否有微任务,JS引擎去微任务队列一看,好家伙,还藏着一个 then(function () {console.log('promise2');}); 把此任务回调推到执行栈中执行,输出 promise2;
  • 此次tick执行结束,开启下一个宏任务;
  • 从任务队列拿setTimeout这个宏任务,塞入执行栈执行,打印输出setTimeout,本次无微任务,结束tick;
  • 循环结束;

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

作者最新文章