ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

事件循环Event Loop #12

Open ArthurWangCN opened 2 years ago

ArthurWangCN commented 2 years ago

所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。

ArthurWangCN commented 2 years ago

JS异步是怎么实现的

我们都知道JS是单线程的,所谓的"JS是单线程的"只是指JS的主运行线程只有一个,而不是整个运行环境都是单线程。JS的运行环境主要是浏览器,以大家都很熟悉的Chrome的内核为例,他不仅是多线程的,而且是多进程的:

渲染进程

GUI线程 GUI线程就是渲染页面的,他解析HTML和CSS,然后将他们构建成DOM树和渲染树就是这个线程负责的。

JS引擎线程 这个线程就是负责执行JS的主线程,前面说的”JS是单线程的“就是指的这个线程。大名鼎鼎的Chrome V8引擎就是在这个线程运行的。需要注意的是,这个线程跟GUI线程是互斥的。互斥的原因是JS也可以操作DOM,如果JS线程和GUI线程同时操作DOM,结果就混乱了,不知道到底渲染哪个结果。这带来的后果就是如果JS长时间运行,GUI线程就不能执行,整个页面就感觉卡死了。

定时器线程 setTimeout、setInterval

事件触发线程 定时器线程其实只是一个计时的作用,他并不会真正执行时间到了的回调,真正执行这个回调的还是JS主线程。所以当时间到了定时器线程会将这个回调事件给到事件触发线程,然后事件触发线程将它加到事件队列里面去。最终JS主线程从事件队列取出这个回调执行。事件触发线程不仅会将定时器事件放入任务队列,其他满足条件的事件也是他负责放进任务队列。

异步HTTP请求线程 这个线程负责处理异步的ajax请求,当请求完成后,他也会通知事件触发线程,然后事件触发线程将这个事件放入事件队列给主线程执行。


所以JS异步的实现靠的就是浏览器的多线程,当他遇到异步API时,就将这个任务交给对应的线程,当这个异步API满足回调条件时,对应的线程又通过事件触发线程将这个事件放入任务队列,然后主线程从任务队列取出事件继续执行。这个流程我们多次提到了任务队列,这其实就是Event Loop

ArthurWangCN commented 2 years ago

Event Loop

所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。

浏览器的Event Loop

事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是事件队列。各个异步线程执行完后,通过事件触发线程将回调事件放到事件队列,主线程每次干完手上的活儿就来看看这个队列有没有新活儿,有的话就取出来执行。

流程如下:

  1. 主线程每次执行时,先看看要执行的是同步任务,还是异步的API
  2. 同步任务就继续执行,一直执行完
  3. 遇到异步API就将它交给对应的异步线程,自己继续执行同步任务
  4. 异步线程执行异步API,执行完后,将异步回调事件放入事件队列上
  5. 主线程手上的同步任务干完后就来事件队列看看有没有任务
  6. 主线程发现事件队列有任务,就取出里面的任务执行
  7. 主线程不断循环上述流程
ArthurWangCN commented 2 years ago

定时器不准

一个定时器的执行:

  1. 主线程执行同步代码
  2. 遇到setTimeout,将它交给定时器线程
  3. 定时器线程开始计时,2秒到了通知事件触发线程
  4. 事件触发线程将定时器回调放入事件队列,异步流程到此结束
  5. 主线程如果有空,将定时器回调拿出来执行,如果没空这个回调就一直放在队列里。

上述流程我们可以看出,如果主线程长时间被阻塞,定时器回调就没机会执行,即使执行了,那时间也不准了。

所以写代码时一定不要长时间占用主线程。

ArthurWangCN commented 2 years ago

引入微任务

事件队列里面的事件还可以分两类:宏任务和微任务。微任务拥有更高的优先级,当事件循环遍历队列时,先检查微任务队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。

上图需要注意以下几点:

  1. 一个Event Loop可以有一个或多个事件队列,但是只有一个微任务队列。
  2. 微任务队列全部执行完会重新渲染一次
  3. 每个宏任务执行完都会重新渲染一次
  4. requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列

常见宏任务有: script (可以理解为外层同步代码)、setTimeout/setInterval、setImmediate(Node.js)、I/O、UI事件、postMessage

常见微任务有: Promise、process.nextTick(Node.js)、Object.observe、MutaionObserver

ArthurWangCN commented 2 years ago

Node.js的Event Loop

...

ArthurWangCN commented 2 years ago

总结

  1. JS所谓的“单线程”只是指主线程只有一个,并不是整个运行环境都是单线程
  2. JS的异步靠底层的多线程实现
  3. 不同的异步API对应不同的实现线程
  4. 异步线程与主线程通讯靠的是Event Loop
  5. 异步线程完成任务后将其放入任务队列
  6. 主线程不断轮询任务队列,拿出任务执行
  7. 任务队列有宏任务队列和微任务队列的区别
  8. 微任务队列的优先级更高,所有微任务处理完后才会处理宏任务
  9. Promise是微任务
  10. Node.js的Event Loop跟浏览器的Event Loop不一样,他是分阶段的
  11. setImmediate和setTimeout(fn, 0)哪个回调先执行,需要看他们本身在哪个阶段注册的,如果在定时器回调或者I/O回调里面,setImmediate肯定先执行。如果在最外层或者setImmediate回调里面,哪个先执行取决于当时机器状况。
  12. process.nextTick不在Event Loop的任何阶段,他是一个特殊API,他会立即执行,然后才会继续执行Event Loop

链接:https://juejin.cn/post/6844904100195205133

ArthurWangCN commented 2 years ago

为什么要存在微任务和宏任务两个队列

JS 作为浏览器脚本语言,为了避免复杂的同步问题(例如用户操作事件以及操作DOM),这就决定了被设计成单线程语言,而且也将会一直保持是单线程的。而在单线程中若是遇到了耗时的操作(IO,定时器,网络请求)将会一直等待,CPU利用率将会大打折扣,时间大量浪费。所以需要设计一种方案让一些耗时的操作放在一边等待,让后面的函数先执行,于是有了EventLoop的设计。

页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么若只有一种类型的任务就不合适了,所以引入了微任务队列。

微任务和宏任务的区分: