事件循环

217 阅读8分钟

浏览器进程模型

进程

为了使程序运行,每个应用至少有一个进程,进程之间相互独立。当一个进程崩溃时,其他进程不会受到影响。每个进程都有其自己的内存空间和系统资源,以确保它们彼此独立。

image.png

线程

运行代码的执行实体叫做线程。一个进程至少有一个线程,即主线程,它负责运行程序的主要代码。如果需要执行多个任务,主线程可以创建额外的线程来执行代码。因此,一个进程可以有多个线程同时运行。

image.png

浏览器进程和线程

浏览器是一个多进程多线程的应用程序,以避免崩溃影响整个浏览器。当浏览器启动时,会启动多个进程来处理不同的任务。

image.png

浏览器主要包含以下几种进程:

浏览器进程

浏览器进程是整个浏览器的基石,负责管理浏览器窗口、标签页、插件等所有内容。它还会启动其他子进程,如网络进程和渲染进程等。渲染器进程主要负责页面渲染和JavaScript代码的执行。每个标签页都会对应一个独立的渲染进程。

网络进程

网络进程负责加载网络资源,如HTML、CSS、JavaScript等文件。当浏览器请求这些资源时,它会通过浏览器进程启动一个网络进程来处理这个请求,并加载和解析这些资源。每个网络进程都是独立的,因此它们可以同时处理多个请求。

渲染进程

渲染进程是浏览器中负责页面渲染的进程。当浏览器收到HTML、CSS和JavaScript等文件后,它会将这些文件传递给渲染进程进行解析和渲染。每个标签页都有自己的渲染进程,以确保它们之间的互不干扰。渲染进程还可以执行JavaScript代码并与其他渲染进程通信。

事件循环

image.png 事件循环,又称为消息循环或event loop,是一种在程序中处理异步任务的主要方式。异步任务,即那些不直接影响程序主线程运行的任务,如用户点击、定时器超时或者网络请求等。

事件循环的工作流程可以概括为以下四个步骤:

  1. 主线程首先进入一个无限循环的状态,不断地轮询任务队列是否有待处理的任务。
  2. 在每一次循环中,主线程会检查消息队列(或其他类型的任务队列)是否有待处理的任务。如果有任务,它就会将这个任务加入到主线程的任务队列中,以便后续执行。
  3. 主线程会按照先入先出(FIFO)的顺序,依次从任务队列中取出任务进行执行。在执行任务的过程中,主线程可能会进入阻塞状态,如等待用户输入或等待网络响应等。如果没有任务需要执行,主线程通常会进入休眠状态,以避免浪费CPU资源。
  4. 所有其他的线程(如子线程或后台线程)可以在任何时候将任务添加到当前的消息队列末尾,等待主线程执行。这个过程可以发生在主线程的任何阶段,包括在主线程执行任务的过程中。

这个重复从消息队列中取出任务执行的过程就称为事件循环。事件循环机制使得主线程可以高效地处理大量的异步任务,而不需要等待每一个任务依次完成。这大大提高了程序的响应性和并发性,是现代编程中不可或缺的一部分。

如何理解异步

JavaScript是一门单线程的编程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

渲染主线程承担着许多工作,包括渲染页面和执行JavaScript等。如果使用同步的方式,可能会导致主线程产生阻塞,从而使得消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白地消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

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

在这种异步模式下,浏览器永远不会阻塞,从而最大限度地保证了单线程的流畅运行。

同步: image.png

异步: image.png

任务与消息队列

任务没有优先级,先进先出
消息队列具有优先级

w3c对消息队列的解释:

  1. 每个任务都有一个任务类型,同一类型的任务必须在一个队列,不同类型任务可以分属于不同的队列

  2. 事件循环中,浏览器可以根据实际情况从不同的消息队列中取出任务执行

  3. 浏览器必须有一个微队列,优先于其他队列的执行

    chrome 的实现中,⾄少包含了下⾯的队列:

  • 延时队列 计时器结束的回调任务,优先级中
  • 交互队列 用户交互的操作任务,优先级高
  • 微队列 最快执行的任务,优先级最高(通过Promise、MutationObserver添加到微队列)

宏任务和微任务

执行顺序:

  1. 顺序执行代码,执行同步任务,直到遇到第一个宏任务或微任务
  2. 如果遇到宏任务添加到宏任务队列中,如果遇到微任务添加到微任务队列中,继续执行同步任务
  3. 当最后一个同步任务执行完毕,执行宏任务队列和微任务队列中的任务,重复执行宏任务和微任务的过程,直到运行结束

宏任务和微任务在同一任务中,先执行微任务;宏任务和微任务不在同一任务中,先执行宏任务

微任务:Promise 回调函数、process.nextTick、Object.observe(已废弃)、MutationObserver。

宏任务:setTimeout、setInterval、setImmediate(Node.js 独有)、requestAnimationFrame、I/O 操作、UI 渲染。

练习1

console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('end');

/*
start
end
promise
setTimeout
*/
  • 执行同步任务start,继续执行
  • setTimeout是宏任务,和延迟秒数无关,放入宏任务队列,继续执行
  • promise是微任务,放入微任务队列,继续执行
  • 执行同步任务end,继续执行
  • 微任务要早与宏任务的执行,所以先promise,后setTimeout

练习2

console.log('start');
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('process.nextTick'));
console.log('end');

/*
start
end
process.nextTick 
setImmediate
*/
  • setImmediate:它的回调函数会在当前事件循环迭代的末尾被调用,即在当前I/O事件完成后。
  • process.nextTick:它的回调函数会在当前操作完成后、但在任何I/O事件(包括定时器)之前被调用。

练习3

console.log('start');  
requestAnimationFrame(() => console.log('requestAnimationFrame'));  
Promise.resolve().then(() => console.log('Promise'));  
console.log('end');

/*
start
end
Promise
requestAnimationFrame
*/

requestAnimationFrame 的回调是一个宏任务,它会在浏览器下一次重绘前执行,这通常发生在当前执行栈清空、微任务队列清空之后。

requestAnimationFrame 的确切执行时间取决于浏览器的刷新率以及当前事件循环的状态。如果浏览器在回调被放入队列后没有立即重绘(例如,如果它正在忙于其他任务),那么 requestAnimationFrame 的回调将等待直到下一次重绘。

练习4

console.log('start');
const xhr = new XMLHttpRequest();
xhr.open('GET', 'XXX');
xhr.onload = () => console.log('XMLHttpRequest');
xhr.send();
Promise.resolve().then(() => console.log('Promise'));
console.log('end');

/*
start
end
Promise
XMLHttpRequest
*/

XMLHttpRequest 的 onload 事件处理器是一个宏任务,它会在所有微任务之后执行,且只有在请求成功加载后才会触发。

思考

  1. 阐述⼀下 JS 的事件循环

事件循环⼜叫做消息循环,是浏览器渲染主线程的⼯作⽅式。 在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息 队列中取出第⼀个任务执⾏,⽽其他线程只需要在合适的时候将任务加⼊到 队列末尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法⽬前已⽆法满⾜复杂的 浏览器环境,取而代之的是⼀种更加灵活多变的处理方式。 根据 W3C 官⽅的解释,每个任务有不同的类型,同类型的任务必须在同⼀ 个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级, 在⼀次事件循环中,由浏览器⾃⾏决定取哪⼀个队列的任务。但浏览器必须 有⼀个微队列,微队列的任务⼀定具有最⾼的优先级,必须优先调度执⾏

  1. JS 中的计时器能做到精确计时吗?为什么

不行,因为:

  1. 计算机硬件没有原⼦钟,⽆法做到精确计时
  2. 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调⽤的 是操作系统的函数,也就携带了这些偏差
  3. 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层, 则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时⼜带来了偏差
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲时运⾏,因此 ⼜带来了偏差