JavaScript目前已经使用非常广泛,前端和后端都可以使用JavaScript来进行编码。但作为后端程序员我其实了解JavaScript并不是很多。今天就来简单研究一下JavaScript的事件循环机制。
JavaScript是一个神奇的语言,由于其动态的特性,被戏称为上帝语言,每次都会想起这样一张图。
JavaScript的执行
JavaScript是单线程的非阻塞脚本语言。那么JavaScript是如何做到单线程又可以执行异步任务的呢。事件循环(Event Loop) 就是让JavaScript做到无阻塞的核心机制。
事件循环机制(Event Loop)
关于事件循环,在MDN上就已经有比较好的描述事件循环 - JavaScript | MDN
下面就简单介绍一下具体的异步执行流程。
JavaScript的任务根据事件的类型分为了两类,一类称为宏任务(Macro Task),一类称为微任务(Micro Task)。
宏任务:整体代码(main script),setTimeout,setInterval,I/O(鼠标键盘事件,网络请求事件),UI 渲染(HTML解析),postMessage,MessageChannel
微任务:Promise.then(),process.nextTick(),MutaionObserver
宏任务与微任务的区别在于队列中事件执行的顺序和优先级。
首先整体的script作为一个宏任务进入队列,主线程开始执行脚本。当执行栈执行完了之后,事件循环就会去检查微任务队列的事件,如果有,那么就循环将微任务事件推送到主线程执行微任务,当微任务执行完成后,再去检测宏任务队列中的事件,将宏任务事件推送到主线程执行宏任务,如此循环。
总结下来就是:
- 首先执行宏任务
- 宏任务完成,执行宏任务产生的微任务
- 微任务如果产生了新的微任务,则一直执行微任务
- 微任务完成,再回到宏任务
关于async/await的执行
async会返回隐式返回一个Promise作为函数的结果,当执行到await的时候,await之后的函数执行完,就会跳出当前async函数,将await之后的代码作为一个微任务加入到队列。
这里我认为有一篇博客讲的很好面试题:说说事件循环机制(满分答案) | 前端随笔 FE-Essay
async function async_fn_1() {
console.log('async_fn_1 start')
await async_fn_2()
console.log('async_fn_1 end')
}
async function async_fn_2() {
console.log('async_fn_2 start')
console.log('async_fn_2 end')
}
//开始脚本
console.log('script start')
//setTimeout
setTimeout(function () {
console.log('setTimeout')
}, 0)
//执行async_fn_1
async_fn_1()
console.log('script end')
-
上面代码的执行流程,首先将整体脚本作为第一个宏任务进入队列,输出
script start
-
执行到
setTimeout
,将里面的console.log(setTimeout)
假如宏任务队列 -
执行async_fn_1,输出
async_fn_1 start
,然后向下执行到await async_fn_2()
,这时将会先执行async_fn_2()
,输出async_fn_2 start
和async_fn_2 end
,然后将后面的console.log("async_fn_1 end")
假如微任务队列中,最后跳出async_fn_1函数 -
然后执行最后一行,输出
script end
。此时宏任务完成,开始执行微任务队列,输出async_fn_1 end
-
继续执行下一轮宏任务,输出
setTimeout
因此最后的输出就是这样的。
任务执行等待
JavaScript每一个任务执行完成才会执行另一个任务。这与后端的一些编程语言,比如C,Java等不同,他们的线程是时时刻刻都在抢占运行的,并且一个线程的任务可以被另一个线程终止。例如Java,通过让另一个线程抛出InterruptedException,或者直接设置一个exit flag。
JavaScript这样的模型有一个缺点,就是如果一个任务需要很长时间才能执行完成,那么之后的任务就只能一直等待,在浏览器中就表现为UI无法展示,Web应用程序无法与用户交互。浏览器一般会弹出一个 这个脚本运行时间过长 的对话框。这就需要开发者去缩短单个任务的处理时间,将一些耗时的任务拆分成多个小的任务。
Window.requestAnimationFrame()
关于这个方法在MDN上也有文档描述Window:requestAnimationFrame() 方法 - Web API | MDN
window.requestAnimationFrame()
方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。
每一轮事件循环的微队列被清空后,有可能会发生UI渲染,不同的浏览器有不同的优化策略,并不是每次循环都会发生UI渲染。重绘之前会调用window.requestAnimationFrame的回调函数。
在MDN文档里示例代码就展示了通过requestAnimationFrame回调来实现动画效果。
const element = document.getElementById("some-element-you-want-to-animate");
let start, previousTimeStamp;
let done = false;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
// 这里使用 Math.min() 确保元素在恰好位于 200px 时停止运动
const count = Math.min(0.1 * elapsed, 200);
element.style.transform = `translateX(${count}px)`;
if (count === 200) done = true;
}
if (elapsed < 2000) {
// 2 秒之后停止动画
previousTimeStamp = timestamp;
if (!done) {
window.requestAnimationFrame(step);
}
}
}
window.requestAnimationFrame(step);
在这个例子中,一个元素的动画时间是 2 秒(2000 毫秒)。该元素以 0.1px/ms 的速度向右移动,所以它的相对位置(以 CSS 像素为单位)可以通过动画开始后所经过的时间(以毫秒为单位)的函数来计算,即 0.1 * elapsed
。该元素的最终位置是在其初始位置的右边 200px(0.1 * 2000
)。
通过每次UI渲染时调用window.requestAnimationFrame设置callback,在事件循环完成UI重绘时修改元素的位置来实现动画。
以上就是个人对于JavaScript Event Loop的一些研究和总结。