阿尔卑斯丶
圣骑士
圣骑士
  • 最后登录2023-11-03
  • 发帖数59
  • 社区居民
  • 原创写手
阅读:728回复:0

[es 6]事件循环

楼主#
更多 发布于:2023-07-26 11:50
最近在网上重新了解一下事件循环,看到一个不错的讲解,分享一下。


说事件循环离不开的话题,浏览器的进程与线程。
浏览器是多进程多线程的应用程序,浏览器为了减少崩溃的几率,因此在浏览器启动的时候就会打开多个进程,如图:


图片:浏览器任务管理器.png


浏览器的主要进程包括浏览器进程、网络进程和要详细说的渲染进程。
渲染进程开启后会启动一个最主要的线程,渲染主线程,当然渲染进程还包括很多其他内容,如合并线程等其他线程。
渲染主线程的工作有很多,例如解析HTML、CSS、进行样式计算、执行全局js、执行事件处理函数等。渲染主线程的工作难点是什么呢?是调度任务。
比如:

  • 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?

  • 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?

  • 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?

绝妙办法就是排队。

图片:渲染主线程.png



如果查看浏览器源码我们能够发现:
浏览器会在最开始的时候,渲染进程会进入一个死循环;

图片:浏览器事件循环源码.png






每次会检查消息队列是否有任务,有就取第一个,没有就休眠。
 所有其他线程(包括其他进程)都可以向消息队列添加任务,新任务加在末尾,消息队列没有任务,主线程就会休眠。这就是事件循环。
仔细看会发现这样有bug,如果这样的话,有任务有个10分钟的计时器怎么办,总不能让主线程执行这个任务的时候等10分钟吧?
 这就需要异步。
 文字描述就是:
 如果消息队列中出现需要计时器等类似任务时,渲染主线程会通知计时线程开始计时,然后就会开始从消息队列获取新的任务执行,当计时线程结束,计时线程会把计时器回调放在消息队列末尾排队。
 以上可以保证渲染主线程不堵塞。上边看着没问题了,但是如果有紧急任务怎么办呢?任务有优先级吗?
 不,任务没有优先级,总不至于让任务执行一半去执行其他任务。
 但是,消息队列有优先级。
 W3C解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。

  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行

  按照当下W3C提供的标准,已经不再使用宏队列的说法了。在当前的谷歌浏览器中,至少包含下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」

  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」

  • 微队列:用户存放需要最快执行的任务,优先级「最高」

  添加任务到微队列的主要方式主要是使用 Promise、MutationObserver例如:让一个函数编程vip,添加到微队列的办法:
 Promise.resolve().then(函数)
有了上述描述,来看一些相关的面试题:

setTimeout(() => {
console.log(1); 
Promise.resolve().then(() => { console.log(7) }) 
}, 0); 
console.log(2); 
Promise.resolve().then(() => { console.log(3) }) 
setTimeout(() => { 
console.log(8); 
setTimeout(() => { console.log(5) }, 0) 
}, 0);
setTimeout(() => { 
Promise.resolve().then(() => { console.log(4) })
}, 0);
console.log(6);

 代码格式化的不好看,凑合凑合。
 分析:
 渲染主线程会先执行js全局代码:
 从上往下看:
 1、看到计时器就先放到计时线程不管他;
 2、看到console.log(2),先执行它,输出2;
 3、看到promise,放到微队列排队;
 4、看到计时器,放到计时线程不管他,继续;
 5、再一个计时器,添加计时线程,继续;
 6、看到console.log(6),输出6;
主线程执行完全局,开始从消息队列获取任务执行,首先看vip队列(微队列):
 1、Promise.resolve().then(() => { console.log(3) }),执行输出3;
 微队列没了,开始从消息队列获取任务执行,消息队列当前都是从计时线程回来排队的回调,因为延迟都是0,所以从上往下看。
 开始普通消息队列。
 1、console.log(1)执行,输出1。看到promise,放到微队列;
 2、此时会没有其他排队中的微队列任务,会先执行微队列,再继续执行普通消息队列;
 3、Promise.resolve().then(() => { console.log(7) });输出7;
 4、继续往下,没有微队列排队任务,执行普通消息队列中排名第一的任务;console.log(8);输出8,看到计时器,继续排队;
 5、再执行普通队列中的第一个任务,看到,看到promise,微队列排队;
 再回头看,还剩一个微队列任务和一个普通队列任务;
 1、执行微队列第一个任务,输出4;
 2、执行普通队列第一个任务,输出5;
 综上全部执行完,输出是:2、6、3、1、7、8、4、5;
 这里是文字描述,如果能像上边那样将消息队列的图画出来能更直观看到这类面试题怎么解答。接下来看看怎么回答面试提问(最好不是死记硬背,能理解):
 提问:如何理解 JS 的异步?
 回答:
 JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
 而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。
 如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
 所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
提问:阐述一下 JS 的事件循环
 回答:
 事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
 在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
 过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
 根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。单线程是异步产生的原因,事件循环是异步的实现方式。
[阿尔卑斯丶于2023-07-27 09:35编辑了帖子]
游客


返回顶部

公众号

公众号