浏览器事件循环

一、浏览器的进程模型

浏览器是一个多进程多线程的应用程序,浏览器内部工件极其复杂,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程,其中,有以下主要进程:

1.浏览器进程

浏览器主要负责界面显示、用户交互、子进程管理等,浏览器进程内部会启动多个线程处理不同的任务。

2.网络进程

网络进程主要加载网络资源,也会启动多线程。

3.渲染进程

渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS代码,在默认情况下,浏览器会为每个标签开启一个新的渲染进程,以保证不同的标签页之间不相互影响。

二、渲染主线程的工作

渲染主线程是浏览器中最繁忙的线程,它要处理的任务包括但不限于:

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 进行布局
  • 处理图层
  • 连续渲染画面(FPS帧率,如每秒把页面画60次)
  • 执行全局JS代码
  • 执行事件处理函数
  • 执行计时器的回调函数等

渲染主线程的任务调度方式是排队,浏览器的任务会进入消息队列(事件队列)

三、事件队列(消息队列)

  1. 在最开始的时候,渲染主线程会进入一个无线循环
  2. 每一次循环会检查事件队列中是否有任务存在,如果有,就取出队头的任务执行(第一个任务),执行完一个后进入下一次循环,如果没有,则进入休眠状态。
  3. 其他所有的线程(包括其他进程的线程)可以随时向事件队列添加任务,新任务会加到事件队列的队尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒来继续循环拿取任务。

四、浏览器的异步机制

在代码执行过程中,会遇到一些无法立即处理的任务。例如::

  1. 计时完成后需要执行的任务——setTimeout、setInterval
  2. 网络通信完成后需要执行的任务——XHR、Fetch
  3. 用户操作后需要执行的任务——addEventListener

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于阻塞的状态,从而导致浏览器卡死。这是浏览器就会使用异步来解决这个问题。

具体操作

异步的具体做法是当某些任务发生时,比如计时器、网络、事件监听,这是主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到事件队列的队尾排队,等待主线程调度执行。

五、事件队列的优先级

任务没有优先级,但事件队列有优先级

在过去的浏览器中,有一个宏队列和一个微队列(VIP),而现在的浏览器中,每个任务都有一个任务类型,同一个类型的任务必须在一个队列中,不同类型的任务可以分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行,w3c规定,浏览器必须准备好一个微队列(micro task queue),微队列中的任务优先所有其他任务执行。

chrome中的队列优先级

在目前chrome中有延时队列(中)、交互队列(高)、微队列(最高)等,添加到微队列的主要方式是

Promise.MutationObserver

例如:

Promise.resolve().then(函数);     //立即把一个函数加入微队列

这里对上述两个方法进行一定的解释

Promise.MutationObserver

Promise.MutationObserver是一个基于Promise对象的封装实现,用于将MutationObserver和Promise结合起来处理DOM树的变化。

MutationObserver是浏览器提供的一个API,用于监视DOM树的变化。它可以观察DOM节点的增删改操作,并在变化发生时触发回调函数。然而,原生的MutationObserver API并不支持使用Promise来处理异步操作。

为了解决这个问题,Promise.MutationObserver通过封装MutationObserver对象并返回一个Promise对象来处理异步操作。它可以将MutationObserver的回调函数包装成Promise的resolve或reject函数,从而能够使用Promise的链式调用和错误处理机制。

使用Promise.MutationObserver可以实现以下功能:

  1. 监视DOM树的变化,并在变化发生后执行一系列的异步操作。
  2. 可以通过链式调用的方式依次执行一系列的异步操作,并在最后返回一个Promise对象。
  3. 可以通过catch方法捕获错误,并进行相应的错误处理。

以一个例子来说明Promise.MutationObserver的使用:

const observer = new Promise.MutationObserver(callback);

observer.observe(document.body, {
  subtree: true,
  childList: true,
});

observer.then(() => {
  console.log('DOM树发生了变化');
})
.catch((error) => {
  console.error(error);
});

function callback(mutations, observer) {
  // 异步操作
  setTimeout(() => {
    console.log('异步操作完成');
    observer.takeRecords(); // 清空记录
    observer.disconnect(); // 停止观察
    observer.resolve(); // 触发Promise的resolve函数
  }, 1000);
}

在上面的例子中,我们创建了一个Promise.MutationObserver对象,并传入一个回调函数。然后通过调用observe方法开始观察DOM树的变化。

当DOM树发生变化时,回调函数会执行一系列的异步操作,最后调用resolve函数来触发Promise对象的resolve。我们可以通过then方法来监听resolve的触发,并执行相应的操作。

如果在异步操作过程中发生了错误,可以通过catch方法来捕获错误并进行处理。

总之,Promise.MutationObserver是一个方便而强大的工具,可以简化DOM树变化的处理,并且提供了Promise的链式调用和错误处理机制。

Promise.resolve().then()

Promise.resolve().then()是Promise对象的一个方法链,用于处理异步操作的结果。

Promise.resolve()是一个静态方法,它返回一个已经被解决(resolved)的Promise对象。它可以接受一个值作为参数,并将这个值封装成一个已经解决的Promise对象返回。如果参数本身就是一个Promise对象,则直接返回该Promise对象。

.then()方法是Promise对象的方法,用于添加回调函数来处理Promise对象的解决结果。它接受两个参数:一个是解决的回调函数,一个是拒绝的回调函数。在Promise对象解决后,会调用对应的回调函数来处理解决的值。

结合起来,Promise.resolve().then()的作用可以总结为:

  1. 创建一个已经解决的Promise对象;
  2. 添加回调函数来处理解决的结果。

通过这个方法链,我们可以进行一系列的异步操作,并在每个异步操作完成后,使用.then()方法链式地处理结果。这样的代码结构更加简洁,易于理解和维护。

下面是一个简单的例子来说明Promise.resolve().then()的使用:

Promise.resolve(42)
  .then((value) => {
    console.log(value); // 42
    return value + 1;
  })
  .then((value) => {
    console.log(value); // 43
    return Promise.reject('出错了');
  })
  .catch((error) => {
    console.error(error); // 出错了
    return Promise.resolve('处理错误');
  })
  .then((value) => {
    console.log(value); // 处理错误
  });

在上面的例子中,我们首先使用Promise.resolve()创建了一个已经解决的Promise对象,并传入值42。

然后通过.then()方法添加了第一个回调函数,打印出了解决的值42,并返回了value + 1。

接下来,在第二个.then()方法里,我们返回了一个被拒绝的Promise对象,并在.catch()方法里捕获了错误,并打印出了错误信息"出错了"。

接着,我们又通过.then()方法添加了一个回调函数,并返回了一个已经解决的Promise对象,并且打印出了返回的值"处理错误"。

通过这个例子,我们可以看到,使用Promise.resolve().then()方法可以方便地处理多个异步操作的结果,并且可以在每个.then()方法里进行相应的处理和错误处理。

总之,Promise.resolve().then()是Promise对象的方法链,用于处理异步操作的结果,它简化了异步操作的处理和错误处理。

六、JS的相关案例及知识

1.JS阻塞渲染




    
    
    js阻碍渲染案例


    
    

启动函数前

在上述代码中,页面会卡住3秒后再进行重新渲染,死循环函数会占用渲染主线程3秒,这段时间内,如h1的更改操作,滚动页面等都会进入等待,在事件队列中排队。

2.JS中的计时器不能精准计时的原因

  • 计算机硬件误差(计算机内没有原子钟)
  • 该方法调用的是操作系统的计时函数,但操作系统的计时函数本身误差
  • w3c中规定,如果嵌套层超过5层,则会带有4毫秒的最少时间
  • 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行

七、总结

单线程是异步产生的原因,事件循环是异步的实现方式

你可能感兴趣的:(前端,前端,chrome,javascript)