Skip to content

JavaScript 事件循环机制

为什么 JavaScript 是单线程?

js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。在js高程中举过例子🌰,如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?

为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。

执行栈与任务队列

因为js是单线程语言,当遇到异步任务(如ajax操作等)时,不可能一直等待异步完成,再继续往下执行,在这期间浏览器是空闲状态,显而易见这会导致巨大的资源浪费。

主线程跟执行栈是不同概念,执行栈是js引擎内部的概念,主线程是js引擎外部概念。 主线程执行栈执行同步任务,异步任务会进入任务队列,任务队列中的任务会进入执行栈执行。 遇到异步事件后,主线程会先执行同步任务,同步任务执行完毕后,主线程会从任务队列中取出异步任务执行。 任务队列中的任务会依次执行,直到任务队列为空。

示例代码:

javascript
let a = () => {
  setTimeout(() => {
    console.log('任务队列函数1')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('a的for循环')
  }
  console.log('a事件执行完')
}

let b = () => {
  setTimeout(() => {
    console.log('任务队列函数2')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('b的for循环')
  }
  console.log('b事件执行完')
}

a();
b();

执行顺序分析:

  1. 先执行script同步代码
    • 输出a的for循环
    • 输出b的for循环
  2. 执行异步任务
    • 输出a事件执行完
    • 输出b事件执行完
    • 输出任务队列函数1
    • 输出任务队列函数2

js异步执行的原理:

  1. 所有任务都在主线程上执行,形成一个执行栈
  2. 主线程执行栈执行同步任务,遇到异步任务,将异步任务放入任务队列
  3. 同步任务执行完毕后,主线程会从任务队列中取出异步任务执行
  4. 任务队列中的任务会依次执行,直到任务队列为空

宏任务与微任务

异步任务分为宏任务(macrotask)与微任务(microtask),不同的API注册的任务会依次进入自身对应的队列中。

宏任务包括:

  • script(整体代码)
  • setTimeout/setInterval
  • UI 渲染
  • I/O
  • postMessage
  • MessageChannel
  • setImmediate(Node.js环境)

微任务包括:

  • Promise
  • MutaionObserver
  • process.nextTick(Node.js环境)

面试题示例:

javascript
setTimeout(function () {
    console.log(1);
});
new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})
console.log(4);

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

执行顺序分析:

  1. 先执行script同步代码
    • 输出2(Promise构造函数中的代码是同步的)
    • 输出4
  2. 执行微任务
    • 输出3(Promise.then)
  3. 执行下一个宏任务
    • 输出1(setTimeout)

Event Loop 事件循环

事件循环的基本概念

事件循环是 JavaScript 实现异步的核心机制。它负责执行代码、收集和处理事件以及执行队列中的子任务。

事件循环的执行顺序

  1. 执行同步代码(这属于宏任务)
  2. 执行微任务队列(microtask queue)
  3. 执行宏任务队列(macrotask queue)
  4. 重复 2-3 步骤

完整的事件循环过程

javascript
console.log('script start');

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

new Promise((resolve, reject) => {
    console.log('promise创建');  // 同步代码!
    resolve();
})
.then(() => {
    console.log('promise1');
})
.then(() => {
    console.log('promise2');
});

console.log('script end');

// 正确的输出顺序:
// script start
// promise创建
// script end
// promise1
// promise2
// setTimeout
  • Promise 构造函数是同步的,then/catch/finally 的回调才会进入微任务队列

详细执行过程:

  1. 首先执行宏任务中的同步代码

    • 打印 'script start'
    • 遇到 setTimeout,将其回调放入宏任务队列
    • 遇到 Promise,将 then 回调放入微任务队列
    • 打印 'script end'
  2. 执行所有微任务

    • 执行第一个 then,打印 'promise1'
    • 第二个 then 进入微任务队列
    • 执行第二个 then,打印 'promise2'
  3. 执行下一个宏任务

    • 执行 setTimeout 回调,打印 'setTimeout'

注意事项

  1. 微任务优先级高于宏任务
  2. 新产生的微任务会立即执行
  3. requestAnimationFrame 在重新渲染前执行
  4. 浏览器完成一次事件循环后可能会进行重新渲染

实际应用示例:

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

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

console.log('script start');

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

async1();

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

console.log('script end');

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

浏览器渲染时机

  • 一般是在一次事件循环结束后
  • 如果有 requestAnimationFrame,会在重新渲染前执行
  • CSS 变动和 DOM 变动可能会触发重新渲染

Promise 核心逻辑实现

原生Promise:

javascript
const promise = new Promise((resolve,reject)) => {
  resolve('success')
  reject('error')
}

promise.then(value => {
  console.log('resolve',value)
},reason => {
  console.log('reject',reason)
})