在浏览器中,JavaScript 的执行是单线程的。如何在单线程中实现异步操作呢?答案就是事件循环。
事件循环(Event Loop)
浏览器通过事件循环来处理事件、用户交互、JS 代码执行、渲染、网络请求等。通常又两种事件循环,一种是 Window 事件循环,一种是 Worker 事件循环。由于它们核心的工作原理相同,本文我们仅仅讨论 Window 事件循环。
事件循环,首先是一个循环,每个循环周期会执行一些代码,一个循环周期被称为 tick。
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
一个事件循环有一到多个任务队列。每个任务队列就是一个有序的任务列表。可以理解为一段要执行的代码,或者浏览器要执行的一个动作,比如发送事件、解析 HTML 等。
网页和浏览器本身的用户界面程序运行在相同的线程中,共享相同的事件循环。 该线程就是主线程,它除了运行网页本身的代码之外,还负责收集和派发用户和其它事件,以及渲染和绘制网页内容等。然后,事件循环会驱动发生在浏览器中与用户交互有关的一切。
执行过程
简略的说,事件循环在每一个循环周期都会顺序执行下面的步骤:
1.选择一个任务队列,从队列中取出最靠前(最老的)的任务。如果已经没有任务了,则跳到第 3 步。
2.执行取出的任务。
3.从微任务(Micro Task)队列中取出微任务执行,知道清空微任务队列。
4.更新渲染(resize、scroll、动画等)
5.返回第 1 步。
每一个时间循环都有一个微任务队列。微任务队列与任务队列很像,不同的地方在于,每次循环周期只会执行任务队列中的一个任务,在这期间产生的任何任务都只能在下一个循环周期中才能得以执行。在执行前任务后,事件循环会一次执行微任务队列中的每一个微任务,直到微任务队列为空。也就是说,在微任务执行过程中新产生的微任务,也会在当前循环周期内得到执行。
不同的任务队列可能有不同的优先级。比如浏览器可能会将用户鼠标和键盘输入(用户交互)的任务都放在一个任务队列中,其他任务放到另外一个队列中。在每个循环周期中优先从用户交互队列中取出任务执行,来保证及时响应用户操作。
下图展示了一个事件循环周期的执行过程。
任务与微任务
一个任务可以简单的理解为一段要执行的 JavaScript 代码。比如当执行 <script> 标签中的代码时,一个任务会被添加到任务队列中。事件的回调函数、setTimeout、setInterval 的回调函数都会作为任务放到任务队列中。
每个事件循环周期,事件循环会从任务队列中取出一个最老的任务执行,其他任务要等到下一个事件循环周期才会执行。
微任务与任务没有本质的区别,只是因为被放入的微任务任务队列。事件循环在每个循环周期都会清空微任务队列中的任务。
在浏览器的实现中,setTimeout
和setInterval
被放在了任务队列中,Promise
和 Mutation Observer API被放在的微任务队列中。我们也可以借助于 queueMicrotask()函数向微任务队列中添加任务。
执行异步代码
我们有很多种方式来执行异步代码。
回调函数
我们可以给一些事件添加监听,设置回调函数,从而在触发事件的时候执行函数。
buttonEl.addEventListener('click', () => { /* 点击响应 */ });
回调函数会被放到任务队列中执行。
setTimeout 和 setInterval
这两个函数大家都非常熟悉,setTimeout 会在指定的时间之后执行回调函数,setInterval 会每个一定的时间执行一次回调函数。这些回调函数都是以任务的形式被添加到任务队列中。
setTimeout(() => {
/* 1s 后执行 */
}, 1000);
setInterval(() => {
/* 每 500 ms 执行一次 */
}, 500);
注意,这两个函数都接收一个时间参数。但是实际运行的时候,事件循环并不会保证一定按照这个时间执行。
requestAnimationFrame
requestAnimationFrame 是一个特殊的工具函数,浏览器会在重绘页面之前调用这个函数设置的回调,允许我们在页面重绘之前更新页面。
通过这个函数,我们可以很好的在代码执行和运行设备的显示帧率(display frame rate)之间取得一个平衡。
假如,我们通过 setInterval 来控制动画,由于每个事件循环周期执行的时间不可控,而显示器的刷新频率是固定的(通常是 60Hz),因此如果我们的动画执行过快,会出现掉帧,执行过慢又会有卡顿现象。
requestAnimationFrame 会综合考虑显示器的刷新频率和代码的执行,以达到动画能够顺滑执行。
requestAnimationFrame(() => {
/* 执行一次 */
})
// 每个事件循环都会考虑执行
function alwaysRun() {
/* 函数逻辑 */
requestAnimationFrame(alwaysRun);
}
alwaysRun();
Promise 和 Async/await
Promise 和 Async/await 最终都会以 promise 的形式在代码中执行。
Promise 可以帮助我们管理有依赖关系的任务,同时可以一定程度的避免回调函数的回调地狱问题。
new Promise((resolve, reject) => {
/* 函数逻辑 */
})
.then()
.then() // 链式调用
Async/await 是 Promise 的语法糖,可以帮助我们实现以同步的形式编写异步代码,解决了回调地狱问题。
async function task() {
await someFunc();
await someOtherFunc();
}
Promise 和 Async/await 的任务都是微任务,会被放到微任务队列中。
事件循环搞懂了,面试也不怕了,为一劳永逸我把JS相关的面试题整理出来,碰上有没搞懂的,面试题会了也行哈哈哈哈。
JS题目
1.请你谈谈Cookie的优缺点
2.Array.prototype.slice.call(arr,2)方法的作用是:
4、简单说一下浏览器本地存储是怎样的
5.原型 / 构造函数 / 实例
6.原型链:
7.执行上下文(EC)
8.变量对象
9.作用域
10.闭包
11.对象的拷贝
12.new运算符的执行过程
13.instanceof原理
14.代码的复用
15.继承
16.类型转换
17.类型判断
18.模块化
19.防抖与节流
20.函数执行改变this
21.ES6/ES7
22.AST
23.babel编译原理
24.函数柯里化
25.get请求传参长度的误区
篇幅原因列举了一部分题目,如何获取这份优质的资料呢?
快速入手通道:点击这个,免费下载!诚意满满!!!
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编谢谢~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。