Event Loop事件循环机制--图文并茂详解

5,989 阅读5分钟

概念

js的事件循环机制用来协调事件、用户交互、脚本执行、渲染、网络等等,这由一种叫做worker机制的代理去管理。 下图很好的展示了worker-事件循环机制——客户端之间的关系。worker协调管理事件机制的执行,将执行的结果反馈到Client。

为什么js要用这个机制呢?没有事件循环机制会发生什么? 众所周知,js是单线程语言,所有task都会在一个线程上完成,后一task必须在前一个task完成后执行,这很容易就引起了阻塞,体验很差。如下图,其中灰色背景部分是进程中阻塞阶段,网络请求只有等读取文件完全执行后,也就是waiting阶段完成后,才进行。

(图:同步事件执行)

而EventLoop会怎么做?引入Worker机制,可以实现多线程,(记住:js始终是单线程运行的),

js很多时候是在进行等待操作的,比如IO操作、网络请求、等待允许结果返回...,等,因此worker设置两个线程,一个主线程,负责程序本身的运行,另一个是负责主线程与其他进程之间通信(如IO操作)的EventLoop线程。如图,上面的箭头是主线程(Main Thread),下面的线程是EventLoop,主线程控制整个流程的进行,相当于控制器,EventLoop对主线程有协程控制作用,将主线程交给他的任务执行完成后,经结果返回给主线程,并执行后续的任务。两条线程是并行的,相互不会有阻塞、副作用。主线程可能会存在一些空闲状态,但不存在等待时间(因为主线程不会等待EventLoop事件执行完成,而是EventLoop主动通知到主线程,并将结果返回),也就是主线程的任务之间不会有阻塞(资源竞争),网络请求不必等文件读取完成后开始进行,EventLoop接到任务后空闲时立即执行。

(图:异步事件执行)

EventLoop

eventLoop是不浏览器专有的东西,是一种机制,NodeJs也有实现eventLoop,看下图,c++也有事件循环的使用,事实上任何一种语言,python、java都有事件循环机制,这是在特定需要的场景下诞生的。而js或者node的事件循环机制是为了解决js单线程带来的问题,只是各自的实现方式有差别。

宏任务和微任务&宏队列和微队列

EventLoop将任务分为两种,一种是宏任务和微任务。而对应的队列就是任务的集合(set),虽然称为队列(Queue),但是并不是每次要执行任务时,从可选队列中抢夺(grab)首个可执行任务,而不是从队列中出列(dequeuing)。我的理解是等任务执行完成后,才出列。

事件队列不是事件分发的唯一方式,有很多其他的任务采用的是其他的事件分发机制

宏任务
  • setTimeout
  • setInterval
  • setImmediate (Node独有)
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微任务

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

规则

举例说明

下面是一个实例方法exampFun,为了详细说明整个事件队列执行过程,我将它分的很细,注释部分表示对应的task的简称,如exampFun方法就是一个宏任务,命名exampFun,console.log(1)这段I/O任务命名为console_1,setTimeout内部的callback也会细分,如cb_fun_1。下面会依次较为直观地讲述这段代码的整体执行过程。

function exampFun(){//exampFun
    console.log(1); //console_1
    setTimeout(//setTimeout_1
        function(){//cb_fun_1
            console.log(2); //console_2
            //Promise
            Promise.resolve(1).then(
            function(){ //cb_fun_3
                //console_promise
                console.log('promise') 
            })
        })
    setTimeout(//setTimeout_2
        function(){ //cb_fun_2
        console.log(3); //console_3
    })
}

(1)exampFun会被推入栈(stack),默认主线程当前为空闲,微任务队列和宏任务队列为空

(2)main thread从stack中取出方法,将方法中的任务依次读取出来。

(3)main Thead 按序将task按类型分配给微任务和宏任务队列,恰好这么都是宏任务。

(4)宏任务中执行完,若有回调,直接将回调函数推入stack,根据上图,console不存在回调,运行完直接出队(控制台看到输出1)。因此,setTimeout_1执行完后,其回调函数cb_fun_1会被解析,将其内部方法进行任务的分配,其中console_2会被放到宏任务队列,Promise会被放到微任务队列。

(5)上一步的宏任务setTimeout_1执行完后,微任务队列不为空,开始执行微任务,直至全部执行完。promise执行完后,为其callback内的任务进行分配,console_promise插入到宏任务队列,预期结果如图:

(6)此前微任务列表为空,执行宏任务队列的首条task(setTimeout_2),执行完后,出列,并将callback内的任务进行分配,即console_3插入到宏队列

(7)最后宏任务依次执行,恰好都没有callback,因此依次输出3promise (8)执行结果验证,如图:

总结

eventLoop主要是控制宏队列和微队列中的执行,并且控制回调的继续运转。它不用去监控main thread如何传task给他,只检查队列的内容,并依据以下规则运转:

(1)宏任务优先级较低,若当前执行的是微任务,且微任务队列中任然有task,他会将其中的微任务全部执行完成

(2)当没有微任务时,才去执行宏任务中的第一条。一旦宏任务执行完,发现有微任务,将继续执行微任务,遵循(1)

(3)eventLoop是一个单线程,不存在同时执行微任务和宏任务,且每次只取一个任务执行

(4)mainThread相当于任务执行的总控制器,一边从stack读取数据,一边将task分配给evenLoop,eventLoop在根据任务的不同,分配到不同的队列,并进行后续的运算。任何一个有callback的任务,其callback的控制器继续交由eventLoop控制器进行再次分配,分配和执行方式依旧遵循上述规则