Event Loop事件循环

195 阅读9分钟

Promise

Promise.resolve("green").then((result) => {
    console.log(result);

    Promise.resolve("red").then((result) => {
        console.log(result);

        Promise.resolve("yellow").then((result) => {
            console.log(result);
        });
    });
});
async () => {
    await Promise.resolve("green").then((res) => {
        console.log(res);
    });
    await Promise.resolve("red").then((res) => {
        console.log(res);
    });
    await Promise.resolve("yellow").then((res) => {
        console.log(res);
    });
};
// 浏览器不直接支持 async/await,需要 babel 转译
  • async 是 function 的一个前缀,只有 async 函数中才能使用 await 语法
  • async 函数是一个 Promise 对象,有无 resolve 取决于有无函数中 return 值
  • await 后边跟得是一个 Promise 对象,如果不是,则会包裹一层 Promise.resolve(),将其转化为 Promise 对象

await 只能在 async 函数中使用,不能在普通函数中使用,它会暂停函数执行, 直到 等待的 Promise 成功或者失败

async function fetchData() {
    return "Data fetched successfully";
}

async function showData() {
    const data = await fetchData();
    console.log(data);
}

/* ************ */
async function processData() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (err) {
        console.log(err);
    }
}
async function processSequentAll() {
    const p1 = await fetchData1();
    const p2 = await fetchData2(data2);
    const p3 = await fetchData3(data3);
}
// 或者
async function processSequentAll() {
    const p1 = fetchData1();
    const p2 = fetchData2(data2);
    const p3 = fetchData3(data3);

    const result = await Promise.all([p1, p2, p3]);
    console.log(result);
}
  • 从 async/await 到 promise
function fetchDataAsPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("data fetched successfully");
        }, 1000);
    });
}
// 从 promise 到 async
async function useFetchDataAsPromise() {
    const data = await fetchDataAsPromise();
    console.log(data);
}
useFetchDataAsPromise();

JavaScript 事件循环

单线程的 JavaScript

JavaScript 是单线程的,这意味着它一次只能执行一个任务。然而,由于 JavaScript 的异步特性,它可以同时处理多个任务,而不会阻塞主线程。

  • JavaScript 是单线程的,但是它通过事件循环机制实现了异步编程
  • 事件循环机制包括两个主要部分:调用栈和任务队列
  • 调用栈用于执行同步代码,任务队列用于处理异步代码
  • 当一个异步操作完成时,它的回调函数会被添加到任务队列中,等待调用栈为空时执行
  • 事件循环会不断检查任务队列,如果有任务就将其添加到调用栈中执行
  • 事件循环机制使得 JavaScript 可以在等待异步操作完成的同时继续执行其他代码,从而实现非阻塞的异步编程

同步和异步

同步:代码按照顺序执行,一个任务执行完才会执行下一个任务

异步:代码可以同时执行多个任务,一个任务执行完不会等待其他任务执行完,而是继续执行下一个任务

JavaScript 本身是单线程的,为了处理异步任务,宿主环境(浏览器 /v8)会将其交给其他线程处理, 执行

事件循环

事件循环是宿主环境处理 js 异步操作的方式,让其能够非阻塞式运行的机制

  • 浏览器进程
    • 主进程,无论打开多少个浏览器窗口,它仅有一个,负责浏览器界面显示、用户管理、进程管理等
  • 网络进程
    • 处理网站的数据请求和响应,网络进程内部会开启多个线程,以实现网络请求的异步处理
  • 渲染进程
    • 主要解析 html、css、js 等资源,并生成渲染树、执行布局、绘制,负责页面渲染

浏览器中的 Event Loop

2025-03-15-22-02-46-image.png

  • 宏队列和微队列

    • 宏队列排队宏任务(DOM 操作回调,定时器回调,UI 绘制)

    • 微队列排队微任务(Promise 回调)

除了微队列外,队列的种类和数量可能不同,这取决与浏览器厂商

以 Chrome 为例:

  • 微队列:用于存放需要执行最快的任务,优先级最高,比如:promise.then(),MutationObserve

  • 交互队列:用于存放用户操作后产生的事件任务,优先级仅次于微队列

  • 延迟队列:用于存放定时器到达后的回调任务,优先级次于交互队列

人工合成的事件派发,即直接在代码里写的 dom.click()dispatchEvent()相对于浏览器而言并不是真正的用户交互,会被当作同步任务执行

执行栈和任务队列

JS 在解析一段代码的时候,会将同步代码顺序排在某个地方,即执行栈,然后依此执行里面的函数。

当遇到异步任务就交给其他线程处理,待当前执行栈所有的同步代码执行完成后,会从一个队列中取出已完成的异步任务的回调加入到执行栈中继续执行,遇到异步任务又交给其他线程……如此循环往复

2025-05-01-15-56-25-image.png

宏任务和微任务

任务队列不止一个,根据任务的种类不同,可以分为微任务(micro task)队列和宏任务(macro task)队列

事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列中是否有任务需要执行,如果没有,再去宏任务队列检查,如此往复

微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环

微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个

常见的宏任务

  • setTimeout()

  • setInterval()

  • setImmediate()

常见的微任务

  • Promise.then(),Promise.cathch()

  • new MutaionObserver()

  • processs.nextTick()

console.info('同步代码1')
setTimeout(()={
    console.info('setTimeot')
})
new Promise((resolve)=>{
    console.info('同步代码2')
    resolve()
}).then(()=>{
    console.info('promise.then')
})
console.info('同步代码3')

宏任务和微任务的本质区别

对于 promise.then(微任务),当执行到promise.then的时候,浏览器引擎并不会将异步任务交给浏览器其他线程,而是将回调存放在自己的一个任务队列中,待当前执行栈执行完成后,立马去执行promise.then存放的队列 promise.then微任务本身没有多线程参与 setTimeout 有“定时等待”的任务,需要定时器现行执行。ajax 请求有“发送请求”这个任务,需要 HTTP 线程处理 宏任务特征: 有明确的异步任务需要执行回调,并且需要等待异步任务执行完成,比如setTimeoutsetIntervalajax微任务特征: 没有明确的异步执行任务,不需要等待异步任务执行完成,比如promise.thenprocess.nextTick

视图更新渲染

视图重绘之前会先执行 requestAnimationFrame回调

浏览器 JS 异步执行的原理

浏览器是多线程的,当 js 需要执行异步任务的时候,浏览器会启动另一个线程去执行这个任务

2025-05-01-15-57-13-image.png

  • 微任务(microtasks):如 Promise.then(...)
  • 宏任务(macrotasks):如 setTimeout、click 事件
  • JS 执行顺序:同步任务 > 微任务 > 宏任务
const btn = document.getElementById("btn"); // 同步,获取元素

function test() {
    console.info("test");
    Promise.resolve().then((result) => {
        console.info("promise1");
    });
}

setTimeout(() => {
    console.info("set Timer"); // 宏任务①:执行顺序较后
    Promise.resolve().then(test); // 微任务将被加入下一轮微任务队列
}, 0);

btn.onclick = () => {
    console.info("click button"); // 注册点击事件
};

btn.click(); // 同步触发点击事件处理器

Promise.resolve().then(() => {
    console.info("promise2"); // 微任务
});

console.info("script end"); // 同步任务
// → 输出: "script end"

/* 实际输出结果如下 */
// click button
// script end
// promise2
// set Timer
// test
// promise1

渲染进程启动后,会开启一个渲染主线程,它是浏览器中最繁忙的线程,负责处理各种任务

  • 解析 html、css,计算样式、布局,构建 DOM 树和 CSSOM 树
  • 处理涂层,绘制页面
  • 执行 js 代码,包括同步代码和异步代码
  • 调用栈(Call Stack)
    • 用于执行同步代码,当调用栈为空时,事件循环会从任务队列中取出一个任务执行
  • 任务队列(Task Queue)
    • 用于存放异步任务,当异步任务完成时,它的回调函数会被添加到任务队列中,等待调用栈为空时执行
  • 微任务队列(Microtask Queue)

Node.js 事件循环

基于 Libuv 实现的,Libuv 是一个跨平台的异步 I/O 库,它提供了事件循环、文件系统操作、网络操作等功能 Libuv: 一个用 C 语言实现的高性能解决单线程非阻塞异步 I/O 的开源库

2025-05-01-16-11-29-image.png

Node.js Event Loop

2025-05-01-16-12-37-image.png

  • 宏队列
    • timers(重要)
    • penging callback
      • 调用上一次事件循环没有在 pool 阶段立即执行,而延迟的 IO 回调函数
    • idle prepare
      • 仅供 nodejs 内部使用
    • poll(重要)
    • check(重要)
    • close callback
      • 执行所有注册 close 事件的回调函数
  • 微队列
    • nextTick
    • Promise

timers

定时器队列,负责处理setTimeoutsetInterval的回调函数 不管是nodejs还是浏览器,所有的定时器回调函数都不能精准保证到达时间后立即执行

  • 一是因为计算机硬件和底层操作系统
  • 二是pool阶段对timers阶段的深刻影响。因为在没有满足pool阶段的结束条件前,就无法进入下一次事件循环的timers阶段

Pool

pool成为轮询队列,该阶段会处理timerscheck队列外的绝大多数 IO 回调任务,比如文件读取、监听用户请求等 当事件循环到达该阶段,它的运行方式是:

  • 如果pool队列中有回调任务,则依此执行回调,直到队列清空
  • 如果pool队列中没有回调任务
    • 如果其他队列中后续可能会出现回调任务,则一直等待,等其他队列中后续的回调任务来临时,结束该阶段
    • 如果等待的时间超过预设的时间限制,则也会自动进入下一次事件循环
    • 若其他队列中后续不可能出现回调任务了,则立即结束该阶段,并在本轮事件循环完成后,退出node程序
const fs = require("fs");
const start = Data.now();
setTimeout(() => {
    console.info("setTimeout exe", Data.now() - start);
}, 200);

fs.readFile("/index.js", "utf-8", (err, data) => {
    console.info("file read");
    const start = Data.now();
    while (Data.now() - start < 300) {}
});

check

check称为检查队列,负责处理setImmediate定义的回调函数 setImmediatenodejs特有的定时器,它会在当前事件循环的末尾执行回调函数,类似于setTimeout,但setImmediate的回调函数会在pool阶段结束后立即执行 在nodejs中,setImmediate的执行效率远远高于setTimeout,setImmediate的执行顺序无法预测

setTimeout(() => {
  console.info("setTimeout");
}, 0);
setImmediate(() => {
  console.info("setImmediate");
}

nextTick

我们可以通过process.nextTick()将回调函数加入到nextTick队列中,和通过Promise.resolve().then()将回调函数加入到Promise队列,并且nextTick队列中优先级高于Promise队列。所有process.nextTick()是 nodejs 执行最快的异步操作

Promise 面试

  • Promise 特点
  • 事件循环
  • 解题思路
console.info("1");
new Promise((resolve) => {
    resolve();
    console.info("2");
}).then(() => {
    console.info("3");
});
setTimeout(() => {
    console.info("4");
}, 0);
console.info("5");
  1. new Promise会立即执行,所以会先输出2
  2. resolve或者reject之后状态不再改变,但是后面代码会执行
  3. new Promise((res) => {
        res();
        console.info("test");
        reject();
    });
    
  4. promisethen(catch)回调放入到微任务队列,setTimeout放入到宏任务队列
  5. 调用栈中代码执行完后,先去微任务队列中的任务执行,直到微任务队列为空
  6. 微任务队列为空,取宏任务队列中的一个任务开始执行,然后重复上一步,直到宏任务队列为空