| JavaScript对话面试官系列 | - 事件循环

74 阅读5分钟

浏览器事件循环是什么?

浏览器的事件循环是一个我们编写的 JavaScript 代码和浏览器 API 调用的一个桥梁。

为什么要有事件循环?

我们是通过 JavaScript 的事件循环模型结合浏览器多线程管理回调函数来实现协程作为异步解决方案。

他是怎么样工作的?先做概述,在聊流程。

也就是将 DOM 操作,HTTP 请求,定时器等的回调函数都交给浏览器的对应线程来管理,有了操作结果之后就将回调函数加入 JavaScript 事件循环模型的消息队列当中,来等待 JavaScript 主线程的执行。

从同步任务和异步任务的角度来聊 JavaScript 事件循环?

浏览器 有一个 main thread 主线程/ JS 引擎线程 和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。Javascript 单线程任务又被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

从宏任务和微任务的角度来聊 JavaScript 事件循环?

从浏览器主线程的角度来讲,任务可以分为同步任务和异步任务, 而在 JavaScript 中,任务被分为两种,一种宏任务(MacroTask)也叫 Task,一种叫微任务(MicroTask)。

  • MacroTask(宏任务)

    • script 全部代码(普通任务)、setTimeout、setInterval、setImmediate、I/O、UI Rendering。
  • MicroTask(微任务)

    • Process.nextTick(Node 独有)、Promise、MutationObserver、垃圾回收

在每一个宏任务中会定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成才去执行下一个宏任务。

node 事件循环和浏览器事件循环的区别是什么?

两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,(在执行下一个宏任务的时候会清空微任务队列)而nodejs中的微任务是在不同阶段之间执行的。

node 事件循环流程?

  • node 的初始化

    • 初始化 node 环境。

    • 执行输入代码。

    • 执行 process.nextTick 回调。

    • process.nextTick 是一个独立于 eventLoop 的任务队列。在每一个 eventLoop 阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务执行。所以在 nodejs 事件循环的每一个子阶段退出之前都会按顺序执行如下过程:

      1. 检查是否有 process.nextTick 回调,如果有,全部执行。
      2. 检查是否有 microtaks,如果有,全部执行。
      3. 退出当前阶段。
    • 执行 microtasks

  • 进入 event-loop

    • 进入 timers 阶段

      • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。这些定时器就是setTimeout、setInterval
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入IO callbacks阶段。

      • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入 poll /轮询 阶段

      • 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。

        • 第一种情况:

          • 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
          • 检查是否有 process.nextTick 回调,如果有,全部执行。
          • 检查是否有 microtaks,如果有,全部执行。
          • 退出该阶段。
        • 第二种情况:

          • 如果没有可用回调。
          • 检查是否有 immediate 回调,如果有,退出 poll 阶段,进入 check 阶段。
      • 如果不存在尚未完成的回调,退出poll阶段。

    • 进入 check 阶段。

      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 check 阶段
    • 进入 closing 阶段 / 关闭事件的回调阶段。

      如果一个 socket 或句柄(handle)被突然关闭,例如 socket.destroy(), close 事件的回调就会在这个阶段执行。

      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 closing 阶段
    • 进行下一轮事件循环

#面试题

  • 做这类题,就从任务队列的三个部分开始写,main script , 宏任务, 微任务。如果定时器有时间的话,要按照先后顺序进入任务队列的宏任务。
  • 注意第三道面试题,返回的是普通值,正常加入微任务队列,返回的是 thenable 对象,推迟一次加入微任务队列,返回的是 promsie 对象,推迟两次加入微任务队列。

setTimeout(function () {
  console.log("setTimeout1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});
​
new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("then1");
});
​
setTimeout(function () {
  console.log("setTimeout2");
});
​
console.log(2);
​
queueMicrotask(() => {
  console.log("queueMicrotask1");
});
​
new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});

console.log("script end");
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

Promise.resolve()
  .then(() => {
    console.log(0);
    // 1.直接return一个值 相当于resolve(4)
    // return 4

    // 2.return thenable的值
    return {
      then: function (resolve) {
        // 大量的计算
        resolve(4);
      },
    };

    // 3.return Promise
    // 不是普通的值, 多加一次微任务
    // Promise.resolve(4), 多加一次微任务
    // 一共多加两次微任务
    return Promise.resolve(4);
  })
  .then((res) => {
    console.log(res);
  });

Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  })
  .then(() => {
    console.log(6);
  });

// 1.return 4
// 0
// 1
// 4
// 2
// 3
// 5
// 6

// 2.return thenable
// 0
// 1
// 2
// 4
// 3
// 5
// 6

// 3.return promise
// 0
// 1
// 2
// 3
// 4
// 5
// 6

  • mainScript -> nextTick -> 微任务 -> 宏任务(setTimeoutsetImmediate)。
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout0");
}, 0);

setTimeout(function () {
  console.log("setTimeout2");
}, 300);

setImmediate(() => console.log("setImmediate"));

process.nextTick(() => console.log("nextTick1"));

async1();

process.nextTick(() => console.log("nextTick2"));

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
  console.log("promise2");
}).then(function () {
  console.log("promise3");
});

console.log("script end");

// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nextTick1
// nextTick2
// async1 end
// promise3
// setTimeout0
// setImmediate
// setTimeout2