• 理解 Node.js 的 Event loop


    问题

    考察如下代码,脑回路中运行并输出结果:

    console.log("1");
    

    setTimeout(function setTimeout1() {
    console.log("2");
    process.nextTick(function nextTick1() {
    console.log("3");
    });
    new Promise(function promise1(resolve) {
    console.log("4");
    resolve();
    }).then(function promiseThen1() {
    console.log("5");
    });
    setImmediate(function immediate1() {
    console.log("immediate");
    });
    });

    process.nextTick(function nextTick2() {
    console.log("6");
    });

    function bar() {
    console.log("bar");
    }

    async function foo() {
    console.log("async start");
    await bar();
    console.log("async end");
    }

    foo();

    new Promise(function promise2(resolve) {
    console.log("7");
    resolve();
    }).then(function promiseThen2() {
    console.log("8");
    });

    setTimeout(function setTimeout2() {
    console.log("9");

    new Promise(function promise3(resolve) {
    console.log("11");
    resolve();
    }).then(function promiseThen3() {
    console.log("12");
    });

    process.nextTick(function nextTick3() {
    console.log("10");
    });
    });

    JS 事件循环

    JS 是单线程,朴素地讲,同时只能完成一件事件。如果有耗时的任务,那后续的所有任务都要等待其完成才能执行。

    为了避免这种阻塞,引入了事件循环。即,将代码的执行分成一个个很小的阶段(一次循环),每个阶段重复相应的事情,直到所有任务都完成。

    一个阶段包含以下部分:

    • Timers:到期的定时器任务,setTimeoutsetInterval 等注册的任务。
    • IO Callbacks:IO 操作,比如网络请求,文件读写。
    • IO Polling:IO 任务的注册
    • Set Immediate:通过 setImmediate 注册的任务
    • Close:close 事件的回调,比如 TCP 的断开。

    image

    Ticks and Phases of the Node.js Event Loop 图片来自 Daniel Khan 的 Medium 博客,见文末

    同步代码及上面每个环节结束时都会清空一遍微任务队列,记住这点很重要!

    代码执行流程

    执行的流程是,

    • 将代码顺序执行。
    • 遇到异步任务,将任务压入待执行队列后继续往下。
    • 完成同步代码后,检查是否有微任务(通过 Promiseprocess.nextTickasync/await 等注册),如果有,则清空。
    • 清空微任务队列后,从待执行队列中取出最先压入的任务顺序执行,重复步骤一。

    另,

    • async/await 本质上是 Promise,所以其表现会和 Promise 一致。
    • process.nextTick 注册的回调优先级高于定时器。
    • setImmediate 可看成 Node 版本的 setTimeout,所以可与后者同等对待。

    示例代码分析

    Round 1

    • 首先遇到同步代码 console.log(1),立即执行输出 1
    • 接下来是一个 setTimeout 定时器,将其回调压入待执行队列 [setTimeout1]
    • 遇到 process.nextTick,将其回调 nextTick2 压入微任务队列 [nextTick2]
    • 然后是 async 函数 foo 的调用,立即执行并输出 async start
    • 然后是 await 语句,这所在的地方会创建并返回 Promise,所以这里会执行其后面的表达式,也就是 bar() 函数的调用。
    • 执行 bar 函数,输出 bar
    • 在执行了 await 后面的语句后,它所代表的 Promise 就创建完成了,foo 函数体后续的代码相当于 promise 的 then,放入微任务队列 [nextTick2, rest_of_foo]
    • 继续往下遇到 new Promise,执行 Promise 的创建输出 7,将它的 then 回调压入微任务队列 [nextTick2, rest_of_foo,promiseThen2]
    • 遇到另一个 setTimeout,回调压入待执行队列 [setTimeout1,setTimeout2]
    • 至此,代码执行完了一轮。此时的输出应该是 1, async start, bar,7

    Round 2

    • 查看微任务队列,并清空。所以依次执行 [nextTick2, rest_of_foo,promiseThen2],输出 6,async end,8

    Round 3

    • 查看待执行队列 [setTimeout1,setTimeout2],先执行 setTimout1
    • 遇到 console.log(2) 输出2
    • 遇到 process.nextTicknextTick1 压入微任务队列 [nextTick1]
    • 遇到 new Promise 立即执行 输出 4,执行 resolve() 后将 promiseThen1 压入微任务队列 [nextTick1,promiseThen1]
    • 遇到 setImmediate 将回调压入待执行队列 [setTimeout2,immediate1]
    • 此时 setTimeout1 执行完毕,此时的输出应该为 2,4

    Round 4

    • 检查微任务队列 [nextTick1,promiseThen1] 依次执行并输出 3,5

    Round 5

    • 检查待执行队列 [setTimeout2,immediate1],执行 setTimeout2
    • 遇到 console输出 9
    • 遇到 new Promise 执行并输出 11,将 promiseThen3 压入微任务队列 [promiseThen3]
    • 遇到 process.nextTicknextTick3 压入微执行队列。注意,因为 process.nextTick 的优化级高于 Promise,所以压入后的结果是: [nextTick3,promiseThen3]
    • 此时 setTimeout2 执行完毕,输出为 9,11

    Round 6

    • 检查微任务队列 [nextTick3,promiseThen3] 执行并输出 10,12

    Round 7

    • 检查待执行队列 [immediate1],执行并输出 immediate

    至此,走完了所有代码。

    结果

    以下是文章开头的结果:

    1
    async start
    bar
    7
    6
    async end
    8
    2
    4
    3
    5
    9
    11
    10
    12
    immediate

    参考

  • 相关阅读:
    冒泡排序&快速排序
    1252. Cells with Odd Values in a Matrix
    位运算小结
    832. Flipping an Image
    1812. Determine Color of a Chessboard Square
    10、属性、构造函数与析构函数
    09、封装与类成员
    07、面向对象简介
    06、C#异常处理
    03、运算符
  • 原文地址:https://www.cnblogs.com/Wayou/p/understanding_event_loop.html
Copyright © 2020-2023  润新知