JavaScript事件循环机制
一 .概念
1. 进程与线程
进程是资源分配最小单位,线程是程序执行的最小单位。
计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。
2. 一个进程包含多个线程
-
java c c++ 多线程编程 可以通过代码创建线程
- 多线程同时并发执行
-
javascript语言 单线程 不能自己创建线程
- javascript引擎单线程负责js代码
二.为什么javascript语言不能像java语言允许我们创建线程?
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,JavaScript就是单线程。
引入单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这同时又导致了一个问题:如果前一个任务耗时很长,后一个任务就不得不一直等着。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
三.javascript中的事件循环机制
JS是一门单线程语言,所有的任务都需要排队,前一个任务执行完成,才会执行下一个任务。如果JS执行的时间过长,就会造成页面的渲染不连贯,导致页面渲染加载阻塞。为解决此问题,JS引入了同步任务和异步任务。
同步任务: 主线程上的任务,按顺序依次执⾏,当前⼀个任务执⾏完毕后,才能执⾏下⼀个任务。
异步任务: 主线程外的任务,是进⼊任务队列的任务,执行完毕之后会产生一个回调函数,并且通知主线程。当主线程上的任务执行完后,就会调取最早通知自己的回调函数,使其进入主线程中执行。
微任务和宏任务
如上除了同步任务和异步任务的区分之外,异步任务还有更精确的区分:
- 微任务(micro-task) job
- 宏任务(macro-task) task
| 宏任务(macrotask) | 微任务(microtask) | |
|---|---|---|
| 谁发起的 | Node、浏览器 | JS引擎 |
| 具体事件 | script 网络请求(Ajax) setTimeout/setInterval dom 事件 postMessage MessageChannel setImmediate I/O(Node.js) | Promise.then async/awit MutaionObserver Object.observe(已废弃,Proxy 对象替代) process.nextTick(Node.js) |
| 谁先运行 | 后运行 | 先运行 |
| 会触发新一轮Tick吗 | 会 | 不会 |
1.一个简单的例子
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
setTimeout(() => {
console.log(3)
}, 0)
Promise.resolve().then(()=>{
console.log(5);
})
console.log(4)
//输出结果:1 4 5 3 2
同步任务和异步任务都是由js引擎来调度管理的,在这其中维护了一组任务队列(Event Queue);当执行到setTimeout时会将回调放入到宏任务队列,当执行到Promise then方法时会将会回调放入到微任务队列,当同步任务执行完成之后,就会去任务队列中的读取异步任务拿出来放到主线程中依次执行,首先会将微任务队列清空,然后再读取宏任务队列。
2.Event Loop 执行机制
- 进入到script标签,就进入到了第一次事件循环.
- 遇到同步代码,立即执行
- 遇到宏任务,放入到宏任务队列里.
- 遇到微任务,放入到微任务队列里.
- 执行完所有同步代码即执行栈清空
- 取出微任务队列代码到栈区执行
- 微任务代码执行完毕,本次队列清空
- 寻找下一个宏任务,重复5
以此反复直到清空所以宏任务,这种不断重复的执行机制,就叫做事件循环,流程图如下:
到这里你应该应该清楚上述代码的执行顺序的原因,但是这只是基于代码层面的,实际开发中往往更加复杂。
异步任务就只有微任务和宏任务吗?
思考下一下代码:
//黑色
document.body.style.background = 'black';
Promise.resolve().then(() => {
//红色
document.body.style.background = 'red';
}).then(() => {
//绿色
document.body.style.background = 'green';
});
requestAnimationFrame(() => {
//橙色
document.body.style.background = 'orange';
});
setTimeout(() => {
//蓝色
document.body.style.background = 'blue';
}, 200);
以上代码依次将body背景色改色,在变成橙色的一瞬间,最终变成了蓝色,你可以将其copy到控制台执行试试看效果。
结合上述讲解最终变成蓝色是没问题的,但是为什么会先变成橙色再变成蓝色呢,在这之前的黑色、红色和绿色呢?
3.从浏览器渲染顺序看异步执行机制
以上代码的运行结果在这里需要结合浏览器的渲染顺序来理解它。
我们来分析一下:
首先将body背景色变成黑色,然后遇到Promise then方法,依次将body背景色改成红色、绿色,我们之前提到Promise then方法属于微任务,该任务会在宏任务执行之前被全部清空,然后是执行requestAnimationFrame方法将body背景色改成橙色,最后是setTimeout宏任务将背景色改成蓝色。
这是代码的执行顺序,为什么之前的设置的背景色没有生效呢,只有requestAnimationFrame和setTimeout设置的生效了呢,很显然之前设置的被覆盖掉了。
结合同步任务和异步任务的讲解,我们知道同步代码先执行,首先设置背景色为黑色,然后清空微任务队列依次设置背景为红色、绿色,然后执行了requestAnimationFrame设置背景色为橙色,最后setTimeout将背景色变成蓝色。requestAnimationFrame是在setTimeout之前执行的,最后才算执行setTimeout,很显然requestAnimationFrame的执行时机比setTimeout更靠前,但是为什么会有先变成橙色再变成蓝色闪现的效果呢?
原因是在requestAnimationFrame和setTimeout执行顺序之间还穿插了GUI渲染操作,也就是我们经常说的浏览器绘制,当requestAnimationFrame执行完之后浏览器进行GUI渲染重新绘制页面,然后再执行setTimeout方法将背景色改成蓝色。
4.requestAnimationFrame
上述提到异步任务中分微任务和宏任务,那么requestAnimationFrame是什么东西呢,它是属于微任务还是宏任务呢?为什么它在setTimeout之前执行呢?我们可以在 MDN 看到关于requestAnimationFrame的描述:
window.requestAnimationFrame()告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行语法:
window.requestAnimationFrame(callback);
意思是说requestAnimationFrame是在浏览器绘制页面之前最后修改DOM元素的时机,文档中并未提到微任务和宏任务,这说明它并不属于这两者之间,它是独立于任务队列的,是由浏览器渲染进程来调度的,因为它独立于同步任务和异步任务,不存在同步异步阻塞的情况,所以一般实现动画效果使用它来实现比setTimeout更合适。
结合以上代码示例和讲解我们可以总结出javascript事件循环的执行顺序:
如上图所示,我将它分为两步,首先执行第一个宏任务,也就是script代码块,将script代码块中的同步任务放入主线程中执行,同步任务执行完成之后取出微任务中队列中的所有任务依次执行,然后执行requestAnimationFrame中的回调,其次是GUI渲染,最后执行setTimeout。第一步在脚本加载完成之后执行,其次不断循环第二步,这就是javascript事件循环的具体流程。
四.总结
- javascript是单线程语言,同步任务同步执行,异步任务异步执行
- 异步任务分为微任务和宏任务
- requestAnimationFrame是独立于任务队列的,它是浏览重新绘制页面之前操作DOM的最后时机