摘要:浏览器的事件循环,前端再熟悉不过了,每天都会接触的东西。可以看到,所谓的并不是浏览器定义了哪些任务是,浏览器各个线程只是忠实地循环自己的任务队列,不停地执行其中的任务而已。
浏览器的事件循环,前端再熟悉不过了,每天都会接触的东西。但我以前一直都是死记硬背:事件任务队列分为macrotask和microtask,浏览器先从macrotask取出一个任务执行,再执行microtask内的所有任务,接着又去macrotask取出一个任务执行...,这样一直循环下去。但是对于下面的代码,我一直懵逼,setTimeout属于macrotask,按照上面的规则,setTimeout应该先被取出来执行啊,但是我却被执行结果打脸了。
经过再仔细看别人对任务队列的介绍,才知道,同步执行的js代码其实就算一个macrotask(准确说是每一个script标签内的代码都是一个macrotask),所以上面的规则中说的 先取出一个macrotask执行 是没有问题的。
网上很多文章都是像上面这样解释的,我也一直认为这是HTML对事件循环的规范,我们记着就是。直到最近看了李银城大佬的文章(见文末的参考链接),我才恍然大悟,之前看的文章都没有明确地从浏览器的多线程模型这个角度分析,所以让我们觉得浏览器的事件循环是基于上述的约定,但其实这是浏览器的多线程模型导致的结果。
macrotask本质上是浏览器多个线程之间通信的一个消息队列
在chrome里,每个页面都对应一个进程,该进程又有多个线程,比如js线程、渲染线程、io线程、网络线程、定时器线程等,这些线程之间的通信,是通过向对方的任务队列中添加一个任务(PostTask)来实现的。
浏览器的各种线程都是常驻线程,它们运行在一个for死循环里面,每个线程都有属于自己的若干任务队列,线程自己或者其它线程都可能通过PostTask向这些任务队列添加任务,这些线程会不断地从自己的任务队列中取出任务执行,或者是处于睡眠状态直到设定的时间或者是有人PostTask的时候把它们唤醒。
可以简单地理解为,浏览器的各个线程都在不停地从自己的任务队列中取出任务,执行,再取出任务,再执行,这样无限循环下去。
以下面的代码为例:
首先,script标签中的代码作为一个任务放入js线程的任务队列,js线程被唤醒,然后取出该任务执行
首先执行console.log(1),然后执行setTimeout,向定时器线程添加一个任务,接着执行console.log(3),这时js线程的任务队列为空,js线程进入休眠
大约1000ms后,定时器线程向js线程的任务队列添加定时任务(定时器的回调),js线程又被唤醒,执行定时回调函数,最后执行console.log(2)。
可以看到,所谓的macrotask并不是浏览器定义了哪些任务是macrotask,浏览器各个线程只是忠实地循环自己的任务队列,不停地执行其中的任务而已。
microtask比起macrotask是浏览器的多线程模型造成的“假象”,microtask是确实存在的一个队列,microtask是属于当前线程的,而不是其他线程PostTask过来的任务,只是延迟执行了而已(准确地说是放到了当前执行的同步代码之后执行),比如Promise.then、MutationObserver都属于这种情况。
以下面的代码为例:
首先,script标签中的代码作为一个任务放入js线程的任务队列,js线程被唤醒,然后取出该任务执行
然后执行new Promise以及Promise中的resolve,resolve后,promise的then的回调函数会作为需要延迟执行的任务,放到当前执行的所有同步代码之后
接着执行setTimeout,向定时器线程添加一个任务
此时同步代码执行完毕,接着执行被延迟执行的任务,也就是promise的then的回调函数,即执行console.log(3)
最后,js线程的任务队列为空,js线程进入休眠,大约1000ms后,定时器线程向js线程的任务队列添加定时任务(定时器的回调),js线程又被唤醒,执行定时回调函数,即console.log(2)。
总结通过上面的分析,可以看到,文章开头提到的规则:浏览器先从macrotask取出一个任务执行,再执行microtask内的所有任务,接着又去macrotask取出一个任务执行...,并没有说错,但这只是浏览器执行机制造成的现象,而不是说浏览器按照这样的规则去执行的代码。
最后,看了这篇文章,大家能够基于浏览器的运行机制,分析出下面代码的执行结果了吗(ps:不要用死记硬背的规则去分析哟)
console.log("start") const interval = setInterval(() => { console.log("setInterval") }, 0) setTimeout(() => { console.log("setTimeout 1") Promise.resolve() .then(() => { console.log("promise 3") }) .then(() => { console.log("promise 4") }) .then(() => { setTimeout(() => { console.log("setTimeout 2") Promise.resolve() .then(() => { console.log("promise 5") }) .then(() => { console.log("promise 6") }) .then(() => { clearInterval(interval) }) }, 0) }) }, 0) Promise.resolve() .then(() => { console.log("promise 1") }) .then(() => { console.log("promise 2") }) // 执行结果 /* start promise 1 promise 2 setInterval setTimeout 1 promise 3 promise 4 setInterval setTimeout 2 promise 5 promise 6 */参考
从Chrome源码看事件循环
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/99112.html
摘要:换句话说当一个异步过程调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过状态通知或回调函数处理这个调用。 JavaScript单线程机制 JavaScript的一个语言特性(也是这门语言的核心)就是单线程。什么是单线程呢?简单地说就是同一时间只能做一件事,当有多个任务时,只能按照一个顺序一个完成了再执行下一个 为什么JS是单线程的呢? JS最初被设计用在浏览器中,作为...
摘要:图片转引自的演讲和两个定时器中回调的执行逻辑便是典型的机制。异步编程关于异步编程我的理解是,在执行环境所提供的异步机制之上,在应用编码层面上实现整体流程控制的异步风格。 问题背景 在一次开发任务中,需要实现如下一个饼状图动画,基于canvas进行绘图,但由于对于JS运行环境中异步机制的不了解,所以遇到了一个棘手的问题,始终无法解决,之后在与同事交流之后才恍然大悟。问题的根节在于经典的J...
摘要:提出标准,允许脚本创建多个线程,但是子线程完全受主线程控制,且不得操作。所以,这个新标准并没有改变单线程的本质。事件循环主线程线程只会做一件事,就是从消息队列里面取消息执行消息,再取消息再执行。工作线程是生产者,主线程是消费者。 最近项目中遇到了一个场景,其实很常见,就是定时获取接口刷新数据。那么问题来了,假设我设置的定时时间为1s,而数据接口返回大于1s,应该用同步阻塞还是异步?我们...
阅读 1206·2019-08-30 15:55
阅读 954·2019-08-30 15:55
阅读 2149·2019-08-30 15:44
阅读 2879·2019-08-29 14:17
阅读 1129·2019-08-29 12:45
阅读 3301·2019-08-26 10:48
阅读 3132·2019-08-23 18:18
阅读 2599·2019-08-23 16:47