用考试的思维去思考,我们怎么知道js会怎么执行下去,而当遇到前端关于事件循环的面试题时我们应该怎么去快速解题

125 阅读8分钟

对于考试的解题思维在我的理解中,就是我怎么能在不会的前提下还能把这题作对,所以这篇文章的重点在于怎么让你不会的前提下还能知道js会怎么执行下去。当然我们也需要粗浅的理解事件循环的基础知识。不过相对于要你完全理解事件循环机制,这可能是一条快速让你知道js会怎么执行的捷径。如果你想更快速的学会你可以尝试从事件循环机制的理解这一标题开始阅读,如果无法快速理解再往回看前面的部门

JavaScript的同步任务和异步任务

在讲事件循环之前我们先来了解一下JavaScript的同步任务和异步任务

同步任务(Synchronous): 同步任务是按照顺序依次执行的任务,每个任务的执行需要等待上一个任务完成。在同步任务执行期间,JavaScript 引擎会阻塞后续代码的执行,直到当前任务完成。常见的同步任务包括变量赋值、函数调用、循环等。

例如,下面的代码展示了同步任务的执行过程:

console.log("Start");

function syncTask() {
  console.log("Sync Task");
}

syncTask();

console.log("End");

在上述代码中,按照顺序依次执行了 console.log("Start")syncTask()console.log("End") 这些同步任务。

异步任务(Asynchronous): 异步任务是在将来某个时间点执行的任务,不会阻塞后续代码的执行。JavaScript 使用异步任务处理涉及到等待时间,例如网络请求、定时器操作、事件处理等。异步任务通常会将任务的处理委托给其他部分(浏览器环境、Node.js 环境等),然后继续执行后续代码。

异步任务一般使用回调函数、Promise、async/await 等方式来处理任务的完成或结果。

例如,下面的代码展示了异步任务的执行过程:

console.log("Start");

setTimeout(function () {
  console.log("Async Task");
}, 2000);

console.log("End");

在上述代码中,调用 setTimeout 创建了一个定时器异步任务,它会在 2000 毫秒(2 秒)后执行回调函数。在等待期间,JavaScript 引擎会继续执行后续代码,因此会先输出 "Start"、"End",最后在定时器触发后输出 "Async Task"。

微任务队列(Microtask Queue)和宏任务队列(Macrotask Queue)

然后我们还需要知道异步任务有微任务队列(Microtask Queue)和宏任务队列(Macrotask Queue)

在 JavaScript 中,存在两种任务队列:微任务队列(Microtask Queue)和宏任务队列(Macrotask Queue)。

  • 微任务队列: 微任务队列用于存放需要在当前任务执行完成后立即执行的任务,例如 Promise 的回调函数、process.nextTickMutationObserver 等。微任务具有高优先级,当当前任务执行完成后,JavaScript 引擎会立即检查并执行微任务队列中的任务,直到队列为空。

  • 宏任务队列: 宏任务队列用于存放需要在当前任务执行完成后稍后执行的任务,例如 setTimeout

事件循环的机制

了解完同步任务异步任务之后我们来简单了解一下事件循环的机制

JavaScript事件循环(Event Loop)是JavaScript执行环境中处理异步操作的一种机制。它负责管理JavaScript运行时的任务队列,并确定何时执行每个任务。

JavaScript是一种单线程语言,意味着它一次只能执行一段代码。然而,许多操作,如网络请求、文件读写和定时器等,都是异步的,它们不会立即返回结果,而是在某个未来的时间点提供结果。在这种情况下,JavaScript引擎不能一直等待结果返回,而是需要继续执行其他的代码。

事件循环的主要目标是在JavaScript引擎的单线程执行过程中处理这些异步操作。它通过将异步任务添加到任务队列中,并在合适的时机执行这些任务,实现异步代码的执行。事件循环的基本流程如下:

  1. 执行同步代码,直到遇到第一个异步操作。
  2. 将异步操作注册到相应的事件处理器中,以便在将来的某个时间点触发。
  3. 继续执行后续的同步代码。
  4. 当异步操作完成并触发相应的事件时,将其对应的回调函数添加到任务队列中。
  5. 当任务队列为空时,事件循环会从队列中取出一个任务,并执行其对应的回调函数。
  6. 执行完毕后,返回步骤5,直到任务队列为空。

然后这个逻辑构成的整个事件循环的机制确保了异步任务的执行顺序和同步代码的执行不会互相干扰。它使得JavaScript能够处理大量的并发操作,而不会阻塞整个程序的执行。

事件循环机制的理解

如果按照同步异步的执行顺序来说的话整个的话事件循环的执行过程大概是这样的:

  1. 执行同步任务:JavaScript 会按照代码的顺序执行同步任务,一次执行一个任务,直到所有同步任务执行完成。

  2. 处理微任务队列:在同步任务执行完成后,JavaScript 会立即处理微任务队列中的任务。微任务队列中的任务包括 Promise 的回调函数、process.nextTick、MutationObserver 等。JavaScript 会连续执行微任务队列中的任务,直到队列为空。

  3. 渲染更新:在处理完微任务队列后,如果需要进行页面渲染更新,JavaScript 引擎会执行相应的操作。

  4. 处理宏任务队列:在渲染更新完成后,JavaScript 会从宏任务队列中取出一个任务执行。宏任务队列中的任务包括定时器回调函数(如 setTimeout、setInterval)、事件回调函数等。JavaScript 会执行宏任务队列中的一个任务,然后跳回第 2 步,处理微任务队列。

  5. 重复以上步骤,不断处理微任务队列和宏任务队列,直到两个队列都为空。

在这一套机制之下JavaScript会先执行完所有同步任务,然后处理微任务队列中的任务,再进行渲染更新,最后才处理宏任务队列中的任务。

示例:

console.log('Start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('End');

在上述代码中,首先输出 "Start",然后创建一个定时器和一个 Promise,并将它们的回调函数添加到对应的队列中。最后输出 "End"。当所有同步任务执行完成后,JavaScript 会处理微任务队列中的 Promise 回调函数,输出 "Promise"。然后进行渲染更新(如果需要),最后才会处理宏任务队列中的定时器任务,输出 "setTimeout"。

简述对事件循环机制的理解

用一句简单的话来说就是同步任务>微任务>宏任务这样的顺序执行,然后执行完后看看有没有新的任务,有的话回调>执行>判断有没有>回调>执行。一直循环到没有任务

练习

了解完之后练习一下,如果你没有练习的打算下面这些就不用看了

console.log('1')

Promise.resolve()
    .then(() => {
        console.log('3')
    })

setTimeout(() => {

        console.log('4')

        setTimeout(() => {
            console.log('7')
        }, 0)

        Promise.resolve()
            .then(() => {
                console.log('6')
            })
        console.log('5')
    }, 0)
console.log('2')

解题思路

如何在未理解透彻的情况下做出来事件循环的题

当然前提是知道哪些是同步任务,哪些是异步任务,然后异步任务里面哪些是微任务哪些是宏任务

// 同步任务
console.log('1')

// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
    .then(() => {
        console.log('3')
    })


// setTimeout 的回调是宏任务,进入回调队列排队
setTimeout(() => {
//这里的任务都是属于setTimeout的回调这个宏任务体系下
        // 同步任务
        console.log('4')

        // setTimeout 的回调是宏任务2,进入回调队列排队
        setTimeout(() => {
            console.log('7')
        }, 0)

        // Promise 的回调是微任务,本轮调用末尾直接执行
        Promise.resolve()
            .then(() => {
                console.log('6')
            })
        //同步任务5
        console.log('5')
    }, 0)
    // 同步任务
console.log('2')
// 那如何用考试的思维去解题,把一道不会的题作对呢
// 如果我们将这个题想象成一个对象,每个执行的任务都是一个键值对以这样的方式解题
// 把普通任务写成写成一个key为同步任务的对象
// 把Promise的回调函数、process.nextTick、MutationObserver写成一个key为微任务的对象
// 把setTimeout当成一个key为宏任务的对象
//然后我们会得到一个这样的对象
// {
//     '同步任务': 1,
//     '微任务': {
//         '同步任务': 3
//     },
//     '宏任务': {
//         '同步任务': 4,
//         '宏任务': {
//             '同步任务': 7,
//         }
//         '微任务': {
//             '同步任务': 6,
//         },
//         '同步任务': 5,
//     }
//     '同步任务': 2,
// }
//然后我们在以同步》微任务》宏任务的顺序对对象的各层级进行排序
//我们就会得到这样一个对象
// {
//     '同步任务': 1,
//     '同步任务': 2,
//     '微任务': {
//         '同步任务': 3
//     },
//     '宏任务': {
//         '同步任务': 4,
//         '同步任务': 5,
//         '微任务': {
//             '同步任务': 6,
//         },
//         '宏任务': {
//             '同步任务': 7,
//         }
//     }
// }

//然后我们按照由上往下一行一行的输出,就得到1234567