前端内练基础-彻底搞懂浏览器的Event-loop

1,383 阅读5分钟

背景

Event loop 是一个很重要的概念,本质上指的是计算机的运行机制,JavaScript语言采用的就是这种机制,众所周知JavaScript是单线程,为什么会设计成单线程呢?其实早在几年前阮一峰老师就给出了答案,这样的好处提升效率,同一个时间只做一件事。但也导致了一个问题:就是所有的任务都需要排队,只有前面的任务执行结束,才能执行后面的任务。JavaScript语言的设计者意识到这样不行,于是就把所有的任务分为两种:同步任务异步任务维护一个任务队列,主线程从任务队列中读取任务,整个过程是循环不断,这种机制称为Event loop 又叫 事件循环

为什么需要了解它

在实际的工作中,了解Event loop能帮助你分析一个异步次序的问题,除此之外还能对你了解浏览器Node的内部机制起到积极的作用,最主要的对于面试这是一个百分百会问到的问题。

浏览器的实现

浏览器中主要任务把分为两种: 同步任务、异步任务;

异步任务:Macrotask(宏任务)、Microtask(微任务),宏任务与微任务队列中的任务是随着:任务进栈、出栈、任务出队、进队之间交替进行。可以通过一个伪代码来了解一下这个概念:

// 任务队列(先进先出)
let EventLoop = []; 
let event;

// “永远” 执行
while (true) {
  // 一次tack

  if (EventLoop.length > 0) {
    // 拿到队列中的下一次事件

    event = EventLoop.shift();

    // 现在、执行下一个事件
    try {
      event();
    } catch (error) {
      // 报告错误
      reportError(error);
    }
  }
}

常见的Macrotask(宏任务)
  • script 标签
  • setTimeout
  • setInterval
  • setImmediate (Node环境中)
  • requestAnimationFrame
Microtask(微任务)
  • process.nextTick (Node环境中)
  • Promise callback 包括:()
  • MutationObserver

知道概念后 我们看一个简单的例子入手,先不必知道最后执行的打印的结果,你应该要清楚当前的代码那些是宏任务微任务

栗子🌰

console.log('start');  // 编号1

setTimeout(function () {  // 编号2
  console.log('timeout');
}, 0);

Promise.resolve().then(function () { // 编号3
  console.log('promise');
});

console.log('end'); // 编号4

实现的过程

2.gif

过程

  1. 运行时识别到了log方法将其入栈、然后执行输入start出栈
  2. 识别到了setTimeout为异步的方法(宏任务),把匿名回调函数放在(宏任务)队列中,在下一次事件循环中执行。
  3. 执行遇到promise callback、属于(微任务),放在(微任务)队列中。
  4. 运行时识别到了log方法将其入栈、然后执行输入end出栈。
  5. 主进程执行完毕,栈为空,随即从(微任务)队列取出队首的项,打印 promise、直到(微任务)队列没有数据
  6. 循环下一个(宏任务)队列,遵从先进先出的原则,打印出timeout

任务的类型

  • 编号1 : 同步任务
  • 编号2 : 宏任务
  • 编号3 : 微任务
  • 编号4 : 同步任务

执行的结果:

start
end
promise
timeout

趁热打铁在来一个

console.log('start');  // 编号1

new Promise(function(resolve, rejected){

    console.log('Promise-1')  // 编号 2

    resolve()

}).then(function(res){  // 编号 3

    console.log('Promise-2') 
})

setTimeout(function () {  // 编号 4
  console.log('timeout');
}, 0);

Promise.resolve().then(function () { // 编号5
  console.log('promise');
});

console.log('end'); // 编号6

实现运行过程:

其实这个例子跟上面的的唯一区别就是增加了一个 new Promise 也就是编号2 打印console.log('Promise-1')这个需要注意的是只有Promise callback属于异步任务的(微任务),但是在函数内部里面属于同步任务,很多人常常在这里搞混。

2.gif

结果:

start
Promise-1
end
Promise-2
promise
timeout

彻底解锁Event loop

console.log('1');

async function foo() {
    console.log('13');
    await bar();
    console.log('15');
}

function bar() {
    console.log('14');
}

setTimeout(function () {
    console.log('2');
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5');
    });
});

new Promise(function (resolve) {
    console.log('7');
    resolve();
}).then(function () {
    console.log('8');
});

setTimeout(function () {
    console.log('9');

    new Promise(function (resolve) {
        console.log('11');
        resolve();
    }).then(function () {
        console.log('12');
    });
});

foo();

实现运行过程:

第一次事件循环:

解析整个JavaScript文件处于一个宏任务中,遇到同步console.log('1')直接打印。接着执行遇到function没有进行调用直接跳过,来到第一个setTimeout,直接塞入到(宏任务)的Queue标记为macro1,接着解析到new Promise 执行里面的代码console.log('7'),遇到then塞入到(微任务)Queue中标记micro1,之后又遇到了setTimeout再次塞入到(宏任务)的Queue标记为macro2,最后到foo()函数,变量提升执行foo函数的遇到async只是标明当前的函数为异步,不影响函数的执行console.log('13'),遇到awiat bar 执行bar的console.log('14'),awiat阻塞后面的代码,放入微任务列表标记micro2

当前宏任务:

  • 执行的同步代码为[1、7、13、14]
  • 微任务Queue[8、15]
  • 宏任务Queue[macro1、macro2]

此时输入的结果: 1、7、13、14、8、15

输入完毕清空当前的微任务队列此时micro = []


第二次事件循环: 保持先进先出的形式、会执行第一个setTimeout输出console.log('2'),执行到new promise输入到console.log('4')、遇到then放在micro中重新标记为micro1,检查没有其他微任务的时候直接输出console.log('5')

当前宏任务:

  • 执行的同步代码为[2、4]
  • 微任务Queue[5]
  • 宏任务Queue[macro2]

此时输入的结果: 2、4、5

输入完毕清空当前的微任务队列此时micro = []


第三次事件循环: 跟上一次事件循环一样的顺序

当前宏任务:

  • 执行的同步代码为[9、11]
  • 微任务Queue[12]
  • 宏任务Queue[]

此时输入的结果: 9、11、12

输入完毕清空所有的任务队列

总结

在事件循环中分清什么是宏任务和微任务很关键、只有弄清楚了顺序才能知道当前事件执行的顺序,后面不管是在面试和工作中都会游刃有余。