JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,JavaScript只在一个线程上运行,同时只能执行一个任务,其他任务都必须在后面排队等待
同步与异步任务
在主线程上排队执行的任务是同步任务,这些任务一起形成执行栈
不进入主线程,而是在任务队列中等待进入主线程的,属于异步任务
一、js运行是单线程的
1.1 浏览器渲染进程的多个线程
事件监测和添加任务是独立于事件循环的,否则执行 js 时发生的事件将会被忽略;
- | 线程 | 职能 | - | - |
---|---|---|---|---|
1 | GUI渲染线程 | 解析html、css,构建DOM树、构建渲染树和绘制页面 | 重绘或回流 | - |
2 | 定时器触发线程 | setTimeout和setInterval所在的线程,负责计时 | 定时器到期 | →事件触发线程 |
3 | 异步http请求线程 | XMLHttpRequest连接后新开一个线程 | 状态变更而且有回调 | →事件触发线程 |
4 | 浏览器事件线程 | 监听浏览器事件(click,mouseover之类) | 用户触发浏览器事件 | →事件触发线程 |
5 | 事件触发线程 | 控制事件循环 | 接收到来自其他线程的事件触发 | 该线程会把事件添加到待处理队列的队尾 |
- | - | 宏任务队列维护在这个线程 | 当js引擎空闲时 | 取一个宏任务给js引擎执行 |
6 | js引擎线程 | 解析执行js代码(微任务队列维护在这个线程,所以同步代码执行完时先执行所有的微任务,然后空闲了,事件触发线程会推任务过来) | 当有宏任务来时 | - |
1.2 webworker
- 创建worker时,js引擎向浏览器申请开一个子线程
- 这个线程与主线程的上下文环境不一样,不能操作DOM,不能使用window对象(因为window是浏览器提供给js引擎的)
- 但是可以读取一些浏览器属性例如navigator,location
- JS引擎进程和worker进程之间通过浏览器提供的postMessage API互相通信
1.3 宏任务和微任务
宏任务 | 微任务 |
---|---|
维护在事件触发线程 | 维护在js引擎线程 |
宿主发起的任务(最通用的JavaScript宿主环境是网页浏览器或Node) | js引擎发起的任务 |
script整体代码块、setTimeout/setInterval注册的回调、http请求注册的回调 | promise.then/catch/finally注册的回调 、 MutationObserver (DOM 发生变化) 、 node.js中的 process.nextTick |
可以理解成js引擎自己能处理的异步就维护在自己的线程,属于微任务。
需要别人辅助的异步任务是在别人的线程里(定时器,http请求),这些任务属于宏任务
二、执行顺序
主线程执行栈为空 → 处理 所有 微任务队列中的任务 → 需要的话重新渲染页面 → 处理下一个宏任务 →
主线程执行栈为空 → 处理 所有 微任务队列中的任务 → ......
2.1 事件循环
<1> script代码块在主线程执行:所有同步代码立即执行,异步的进入Event Table并注册函数。 当指定的事情完成时,Event Table会将这个函数移入Event Queue。(比如定时器到期时或者Promise对象resolved时,这里会区分宏任务和微任务,进入各自对应的队列)
<2> 主线程内的任务执行完毕,微任务队列中的每个任务依次进入主线程执行
<3> 需要的话重新渲染页面
<1> 下一个宏任务进入主线程执行
<2> 执行每个微任务
<3> 需要的话重新渲染页面
<1> 下一个宏任务进入主线程执行
......
微任务队列和宏任务队列中的任务不断进入主线程执行,这个过程是循环不断的,这种运行机制又称为Event Loop,事件循环是js实现异步的一种方法,也是js的执行机制
这种机制保证了:虽然JavaScript是单线程的,但是对于耗时任务,可以将它丢到任务队列中,不影响主线程代码执行。
2.2 代码示例:
<script>
setTimeout(function () {
console.log('setTimeout');
})
new Promise(function (resolve) {
console.log('promise');
resolve()
}).then(function () {
console.log('then');
})
console.log('console');
</script>
主线程
- 遇到setTimeout,注册回调函数,并分发到宏任务Event Queue
- new Promise立即执行,输出promise
- then注册回调函数,并分发到微任务Event Queue
- console.log立即执行,输出console
检查微任务队列
- 执行then当时注册的回调函数,输出then
检查宏任务队列
- 执行setTimeout当时注册的回调函数,输出setTimeout
检查微任务队列
- 没有微任务
三、注意
-
代码不是任务,所以
console.log()
这句代码不是任务,更不是宏任务 -
单次事件循环最多处理一个宏任务,但所有微任务都会被处理;
-
当微任务队列执行完时,事件循环会检查是否需要更新 UI;如果一个宏任务执行完,微任务队列中有微任务,则微任务执行之前不允许重新渲染页面
-
单次事件循环不应该超过 16ms,因为浏览器试图每秒渲染 60 次,1000/60=16.66,因为任务执行时不能更新视图,所以超出这个时间会导致页面看起来卡顿;
-
使用定时器,可以将一个长时间的、阻塞页面的任务优化为多个任务,因为 2 个任务之间是可以渲染页面的;
四、setTimeout
-
setTimeout 是经过指定时间后,把要执行的任务加入到 Event Queue 中,主线程执行完后来 Event Queue 中读取,但是如果此任务之前的任务执行时间太久,会导致真正执行 setTimeout 回调的时间点超过预期设置的时间。MDN上给出的最小延迟时间是4ms
-
setTimeout(fn,0),我们想让fn在本次同步代码执行完之后尽可能早的执行。比如下面,我想让这个延时0的任务在延时500ms的任务之前执行,它一定能实现吗?
- 2.1 【√】 延时0的定时器先到期,先进入队列,先执行
setTimeout(() => {
console.log('执行500ms延迟任务');
}, 500);
console.log('同步');
setTimeout(() => {
console.log('执行0ms延迟任务')
}, 0);

- 2.2 【×】 延时500ms的定时器先到期,先进入队列,先执行。
因为同步代码执行的太慢啦,500ms延时的定时器都到期啦,主线程甚至还没有执行到第二个定时器那里,所以这个延时0的任务排在了延时500ms的任务之后
setTimeout(() => {
console.log('执行500ms延迟任务');
}, 500);
// 然后一段长时间执行的同步代码
console.time('test');
var arr = new Array(15000000).fill(10).map(a => {
return { num: 19 + a };
});
console.timeEnd('test');
setTimeout(() => {
console.log('执行0ms延迟任务')
}, 0);

- 2.3 【√】 把想尽早执行的代码设置成微任务,那它永远都会在延时500ms那个任务之前执行啦。
毕竟主线程执行完会先执行所有的微任务,然后才是宏任务。所以要插队还得是微任务
setTimeout(() => {
console.log('执行500ms延迟任务');
}, 500);
// 然后一段长时间执行的同步代码
console.time('test');
var arr = new Array(15000000).fill(10).map(a => {
return { num: 19 + a };
});
console.timeEnd('test');
// setTimeout(() => {
// console.log('执行0ms延迟任务')
// }, 0);
Promise.resolve().then(() => {
console.log('执行0ms延迟任务');
});

五、setInterval的2个问题
→ 如果主线程执行太久,期间也许有多次间隔定时器到期的行为,但如果到期时队列中有排队的此任务,该次到期就会被忽略,并不会重复添加。
→ 【但是】如果setInterval的回调函数fn执行时间超过了延迟时间。此时在浏览器的可能会表现为连续执行,也就是在浏览器中,如果到期时上个定时任务正在执行,并不会忽略本次到期,而是加入队列
由于setInterval的这两个问题,有时我们会使用setTimeout来模拟setInterval
问题1:到期被忽略
-
条件:主线程执行期间,定时器到期多次
-
设想:
如果在主线程执行期间,定时器到期多次,并每次都加入队列,那么在主线程执行完后,队列中等待的多个定时任务会连续执行,就无法达到2次定时任务之间至少间隔指定时间的效果了 -
测试:
// 1秒钟到期1次的定时器
var timer = setInterval(() => {
console.log('每间隔1s定时执行:', Date.now())
}, 1000);
// 执行时间较长的任务
console.time('test');
var arr1 = new Array(5000000).fill(10).map(a => {
return { num: 19 + a }
})
console.timeEnd('test')
console.log('主线程执行完毕-start', Date.now());

循环执行了30s,定时器每秒到期一次,如果这30s内每次定时器到期都将任务加入队列,则主线程执行完毕后会立即执行多个任务。测试截图中:主线程执行完毕后仅立即执行了一个定时任务,之后的都是间隔1s,说明这30s内就只加入了一个定时任务,不会重复添加
问题2:连续执行
- 条件:定时任务执行时间超过了间隔时间
- 设想:
假设定时任务每秒到期一次,执行每个定时任务需要3秒。也就是说,定时器到期时,可能有上个定时任务正在执行。如果此次也将任务加入队列,那么上个任务执行完也有可能会立刻执行,就无法达到2次任务之间的最小间隔时间了 - 测试:
【但是】在浏览器中测试了一下,确实是上次执行结束就立刻执行下次了,也就是说定时器到期时并未被忽略。一旦setInterval的回调函数fn执行时间超过了延迟时间,那么就完全看不出来有时间间隔了。
Chrome


网友评论