阅读 329

两张图,教你秒懂浏览器Event loop原理

事件环(Event loop)是什么?

首先我们都知道JS是单线程的,同一时刻只能做一件事。所有的任务都要依次排队等待主线程的执行。
为了提高JS执行的效率,JS设计者提出了异步的概念,异步任务分为很多种,例如:定时器任务、ajax请求、I/O操作等。不同的异步任务执行的优先级是不一样的,Event loop就是在JS开始执行时,给不同的任务进行排队的一个机制

了解Event loop之前需要知道的一些概念

  • 进程与线程
    进程是操作系统(cpu)分配资源的最小单位,线程是操作系统(cpu)调度任务的最小单位。
    线程是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。
    在浏览器中我们最关注的渲染引擎属于进程。它包括GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步http请求线程。

  • 宏任务与微任务
    macrotask(宏任务):可以理解是每次执行栈执行的代码就是一个宏任务。主要包括:主代码块、setTimeout、setInterval、setImmadiate(node的)、 I/O、UI rendering
    microtask(微任务):可以理解是在当前 task 执行结束后立即执行的任务。主要包括:promise的then、process.nexttick(node的)、MutationObserve(浏览器端的)、messageChannel(浏览器端的)

浏览器中的Event loop

  • 浏览器事件环原理
    在浏览中各线程之间任务流转的关系图:
    我们可以看到浏览器中有一个主线程(也就是JS引擎),JS开始执行的时候,所有的同步任务都放入主线程依次执行,形成一个主线程执行栈。
    主线程之外,还存在两个’任务队列’,微任务队列和宏任务队列。只要异步任务有了结果,可以开始执行了,任务所在的内核的所属线程就会将任务放入其所属的任务队列中。
    那么主线程执行栈和两个任务队列之间是怎么循环调度的呢?这也就是我们图中标红的部分。它的具体执行机制如下图所示:
  1. 首先JS开始执行,所有的同步任务都放入主线程执行栈中,依次开始执行。异步任务都放到各自的任务队列中等待执行。
  2. 一旦主线程执行栈中的任务都执行完毕,事件驱动机制会首先检查微任务队列,如果微任务队列中有等待中的任务,则将微任务队列中的任务全部取出,放入执行栈中依次执行。如果微任务队列中没有任务,则会从宏任务队列中取一个宏任务,放入执行栈中执行。
  3. 如果被放入主线程执行的任务中,发现有微任务或者宏任务,会将它们分配到各自的任务队列中,等待下一次事件循环中主线程来调用。
  4. 每一次主线程执行栈被清空后,都会重复2,3两步,直到所有的任务全部执行完毕。
  • 代码示例
    例1:

    console.log(1);
    setTimeout(function(){
        console.log('setTimeout1')
        setTimeout(function(){
            console.log('setTimeout1-1');
        })
    },0)
    setTimeout(function(){
        console.log('setTimeout2');
        setTimeout(function(){
            console.log('setTimeout2-1');
        })
    },0)
    console.log(2)
    复制代码

    执行结果:

    1. 代码开始执行,console.log(1)和console.log(2)被放入执行栈中,外层的两个setTimeout通过定时触发器线程计时后放入宏任务任务队列。
    2. 执行栈中代码开始执行,依次输出1和2
    3. 执行栈为空,检查微任务队列没有任务可执行,然后检查宏任务队列,取出一个宏任务放入执行栈,开始执行,输出setTimeout1,然后发现有setTimeout异步任务,将其交给定时触发器线程计时后放入宏任务队列,此时宏任务队列中情况如下:
      4.输出setTimeout1后,执行栈再一次被清空,于是再次去检查微任务队列和宏任务队列,然后取出任务一,放入执行栈,任务一执行后输出setTimeout2,将内部的setTimeout放入宏任务队列队尾 5.输出setTimeout2后,会重复上述检查微任务宏任务队列的步骤,输出setTimeout1-1,然后在次检查,输出setTimeout2-1

    例2:

    console.log(1);
    new Promise((resolve, reject)=>{
        console.log('promise');
        resolve();
    }).then((data)=>{
        console.log('then1');
    })
    setTimeout(function(){
        console.log('setTimeout1')
        setTimeout(function(){
            console.log('setTimeout1-1');
        })
        new Promise((resolve, reject)=>{
            console.log('inner-promise');
            resolve();
        }).then((data)=>{
        console.log('inner-then');
        })
    },0)
    setTimeout(function(){
        console.log('setTimeout2');
        setTimeout(function(){
            console.log('setTimeout2-1');
        })
    },0)
    console.log(2)
    复制代码

    执行结果:

    执行过程说明:

    1. 首先明确一点,promise中的代码都是同步执行的,promise.then才是异步的,且是微任务。因此首先输出的是1、promise、2,输出这些后任务队列中的情况如下图:
    2. 执行1之后,执行栈被清空,首先检查微任务队列,发下有任务,则取出放入主线程执行栈执行,输出then1,主线程任务栈清空。
    3. 再次检查微任务队列,没有发现排队中的微任务,开始检查宏任务队列,取出任务1,输出setTimeout1,然后将setTimeout放入宏任务队列,同步执行promise,输出inner-promise,将then放入微任务队列,主线程任务栈清空。此时任务队列的情况如下:
    4. 再次开始检查微任务队列,发现步骤3中放入的微任务,取出执行,输出inner-then,执行栈被清空
    5. 再次开始检查微任务队列,没有可执行的任务,于是检查宏任务队列,取出任务2,放入执行栈执行,输出setTimeout2,将任务2中的setTimeout放入宏任务队列(编号任务4),清空执行栈
    6. 再次开始检查微任务队列,没有可执行的任务,于是检查宏任务队列,取出任务3,输出setTimeout1-1,清空执行栈
    7. 再次开始检查微任务队列,没有可执行的任务,于是检查宏任务队列,取出任务4,输出setTimeout2-1,清空执行栈
    8. 再次开始检查微任务队列和微任务队列,发现没有可执行的任务,程序执行完毕。

小结

浏览器的事件环机制其实就是JS在执行过程中,安排各个任务如何排好队的机制。是主线程执行栈和微任务队列、宏任务队列之间的一个循环调度的机制。

我们需要注意的是主线程每次从宏任务队列里取任务,是一个一个的取的,而从微任务队列取任务时,是全部的取出。这也是浏览器Event loop和node的Event loop区别中的一点。