理解EventLoop:浏览器和nodejs

67 阅读5分钟

1. 理解EventLoop:浏览器

例子:
        console.log('start');  
        setTimeout(() => {  
            console.log('timeout');  
        });  
        Promise.resolve().then(() => {  
            console.log('resolve');  
        });  
        console.log('end');
分析一下:  
    1)刚开始整个脚本作为一个宏任务来执行,因此先打印start和end  
    2setTimeout 作为一个宏任务放入宏任务队列  
    3Promise.then作为一个为微任务放入到微任务队列  
    4)当本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行  
    5)接下来进入到下一个宏任务——setTimeout, 执行  
因此最后的顺序是:
    start  
    end  
    resolve  
    timeout


这样就带大家直观地感受到了浏览器环境下 EventLoop 的执行流程。不过,这只是其中的一部分情况,接下来我们来做一个更完整的总结。  
    1)一开始整段脚本作为第一个宏任务执行  
    2)执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列  
    3)当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空  
    4)执行浏览器 UI 线程的渲染工作  
    5)检查是否有Web worker任务,有则执行 (web worker 是运行在后台的JS,不会影响页面的性能。当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。) 
    6)执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空  

最后留一道题目练习:
    Promise.resolve().then(()=>{  
        console.log('Promise1')  
        setTimeout(()=>{  
            console.log('setTimeout2')  
        },0)  
    });  
    setTimeout(()=>{  
        console.log('setTimeout1')  
        Promise.resolve().then(()=>{  
            console.log('Promise2')  
        })  
    },0);  
    console.log('start');  
    // start  
    // Promise1  
    // setTimeout1  
    // Promise2  
    // setTimeout2

2. 理解EventLoop:nodejs

nodejs 和 浏览器的 eventLoop 还是有很大差别的,值得单独拿出来说一说。  
是否看过关于 nodejs 中 eventLoop 的一些文章, 是否被这些流程图搞得眼花缭乱、一头雾水:
image.png
这里会抛开这些晦涩的流程图,以最清晰浅显的方式来一步步拆解 nodejs 的事件循环机制。  

三大关键阶段  
    首先,梳理一下 nodejs 三个非常重要的执行阶段:  
        1)执行定时器回调的阶段。
            检查定时器,如果到了时间,就执行回调。这些定时器就是setTimeoutsetInterval。这个阶段暂且叫它 timer 。  
        2)轮询(英文叫 poll )阶段。
            因为在node代码中难免会有异步操作,比如文件I/O,网络I/O等等,
            那么当这些异步操作做完了,就会来通知JS主线程,怎么通知呢?
                就是通过'data''connect'等事件使得事件循环到达 poll 阶段。
            到达了这个阶段后:
                如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop将回到timer阶段。
                如果没有定时器, 会去看回调函数队列。  
                如果队列不为空,拿出队列中的方法依次执行  
                如果队列为空,检查是否有setImmdiate的回调  
                有则前往check阶段  
                没有则继续等待,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段。  
       3)check 阶段。这是一个比较简单的阶段,直接 执行 setImmdiate 的回调。  
    这三个阶段为一个循环过程。不过现在的eventLoop并不完整,我们现在就来一一地完善。  

完善  
    首先,当第 1 阶段结束后,可能并不会立即等待到异步事件的响应,这时候 nodejs 会进入到 I/O异常的回调阶段 。
    比如说 TCP 连接遇到ECONNREFUSED,就会在这个时候执行回调。并且在 check 阶段结束后还会进入到 关闭事件的回调阶段 。
    如果一个 socket 或句柄(handle)被突然关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。  

梳理一下,nodejs 的 eventLoop 分为下面的几个阶段:  
    1)timer 阶段  
    2)I/O 异常回调阶段  
  空闲、预备状态(第2阶段结束,poll 未触发之前)  
    3)poll 阶段  
    4)check 阶段  
    5)关闭事件的回调阶段

实例演示
    setTimeout(()=>{  
    console.log('timer1')  
    Promise.resolve().then(function() {  
    console.log('promise1')  
    })  
    }, 0)  
    setTimeout(()=>{  
    console.log('timer2')  
    Promise.resolve().then(function() {  
    console.log('promise2')  
    })  
    }, 0)



node版本 >= 11和在 11 以下的会有不同的表现。  
    首先说 node 版本 >= 11的,它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务。
        timer1  
        promise1  
        time2  
        promise2
    
    而 node 版本小于 11 的情况下,对于定时器的处理是:
        若第一个定时器任务出队并执行完,发现队首的任务仍然是一个定时器,
        那么就将微任务暂时保存,直接去执行新的定时器任务,
        当新的定时器任务执行完后,再一一执行中途产生的微任务。  
        因此会打印出这样的结果:
            timer1  
            timer2  
            promise1  
            promise2

3. nodejs 和 浏览器关于eventLoop的主要区别

两者最主要的区别在于
    浏览器中的微任务是在每个相应的宏任务中执行的,
    而nodejs中的微任务是在不同阶段之间执行的。