浏览器进程模型
进程
为了使程序运行,每个应用至少有一个进程,进程之间相互独立。当一个进程崩溃时,其他进程不会受到影响。每个进程都有其自己的内存空间和系统资源,以确保它们彼此独立。
线程
运行代码的执行实体叫做线程。一个进程至少有一个线程,即主线程,它负责运行程序的主要代码。如果需要执行多个任务,主线程可以创建额外的线程来执行代码。因此,一个进程可以有多个线程同时运行。
浏览器进程和线程
浏览器是一个多进程多线程的应用程序,以避免崩溃影响整个浏览器。当浏览器启动时,会启动多个进程来处理不同的任务。
浏览器主要包含以下几种进程:
浏览器进程
浏览器进程是整个浏览器的基石,负责管理浏览器窗口、标签页、插件等所有内容。它还会启动其他子进程,如网络进程和渲染进程等。渲染器进程主要负责页面渲染和JavaScript代码的执行。每个标签页都会对应一个独立的渲染进程。
网络进程
网络进程负责加载网络资源,如HTML、CSS、JavaScript等文件。当浏览器请求这些资源时,它会通过浏览器进程启动一个网络进程来处理这个请求,并加载和解析这些资源。每个网络进程都是独立的,因此它们可以同时处理多个请求。
渲染进程
渲染进程是浏览器中负责页面渲染的进程。当浏览器收到HTML、CSS和JavaScript等文件后,它会将这些文件传递给渲染进程进行解析和渲染。每个标签页都有自己的渲染进程,以确保它们之间的互不干扰。渲染进程还可以执行JavaScript代码并与其他渲染进程通信。
事件循环
事件循环,又称为消息循环或event loop,是一种在程序中处理异步任务的主要方式。异步任务,即那些不直接影响程序主线程运行的任务,如用户点击、定时器超时或者网络请求等。
事件循环的工作流程可以概括为以下四个步骤:
- 主线程首先进入一个无限循环的状态,不断地轮询任务队列是否有待处理的任务。
- 在每一次循环中,主线程会检查消息队列(或其他类型的任务队列)是否有待处理的任务。如果有任务,它就会将这个任务加入到主线程的任务队列中,以便后续执行。
- 主线程会按照先入先出(FIFO)的顺序,依次从任务队列中取出任务进行执行。在执行任务的过程中,主线程可能会进入阻塞状态,如等待用户输入或等待网络响应等。如果没有任务需要执行,主线程通常会进入休眠状态,以避免浪费CPU资源。
- 所有其他的线程(如子线程或后台线程)可以在任何时候将任务添加到当前的消息队列末尾,等待主线程执行。这个过程可以发生在主线程的任何阶段,包括在主线程执行任务的过程中。
这个重复从消息队列中取出任务执行的过程就称为事件循环。事件循环机制使得主线程可以高效地处理大量的异步任务,而不需要等待每一个任务依次完成。这大大提高了程序的响应性和并发性,是现代编程中不可或缺的一部分。
如何理解异步
JavaScript是一门单线程的编程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
渲染主线程承担着许多工作,包括渲染页面和执行JavaScript等。如果使用同步的方式,可能会导致主线程产生阻塞,从而使得消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白地消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
因此,浏览器采用了异步的方式来避免这种情况。具体做法是当某些任务发生时,比如计时器、网络、事件监听等,主线程会将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永远不会阻塞,从而最大限度地保证了单线程的流畅运行。
同步:
异步:
任务与消息队列
任务没有优先级,先进先出
消息队列具有优先级
w3c对消息队列的解释:
-
每个任务都有一个任务类型,同一类型的任务必须在一个队列,不同类型任务可以分属于不同的队列
-
事件循环中,浏览器可以根据实际情况从不同的消息队列中取出任务执行
-
浏览器必须有一个微队列,优先于其他队列的执行
chrome 的实现中,⾄少包含了下⾯的队列:
- 延时队列 计时器结束的回调任务,优先级中
- 交互队列 用户交互的操作任务,优先级高
- 微队列 最快执行的任务,优先级最高(通过Promise、MutationObserver添加到微队列)
宏任务和微任务
执行顺序:
- 顺序执行代码,执行同步任务,直到遇到第一个宏任务或微任务
- 如果遇到宏任务添加到宏任务队列中,如果遇到微任务添加到微任务队列中,继续执行同步任务
- 当最后一个同步任务执行完毕,执行宏任务队列和微任务队列中的任务,重复执行宏任务和微任务的过程,直到运行结束
宏任务和微任务在同一任务中,先执行微任务;宏任务和微任务不在同一任务中,先执行宏任务
微任务: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 事件处理器是一个宏任务,它会在所有微任务之后执行,且只有在请求成功加载后才会触发。
思考
- 阐述⼀下 JS 的事件循环
事件循环⼜叫做消息循环,是浏览器渲染主线程的⼯作⽅式。 在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息 队列中取出第⼀个任务执⾏,⽽其他线程只需要在合适的时候将任务加⼊到 队列末尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法⽬前已⽆法满⾜复杂的 浏览器环境,取而代之的是⼀种更加灵活多变的处理方式。 根据 W3C 官⽅的解释,每个任务有不同的类型,同类型的任务必须在同⼀ 个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级, 在⼀次事件循环中,由浏览器⾃⾏决定取哪⼀个队列的任务。但浏览器必须 有⼀个微队列,微队列的任务⼀定具有最⾼的优先级,必须优先调度执⾏
- JS 中的计时器能做到精确计时吗?为什么
不行,因为:
- 计算机硬件没有原⼦钟,⽆法做到精确计时
- 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调⽤的 是操作系统的函数,也就携带了这些偏差
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层, 则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时⼜带来了偏差
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运⾏,因此 ⼜带来了偏差