图解事件循环:Node.js 和浏览器环境有何不同

542 阅读4分钟

在浏览器和 Node.js 中,事件循环的机制有一些相似之处,但也有显著的区别。今天主要从图形的角度谈一下个人对这两个事件循环的理解,不喜1楼喷

对于文字版本感兴趣可以移步@从面试中理解浏览器和node环境中的事件循环瞅瞅

上正文!

事件循环在 Node.js 和浏览器中的特性对比

下面是一个详细的对比表格,展示了 Node.js 和浏览器中的事件循环的主要区别和相似点:

特性/方面Node.js 事件循环浏览器事件循环
执行环境服务器端环境,专为高性能 I/O 操作设计客户端环境,专注于用户交互和界面渲染
事件循环阶段1. Timers
2. I/O callbacks
3. Idle, prepare
4. Poll
5. Check
6. Close callbacks
1. Macro-tasks (如 setTimeout, setInterval)
2. Micro-tasks (如 Promises, MutationObserver)
TimerssetTimeoutsetInterval 在 Timers 阶段执行setTimeoutsetInterval 在 Macro-tasks 阶段执行
Micro-tasksprocess.nextTickPromise 回调在每个阶段结束后执行Promise 回调和 MutationObserver 在 Macro-tasks 阶段之后执行
I/O callbacks处理大部分的回调,比如网络请求的回调主要处理用户交互和网络请求的回调
Poll 阶段处理新的 I/O 事件,执行 I/O 相关的回调,几乎所有的回调都在这个阶段被执行浏览器没有明确的 Poll 阶段,I/O 操作通常通过事件机制处理
Check 阶段专门处理 setImmediate 的回调浏览器没有 setImmediate
Idle, prepare 阶段内部使用,通常不涉及用户代码浏览器没有对应的阶段
Close callbacks处理 close 事件的回调,例如 socket.on('close', ...)浏览器没有明确的 Close callbacks 阶段
渲染不涉及 UI 渲染包含 UI 渲染,通常在事件循环的每个循环结束时进行
动画帧不涉及requestAnimationFrame 用于优化动画渲染
典型用例处理高并发 I/O 操作,如文件系统操作、网络请求等处理用户交互、DOM 操作、动画等

为了清晰地展示这些机制,我们可以画两张思维导图对比一下。

浏览器环境中的事件循环思维导图

image.png

Node.js 环境中的事件循环思维导图

image.png

浏览器环境中的事件循环流程图

image.png

Node.js 环境中的事件循环流程图

image.png

Node.js 示例代码解析

下面我们通过一个示例代码来展示 Node.js 中事件循环的工作机制:

const fs = require('fs');
const https = require('https');

// 模拟空闲和准备阶段的操作
function idlePrepareSimulation() {
    // 这是一个示例,Node.js 内部使用的阶段,通常不需要显式处理
}

// 读取文件内容
fs.readFile('data.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('Error reading file:', err);
        return;
    }
    console.log('File content read:', data);

    // 数据处理
    const processedData = processData(data);
    console.log('Data processed:', processedData);

    // 网络请求
    makeNetworkRequest(processedData, (response) => {
        console.log('Network request response:', response);

        // 进一步处理网络响应
        handleNetworkResponse(response);
    });

    // 设置一个定时器
    setTimeout(() => {
        console.log('setTimeout inside fs.readFile');
    }, 0);

    // 设置一个立即执行的回调
    setImmediate(() => {
        console.log('setImmediate inside fs.readFile');
    });

    // 设置一个下一个 tick 执行的回调
    process.nextTick(() => {
        console.log('nextTick inside fs.readFile');
    });
});

// 定时器阶段
setTimeout(() => {
    console.log('setTimeout 1');
    process.nextTick(() => {
        console.log('nextTick inside setTimeout 1');
    });
}, 0);

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

// 检查阶段
setImmediate(() => {
    console.log('setImmediate 1');
    process.nextTick(() => {
        console.log('nextTick inside setImmediate 1');
    });
});

setImmediate(() => {
    console.log('setImmediate 2');
});

// 定时器阶段的循环定时器
const intervalId = setInterval(() => {
    console.log('setInterval');
    clearInterval(intervalId); // 只执行一次
}, 1000);

// 关闭回调
const server = https.createServer((req, res) => {
    res.end('Hello World');
});

server.listen(3000, () => {
    console.log('Server listening on port 3000');
});

// 关闭服务器
server.close(() => {
    console.log('Server closed');
});

function processData(data) {
    // 模拟数据处理
    return data.toUpperCase();
}

function makeNetworkRequest(data, callback) {
    const options = {
        hostname: 'jsonplaceholder.typicode.com',
        path: '/posts/1',
        method: 'GET'
    };

    const req = https.request(options, (res) => {
        let responseData = '';

        res.on('data', (chunk) => {
            responseData += chunk;
        });

        res.on('end', () => {
            callback(responseData);
        });
    });

    req.on('error', (e) => {
        console.error('Problem with request:', e.message);
    });

    req.end();
}

function handleNetworkResponse(response) {
    // 模拟处理网络响应
    console.log('Handling network response:', response);
}

Node.js 示例代码的泳道图

为了更好地理解上述代码在事件循环中的执行顺序,我们可以使用一张泳道图来展示:

image.png

巴拉巴拉其他任务代指用户能介入的阶段

image.png

代码解析

在上述代码中,我们展示了 Node.js 中事件循环的各个阶段。以下是关键点:

  1. 文件读取 (fs.readFile):文件读取完成后,会依次执行 process.nextTicksetTimeoutsetImmediate 回调。
  2. 定时器阶段 (setTimeout):定时器回调会按照设定的时间执行,并在执行后立刻触发 process.nextTick 回调。
  3. 检查阶段 (setImmediate)setImmediate 回调会在当前事件循环的 check 阶段执行,并在执行后立刻触发 process.nextTick 回调。
  4. I/O 操作 (https.request):网络请求的回调在 poll 阶段执行,并在执行后处理响应数据。
  5. 关闭回调 (server.close):关闭服务器时,触发 close 回调。

通过这个示例,我们可以更好地理解 Node.js 中事件循环的各个阶段及其执行顺序。

总结

通过对比 Node.js 和浏览器中的事件循环机制,我们可以看到它们在设计上的差异和各自的优势。Node.js 更加专注于高性能 I/O 操作,而浏览器则更加注重用户交互和界面渲染。希望这篇文章能帮助你更好地理解事件循环的工作原理,我们下期再见~