什么是EventLoop?

238 阅读6分钟

  我们知道js的最大特点就是单线程,就是同一时间只能做一件事。即便HTML5提出Web Worker允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

1、任务队列

  因为js的单线程的特性,所有运行在js线程中的代码需要根据某种规则来按队列执行。因为js中既有同步事件,又有异步事件,所以有时候我们在工作中写js代码时总会一脸懵逼,特别是在浏览器环境中进行断点调试时,所以弄懂js代码的执行顺序非常重要。

   按照js回调事件的特性,将任务分为同步任务和异步任务。

  • 1、所有同步任务都在主线程中执行,并形成一个执行栈,异步任务在有了运行结果之后才会将回调函数添加到任务队列中。
  • 2、代码运行时会先去执行主线程中的代码,等到主线程中的代码执行完毕后,才会去读取任务队列中的回调函数并放置到执行栈上来执行,先添加到任务队列中的会被先执行。等到任务队列中的回调函数执行完毕后,又会回到主线程上来。
  • 3、主线程中会不断重复执行第二步,并形成一个循环。这就是所谓的EventLoop。

2、事件队列

  任务队列按照某种条件又可以细分为microtask 和 macrotask,通常我们会称之为微任务和宏任务。代码执行的优先级为:主线程>微任务>宏任务。

  • 微任务主要有:ES6的Promise(then函数),node.js中的process.nextTick(),MessageChannel(消息通道,类似worker)
  • 宏任务主要有:整体代码script、setTimeout 和 setInterval、还有 MessageChannel,setImmediate, I/O

  js中代码执行的顺序是:首先执行主线程中的代码(宏任务),开始第一次循环,执行完毕后,再执行所有的微任务,然后再执行宏任务,看该宏任务中是否有微任务,如果有的话,就将所有的微任务执行完毕。再执行新的宏任务,如此不断的反复执行下去。
事件执行机制(即js代码执行机制)如下图(图片来自ssssyoki)所示。

详情请看如下代码

        //宏任务
        setTimeout(function(){
            console.log("我是宏任务1");
        })
        let p=new Promise((resolve,reject)=>{
            console.log("promise1");
            resolve();
        });
        //微任务
        p.then(res=>{
            console.log("我是微任务1")
        });
        let p2=new Promise((resolve,reject)=>{
            console.log("promise2");
            resolve();
        });
        //微任务
        p2.then(res=>{
            console.log("我是微任务2")
        });
        //宏任务
        setTimeout(function(){
            console.log("我是宏任务2");
        })
        console.log("我是主线程");
      

执行结果如下所示:

  代码解析:

  • 1、js代码开始执行后,遇到setTimeout,将其回调函数添加到宏任务队列中,记为setTimeout1;
  • 2、遇到Promise,Promise构造函数中的代码是同步任务,所以最先输出promise1,同时将then函数添加到微任务队列中,记为then1;
  • 3、接着再次遇到Promise,然后输出promise2,并将其then函数添加在微任务队列中,记为then2。
  • 4、执行到setTimeout,将其回调函数添加到宏任务队列中,记为setTimeout2。
  • 5、最后输出"我是主线程"。此时主线程中的代码执行完毕。此时任务队列中的情况如下。
宏任务 微任务
setTimeout1 then1
setTimeout2 then2
  • 6、接着开始执行微任务队列中的所有任务,输出"我是微任务1","我是微任务2",此时第一轮事件事件循环结束。
  • 7、随着setTimeout1回调事件的执行,意味着第二轮事件循环的开始,接着输出"我是宏任务1",它的结束意味着第二轮事件循环的结束。
  • 8、随着setTimeout2回调事件的执行,意味着第三轮事件循环的开始,它的结束就意味着第三轮事件循环的结束。
  • 9、开始第二次循环,执行第一个宏任务中的代码,并输出"我是宏任务1",接着输出"我是宏任务2"。 所以我的理解其实就是宏任务队列中有几个宏任务,就意味着在此次js代码执行过程中有几次事件循环。

3、浏览器环境中和node环境中EventLoop的异同

  示例代码如下所示;
   //宏任务,第二次事件循环的开始
        setTimeout(function() {
            console.log('setTimeout1');
            new Promise(function(resolve) {
                console.log('setTimeout1-Promise');
                resolve();
                //微任务
            }).then(function() {
                console.log('setTimeout1-then')
            })
        })
        new Promise(function(resolve) {
            console.log('Promise');
            resolve();
            //微任务
        }).then(function() {
            console.log('then')
        })
        //宏任务,第三次事件循环的开始
        setTimeout(function() {
            console.log('setTimeout2');
            new Promise(function(resolve) {
                console.log('setTimeout2-promise');
                resolve();
            }).then(function() {
                console.log('setTimeout2-then')
            })
        })

1、浏览器中js事件循环机制

  上面代码在浏览器环境中的输出结果如下所示:

  所以我得出结论:在浏览器环境下js代码的执行顺序是。主线程>微任务队列中所有的回调函数>宏任务队列中的所有宏任务。主线程和微任务成为第一次事件循环。宏任务队列中的一个宏任务就是一个事件循环。

2、node环境中js事件循环机制

  个人觉得node中的js执行机制比在浏览器中的要复杂一些。盗用掘金网友的一张图很好的解释了node环境中的事件循环机制。

  node环境中的eventLoop是按阶段来执行的,主要有6个阶段,这个阶段里的代码执行完毕,才会去执行下一个阶段里的代码。6个阶段中的代码都执行完毕才算是完成一个事件循环。

  • Node的Event Loop分阶段,阶段有先后,依次是
       1、expired timers and intervals,即到期的setTimeout/setInterval
       2、I/O events,包含文件,网络等等
       3、immediates,通过setImmediate注册的函数
       4、close handlers,close事件的回调,比如TCP连接断开
  • 同步任务及每个阶段之后都会清空microtask队列
       1、优先清空next tick queue,即通过process.nextTick注册的函数
       2、再清空other queue,常见的如Promise
  • 而和规范的区别,在于node会清空当前所处阶段的队列,即执行所有task

  而在node环境中的输出结果是这样的,两次执行结果还不一样

  上面的代码在node环境中的执行结果如下所示。

结果分析:

  • 首先执行主线程中的代码,输出promise1。
  • 接着清空微队列中的任务,即执行外层then函数中的代码,输出then1(微任务不在任何一个阶段执行,在各个阶段切换的中间执行)。
  • 接着开始执行timer阶段中的所有任务,所以此时输出setTimeOut1——>setTimeOut1-Promise——>setTimeOut2——>setTimeOut2-Promise。
  • timer阶段中的任务执行完毕后,又开始执行微任务队列中的所有任务。所以此时输出setTimeOut1-then、setTimeOut1-then2。

最后: node中的eventLoop运行机制比较复杂,所以还需要花费更多的时间去多多研究。

参考文档
1、这一次,彻底弄懂 JavaScript 执行机制
2、JavaScript 运行机制详解:再谈Event Loop
3、Eventloop不可怕,可怕的是遇上Promise
4、浏览器说:虽然都叫event loop,但是我和node不一样