大厂面试 之事件循环(Event Loop)

368 阅读5分钟

一些面试题,看起来很简单,其实影藏这你没深入理解的知识点,及其容易弄错,现在收集起来,便于不断深入理解和温习复习

1、数组里面的函数

知识储备


function test() {
     console.log('this.length: ', this.length)
  }
  const aa = [test, 1, 3, 4, 5]
  console.log('aa0: ', aa[0]()) // 5

正题


window.onload = function () {
  window.length = 88
  function test() {
    console.log('this.length: ', this.length)
  }
  var obj = {
    length: 99,
    action: function (test) {
      test() // 88 是 window.lenth
      arguments[0]()// 2, 是数组的长度
    },
  }
  obj.action(test, [1, 2, 3])
}

2、事件循环 Event Loop

知识储备


执行流程

  1. 执行栈选择第一个进入任务队列的宏任务(通常是script整体代码),执行完该宏任务下所有同步任务
  2. 微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止
  3. 更新render(每一次事件循环,浏览器都可能会去更新渲染)
  4. 找到下一个执行的宏任务,开始第二个事件循环

宏任务(macrotask)

  • script(整体代码)、setTimeout、setInterval、UI 渲染、UI交互事件、 I/O、postMessage、 requestAnimationFrame、setImmediate(Node.js 环境)

微任务(microtask)

  • MutationObserver、Promise.then catch finally、process.nextTick(Node.js 环境)

正题


console.log('a')

setTimeout(function () {
  console.log('b')
}, 0)

new Promise((resolve) => {
  console.log('c')
  resolve()
})
  .then(function () {
    console.log('d')
  })
  .then(function () {
    console.log('e')
  })

console.log('f')
/**
* 输出结果:a c f d e b
*/

3、async 和 await

知识储备


  • async的作用: async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
  • await的作用: 都认为 await 是在等待一个 async 函数完成,因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

正题


function fn(){
    return new Promise(resolve=>{
        console.log(1)
    })
}
async function f1(){
    await fn()
    console.log(2)
}
f1()
console.log(3)
//1
//3
  • 这个代码因为fn是属于同步的,所以先打印出1,然后是3,但是因为没有resolve结果,所以await拿不到值,因此不会打印2
async function async1() {
    console.log("a");
    const res = await async2();
    console.log("b");
}

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

console.log("d");

setTimeout(() => {
    console.log("e");
}, 0);
// await 完毕 才 走.then
async1().then(res => {
    console.log("f")
})

new Promise((resolve) => {
    console.log("g");
    resolve();
}).then(() => {
    console.log("h");
});

console.log("i");

/**
* 输出结果:d a c g i b h f e 
*/

4、页面渲染


  • 所有JavaScript代码使用内嵌方式的话,浏览器会先把两个script丢到宏任务队列中去,因此执行的顺序也会不一样
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Event Loop</title>
</head>
<body>
    <div id="demo"></div>

    <script>
        const demoEl = document.getElementById('demo');

        console.log('a');

        setTimeout(() => {
            alert('渲染完成!')
            console.log('b');
        },0)

        new Promise(resolve => {
            console.log('c');
            resolve()
        }).then(() => {
            console.log('d');
            alert('开始渲染!')
        })

        console.log('e');
        demoEl.innerText = 'Hello World!';
    </script>
    <script>
        console.log('f');

        demoEl.innerText = 'Hi World!';
        alert('第二次渲染!');
    </script>
</body>
</html>
  • 输出:a c e d "开始渲染!" f "第二次渲染!" "渲染完成!" b

5、综合题


// async 返回的是一个promise generator + co
// await => yield  如果产出的是一个promise 会调用这个promise.then方法
async function async1() {
    console.log('async1 start');
    // 浏览器识别async + await await后面跟的是promise的话默认就会直接调用这个promise的then方法
    async2().then(()=>{
        console.log('async1 end');
    })  
    // await async2();
    // console.log(async1 end)
    new Promise((resolve,reject)=>resolve(async2())).then(()=>{
        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
*/
setTimeout(function () {
  console.log('1');
}, 0);

async function async1() {
  console.log('2');
  const data = await async2();
  console.log('3');
  return data;
}

async function async2() {
  return new Promise(resolve => {
    console.log('4');
    resolve('async2的结果');
  }).then(data => {
    console.log('5');
    return data;
  });
}

async1().then(data => {
  console.log('6');
  console.log(data);
});

new Promise(function (resolve) {
  console.log('7');
  //   resolve()
}).then(function () {
  console.log('8');
});
// 出结果:247536 async2 的结果 1
  • 注意!我在最后一个 Promise 埋了个坑 我没有调用 resolve 方法 这个是在面试美团的时候遇到了 当时自己没看清楚 以为都是一样的套路 最后面试官说不对 找了半天才发现是这个坑 哈哈

6、node.js事件循环原理


  • node 的初始化

    • 初始化 node 环境。
    • 执行输入代码。
    • 执行 process.nextTick 回调。
    • 执行 microtasks。
  • 进入 event-loop

    • 进入 timers 阶段

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

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

      • 这两个阶段与我们编程关系不大,暂且按下不表。
    • 进入 poll 阶段

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

        • 第一种情况:

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

          • 如果没有可用回调。
          • 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
      • 如果不存在尚未完成的回调,退出poll阶段。

    • 进入 check 阶段。

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

      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 closing 阶段
    • 检查是否有活跃的 handles(定时器、IO等事件句柄)。

      • 如果有,继续下一轮循环。
      • 如果没有,结束事件循环,退出程序。

细心的童鞋可以发现,在事件循环的每一个子阶段退出之前都会按顺序执行如下过程:

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