一、请简述浏览器的事件循环机制(Event Loop)基本原理
浏览器的事件循环机制是用于协调处理 JavaScript 中的异步任务与同步任务执行顺序的一种机制,它确保了代码能够按照合理的顺序执行,避免阻塞页面渲染等情况。其基本原理如下:
- 任务分类:
- 同步任务:指那些会立即执行的任务,按照代码编写的顺序依次执行,比如简单的变量声明、函数调用(非异步相关的)等,它们会在主线程上按顺序被执行完。例如这里的变量声明和函数调用及最后的
console.log
操作都是同步任务,会依次执行。 - 异步任务:不会立即执行,而是会在合适的时机被放入任务队列(也叫消息队列)中等待执行,常见的异步任务有定时器任务(
setTimeout
、setInterval
)、DOM 事件回调、网络请求回调(如fetch
回调等)。例如:这个
setTimeout
里的回调函数就是异步任务,会等待 1000 毫秒后被放入任务队列。 - 执行流程:
- 首先,JavaScript 主线程会先执行所有的同步任务,在执行同步任务过程中如果遇到异步任务,会将异步任务交给对应的浏览器模块(比如定时器交给浏览器的定时器模块管理,网络请求交给网络模块等),主线程继续执行后续的同步任务,直至所有同步任务执行完毕。
- 主线程任务执行完后,会去检查任务队列(消息队列),如果任务队列中有任务,就会按照先进先出的顺序将任务取出,放到主线程中执行,每执行完一个任务,就会再次查看任务队列是否还有任务,如此循环往复,这个不断检查并执行任务队列中任务的过程就是事件循环。
- 同步任务:指那些会立即执行的任务,按照代码编写的顺序依次执行,比如简单的变量声明、函数调用(非异步相关的)等,它们会在主线程上按顺序被执行完。例如这里的变量声明和函数调用及最后的
二、宏任务(Macro Task)和微任务(Micro Task)分别有哪些,它们在事件循环中的执行顺序是怎样的?
- 宏任务示例及来源:
- 常见的宏任务类型:包括
setTimeout
、setInterval
、I/O
操作(比如读取文件等,不过在浏览器端主要涉及网络请求等类似的异步I/O
)、script
(整体的 JavaScript 脚本代码初始执行也可看作一个宏任务)、UI render
(浏览器的页面渲染工作通常也会在特定的宏任务执行间隙进行)等。例如: -
宏任务执行特点:宏任务会按照它们被添加到任务队列的顺序依次执行,每次执行完一个宏任务后,如果当前宏任务执行过程中产生了微任务,会先处理完微任务,再去执行下一个宏任务。
-
微任务示例及来源:
- 常见的微任务类型:主要有
Promise.then()
、MutationObserver
(用于监听 DOM 变化的回调)、process.nextTick
(在 Node.js 环境中,不过面试中常和浏览器事件循环对比提及)等。例如:这里Promise.resolve().then()
里的回调就是微任务。 - 微任务执行特点:微任务会在当前宏任务执行结束后、下一个宏任务开始执行前被执行,并且微任务队列中的所有微任务会一次性执行完,如果在执行微任务过程中又产生了新的微任务,会继续执行新产生的微任务,直到微任务队列清空,才会进入下一个宏任务的执行阶段。
-
所以整体的执行顺序大致是:同步任务 -> 微任务队列(清空) -> 宏任务 1 -> 微任务队列(清空) -> 宏任务 2 -> 微任务队列(清空)…… 以此类推。
- 常见的微任务类型:主要有
- 常见的宏任务类型:包括
三、为什么 setTimeout
回调函数的执行时间可能会不准确,结合事件循环机制解释一下
虽然 setTimeout
可以设置一个延迟时间,比如 setTimeout(() => { console.log('test'); }, 1000);
看似是 1000 毫秒后执行回调函数,但实际执行时间可能不准确,原因如下:
-
任务队列排队问题:
setTimeout
只是将回调任务添加到了任务队列中,当设定的延迟时间到了之后,它的回调任务才会被放入任务队列,但它具体何时能执行取决于任务队列前面还有多少任务在排队等待执行。例如,如果前面已经有大量的宏任务或者微任务在排队,即使到了设定的延迟时间,它也得等前面的任务都执行完才能轮到它执行,所以实际执行时间就会晚于设定的延迟时间。 -
浏览器资源分配情况:
浏览器的资源是有限的,主线程需要兼顾很多方面,比如页面渲染、处理用户交互等同时还得执行任务队列中的任务。有时候如果浏览器正在忙于处理页面渲染或者其他高优先级的任务,那么对于任务队列中setTimeout
回调这样的任务处理就会有延迟,导致其执行时间不准。 -
最小延迟限制:
大部分浏览器为了性能等因素考虑,对setTimeout
有一个最小延迟限制,通常是 4 毫秒左右(不同浏览器可能略有差异)。也就是说,即使你设置的延迟时间是 0 毫秒,实际上它最少也得等待大概 4 毫秒后才可能被放入任务队列(只是可能,还得看前面排队情况),这也会使得执行时间和预期设置的不一致。
四、如何理解 async/await
与事件循环机制的关系?
async/await
是基于 JavaScript 中的 Promise 和生成器(generator)等底层机制实现的一种用于处理异步代码的语法糖,它和事件循环机制密切相关:
async
函数本身的执行性质:async
函数在执行时,它内部的代码一开始是同步执行的,直到遇到第一个await
关键字。例如这里
console.log('开始执行 async 函数')
会先同步执行,当遇到await
时,async
函数会暂停执行,并将await
后面跟着的 Promise 的then
方法(也就是await
等待 Promise 状态变为fulfilled
后要执行的后续逻辑)作为一个微任务添加到微任务队列中,然后主线程继续执行其他同步任务(比如上面示例中的console.log('外部同步任务')
)。await
与微任务的关联:
当await
所等待的 Promise 状态变为fulfilled
时,其对应的then
回调(也就是await
后续的代码逻辑)作为微任务会在当前宏任务执行结束后被执行,这符合微任务在事件循环中的执行顺序规则。例如,继续完善上面的示例:执行顺序会是:先执行console.log('开始执行 async 函数')
,然后遇到await
,暂停async
函数执行,添加await
后续逻辑为微任务,接着执行console.log('外部同步任务')
,再执行Promise.resolve().then()
这个额外的微任务,最后执行await
之后async
函数里的console.log('await 之后继续执行,结果为:', result)
等后续逻辑,这个过程充分体现了async/await
遵循事件循环机制中微任务的执行顺序特点,让异步代码可以更清晰地按照顺序执行,类似同步代码的书写体验。
五、结合事件循环机制,说说页面渲染和 JavaScript 执行是如何协调的?
在浏览器中,页面渲染和 JavaScript 执行的协调是通过事件循环机制来实现的:
- 初始页面渲染与 JavaScript 执行顺序:
当浏览器加载一个网页时,首先会解析 HTML 文档构建 DOM 树,在这个过程中如果遇到script
标签内的 JavaScript 代码(内联代码或者外部引入的脚本文件),会暂停 DOM 树的构建,先去执行 JavaScript 代码(此时 JavaScript 代码作为一个宏任务开始执行),等 JavaScript 代码执行完后,再继续构建 DOM 树。例如,以下简单的 HTML 页面结构示例:浏览器先解析到script
标签时,会先执行里面的console.log('页面中的JavaScript代码执行')
这个同步任务,然后再继续解析后面的 HTML 元素构建 DOM 树,创建#app
这个div
元素对应的 DOM 节点等后续操作。 - 后续页面渲染时机:
在 JavaScript 执行的过程中(每个宏任务执行阶段),如果修改了 DOM 元素相关属性或者结构等,浏览器并不会立即进行页面渲染,而是等到当前宏任务执行完毕,并且微任务队列也清空后,才会根据修改后的 DOM 情况进行页面渲染(将新的 DOM 结构、样式等呈现到页面上),这个页面渲染操作本身也是一个宏任务的一部分(可以理解为浏览器内部的一个特定宏任务环节)。例如:所以,通过事件循环机制,页面渲染和 JavaScript 执行有序地交替进行,既保证了 JavaScript 代码能够按规则操作 DOM 等资源,又能适时地将页面更新呈现给用户,避免渲染的混乱和无序。
总之,掌握浏览器的事件循环机制对于理解前端代码的执行顺序、异步处理以及页面渲染等诸多方面都非常关键,在前端面试中也是经常考查的重点内容。