谈谈你是如何理解 JS 异步编程的,EventLoop、消息队列都是做什么的,什么是宏任务,什么是微任务?

436 阅读5分钟

1.理解JS异步编程:

因为js语言初期定位是浏览器脚本语言,为了给网页做交互的,网页交互呢,我们知道都是DOM操作,如果有两个线程,一个线程对这个DOM进行更新,另外一个线程对DOM进行删除,这时候浏览器就不知道以哪个线程处理结果。所以js选择单线程工作模型。但是单线程工作模型,如果所有代码执行都是同步的话。当有一段代码非常耗时,后面的代码无法执行,渲染进程也会停止,这样浏览器就会进入一个假死状态,异步编程就应运而生了。

2. EventLoop

说一个简单场景,如果我们给定一个线程四个任务,这些任务会按照顺序写进主线程中,并且会依次执行,执行完成线程自动退出。但是不是所有的任务都是在执行之前都是统一安排好的,大部分情况下(用户交互事件,请求资源,定时任务等),新的任务是在线程运行过程中产生的。比如在线程执行过程中,又接收到一个新的任务要求计算”10+2“,那上面那种方式就无法处理情况了。

所以,要想在线程过程中,能接收并执行新的任务,就需要采用事件循环机制(可以使用一个for循环语句来监听是否有新的任务)

为了处理线程运行过程中产生的新任务,需要做两点改进。

第一点引入循环机制,在线程语句最后添加一个for循环语句,让线程一直执行 第二点引入事件,在线程执行过程中,等待用户输入的数字,等待过程中出于暂停状态,一旦接受到用户输入的信息,线程激活,运算输出结果

所以在一个线程运行过程中,要想处理这个线程新来的任务解决方案就是事件循环的方案才能解决,事件循环就是这样出来的

3. 消息队列

思考: 上面事件循环解决了线程内部产生新任务怎么处理的问题,那如果主线程处理其他线程发送过来的任务呢?怎么处理。只用事件循环是做不了的。

比如: 在渲染进程中有渲染主线程和IO线程,IO线程的资源加载,鼠标点击,其他事件都需要交给渲染主线程处理。渲染进程接收到这些任务之后需要着手处理,处理完之后主线程才能做DOM解析。

那怎么设计好一个线程模型,能让其能够接收其他线程发送的消息呢?

一个通用的模式是使用消息队列:消息队列是一种数据结构,可以存放要执行的任务。有先进先出的特点从尾部添加从头部取出在尾部。

有了消息队列,IO线程里面事件产生的任务(消息)就会先添加到队列尾部,渲染主线程会循环地从消息队列头部中读取任务,执行任务。

渲染进程 专门有一个IO线程用来接收其他进程传进来的消息,接收消息之后会将这些消息组装成任务发送给渲染主线程,只需要将任务添加到该消息队列中就可以了。

微任务和宏任务

页面线程所有执行的任务都来自于消息队列,消息队列是”先进先出“的属性,也就是说放入队列中的任务,需要等待前面的任务呗执行完,才会被执行。介于先进先出属性,就有两个问题要解决 第一个问题: 比如一个典型的场景是监控DOM节点的变化情况(节点的插入,修改,删除等动态变化)后面还有(Promise),然后根据这些变化来处理相应的业务逻辑。一个通用的设计一套监听接口,当变化发生时,渲染引擎同步调用的这些接口,这是典型的观察者模式。这个模式是有问题的,因为DOM变化非常频繁,如果每次发生变化的时候,都直接用相应的javascript接口,那么这个当前的任务执行时间会被拉长,从而执行效率的下降

那如果将这些DOM变化做异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为添加到消息队列的过程中可能前面就有很多任务在排队了。

所以说,如果DOM发生变化,采用同步通知方式,会影响当前任务的执行效率。如果采用异步方式,又会影响到监控的实时性。

那如何权衡效率和实时性呢?针对这种情况微任务应运而生了。

通常我们将消息队列里面的任务称为宏任务,每个宏任务中都包含一个微任务队列,在执行宏任务队列的过程中,如果有DOM有变化,那么会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此这样解决了执行效率的问题

等宏任务中主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务这时候去执行宏任务中的微任务,因为DOM变化的事件都保存在这些微任务队列中,这样就解决了实时性问题。

同步通知对于一些变化非常频繁的DOM变化 执行效率较低,如果使用异步执行的话实时性不足所以使用微任务介于同步和异步之间,权衡出执行效率和实时性最优解。比异步方案更具实效性,比同步方案更具执行效率。所以就有在宏任务里面加了一个微任务,当前同步代码和宏任务执行完执行微任务