美文网首页前端开发那些事儿
深入理解js 事件循环机制(nodejs篇)

深入理解js 事件循环机制(nodejs篇)

作者: 跟屁虫丶 | 来源:发表于2020-01-07 18:19 被阅读0次

    浏览器篇简单补充:(阮一峰 http://www.ruanyifeng.com/blog/2014/10/event-loop.html) 

    * 任务队列:

      同步任务和异步任务咋来的?

        - 单线程意味着,所有任务需要排队,任务一个一个执行。

        - 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

        - js 设计者意识到,这时候主线程完全可以不管 I/O 这个挂起处于等待中的任务,完全可以先运行排在后面的任务,等到IO这边返回了结果再回头把挂起的任务执行。

     于是,所有任务就被分成两种: 一种 同步, 一种 异步。

          * 同步任务: 在主线程上排队执行的任务(这里会形成一个执行栈)

          * 异步任务: 不进入主线程,进入'任务队列'的任务,只有'任务队列'通知主线程,某个异步任务可以执行了,然后该任务在主线程执行完同步任务才会进入执行栈排队,等待被执行。

     具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

            1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

            2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

            3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,就会结束等待状态,进入执行栈,js 开始执行。

            4. 主线程不断重复上面的第三步。

      其实就是只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

    * 事件和回调函数:

      - "任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

      - "任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

      - 所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

      - "任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

    * Event Loop (事件循环)

    主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

      为了更好地理解Event Loop,看图:

    浏览器 js 主线程运行机制

      上面这张图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码有需要异步处理的(即各种WebAPI),它们在"任务队列"中加入需要在这些api中执行的各种回调事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行这些回调函数。

      即js主线程总是先执行栈中的代码(同步任务),再去读取执行"任务队列"(异步任务)。


    Node.js的Event Loop

        

    Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

    看图:

    node.js 运行机制


    根据上图,Node.js的运行机制如下。

      1. V8引擎解析JavaScript脚本。

      2. 解析后的代码,调用Node API。

      3. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

      4. V8引擎再将结果返回给用户。

    这里我要放上一张关于 浏览器和nodejs 对于 宏任务和微任务 支持与否的区别图:

    宏任务macrotask 微任务microtask

    很明显,除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick(微任务)和setImmediate(宏任务)。它们可以帮助我们加深对"任务队列"的理解。

      * process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。[微任务(=浏览器支持的promise.then),一定是在同步任务执行完之后执行,然后才是异步任务队列]

      * setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。[宏任务(=等同于setTimeout(fn,0)),是异步任务,会在同步任务和微任务执行完后执行]

      1. 来吧,先看看process.nextTick例子:

          process.nextTick(function A() {

          console.log(1);

            process.nextTick(function B(){console.log(2);});

          });

          setTimeout(function timeout() {

            console.log('TIMEOUT FIRED');

          }, 0)

          // 1

          // 2

          // TIMEOUT FIRED

      上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

    process.nextTick

    就像上边说的,这个可以认为是一个类似于Promise和MutationObserver的微任务实现,在代码执行的过程中可以随时插入nextTick,并且会保证在下一个宏任务开始之前所执行。

        - 像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!

            process.nextTick(function foo() {

              process.nextTick(foo);

            });

        - 由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

      2. 现在,再看setImmediate。

        - code1:

            setImmediate(function A() {

              console.log(1);

              setImmediate(function B(){console.log(2);});

            });

            setTimeout(function timeout() {

              console.log('TIMEOUT FIRED');

            }, 0);

            //node1 输出答案不一

                // 1

                // TIMEOUT FIRED

                // 2

                // TIMEOUT FIRED

                // 1

                // 2

        - code2:

            setImmediate(function (){

              setImmediate(function A() {

                console.log(1);

                setImmediate(function B(){console.log(2);});

              });

              setTimeout(function timeout() {

                console.log('TIMEOUT FIRED');

              }, 0);

            });

            // node2 答案不一

                //   1

                //   TIMEOUT FIRED

                //   2

                //   TIMEOUT FIRED

                //   1

                //   2

    * code1 和 code2 的输出结果总是不稳定的问题怎么解决呢?

      1. 方法一:确保这个循环的执行速度会超过定时器的倒计时

            setTimeout(_ => console.log('setTimeout'))

            setImmediate(_ => console.log('setImmediate'))

            let countdown = 1e9;

            while(countdown--) { } // 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`

            // 执行结果:一定是先输出setTimeOut

                // setTimeout

                // setImmediate

      2. 方法二: 如果在另一个宏任务中,必然是setImmediate先执行:

            require('fs').readFile(__dirname, _ => {

              setTimeout(_ => console.log('timeout'))

              setImmediate(_ => console.log('immediate'))

            }); 

            // 执行结果:

                // immediate

                // timeout

    相关文章

      网友评论

        本文标题:深入理解js 事件循环机制(nodejs篇)

        本文链接:https://www.haomeiwen.com/subject/wuiractx.html