简单理解:程序的运行是需要内存空间的。这块内存空间就可以理解为进程。
另外每个应用程序至少会有一个进程。进程之间相互独立,通信需要双方同意。
有了进程(内存)后,就能运行程序了。而具体运行程序代码的是进程的小弟——线程。
现在浏览器是一个多进程多线程的程序。为了避免相互影响和减少连环崩溃的概率,当启动浏览器程序后,它会自动启动多个进程。
其中主要的进程有:
主要负责页面显示,用户交互(比如点击滑动),子进程管理。它会启动多个线程来处理不同的任务。
负责加载网络资源,它也会启动多个线程来处理各种网络任务。
它默认创建的主线程,被称为渲染主线程。负责执行 HTML CSS JS 代码。
默认情况下,浏览器会为每个标签页创建1个新的渲染进程,以保证不同标签页之间互不影响。
渲染主线程是浏览器中最忙的线程,因为需要处理的东西有很多,包括但不限于:
问题来了,如何调度这么多的任务?举例来说
渲染主线程通过排队的方式来处理这个问题。
1,渲染主线程会进入无限循环。
2,每次循环会检查消息队列中是否存在任务 --> 有则取出第1个任务执行–> 执行完进入下次循环。没有任务则进入休眠状态。
3,其他所有线程(包括其他进程的子线程),可以随时向消息队列的末尾添加任务。
添加任务时,如果渲染主线程是休眠状态,则会唤醒让它继续循环取出任务执行。
注意,消息队列不止一个(常见有微队列,延时队列,交互队列)。
以上整个过程被称为——事件循环。
在执行代码时,会遇到一些无法立即处理的任务。
如果渲染主线程一直等待这些任务的时间点到来,那就会一直处于阻塞的状态,表现为浏览器卡死。
浏览器选择用异步来解决这个问题。这样渲染主线程永不阻塞。
因为 js 运行在渲染主线程中,所以是单线程的语言。而渲染主线程有许多的工作,包括渲染页面和执行全局 js。
举例:
<body>
<h1>下雪天的夏风h1>
<button>更改内容button>
<script>
const h1 = document.querySelector("h1");
const btn = document.querySelector("button");
btn.addEventListener("click", function () {
h1.textContent = "求关注";
delay(2000);
});
// 死循环的时间 s
function delay(duration) {
const start = Date.now();
while (Date.now() - start < duration) {}
}
script>
body>
1,当全局 js 执行到绑定事件时,会通知交互线程来处理。当某个时间用户点击后,会将 fn 包装为任务发送给消息队列。
2,当 fn 执行时,第1条语句会创建1个绘制任务,接着死循环 2s 后,此时 fn 才执行完成。渲染主线程再去消息队列中看有没有新的任务。
这就是同步 js 会阻塞渲染的原因。
任务没有优先级,在消息队列中先进先出。但消息队列有优先级!
1,每个任务都有一个任务类型,同一类型的任务必须在一个队列;不同类型的任务可以分属不同的队列。
一次事件循环中,浏览器可以根据实际情况选择从不同的队列中取出任务执行。
2,队列也有 VIP。浏览器必须准备好一个微队列,微队列中的任务优先其他所有任务执行。
现代浏览器的复杂度升高了许多,W3C 不再使用宏队列的说法。
不过我们可以简单的将,除微队列之外的其他队列 “统一看做宏队列”。
3,在目前的 Chrome 浏览器中,至少包含了以下队列(其他的和前端开发关系不大,省略)
交互队列和延时队列,优先级举例如下:
可以理解为:用户的交互行为需要立即响应,而计时器已经等了一段时间了再等一小会也无所谓。
<body>
<button id="btn1">初始化</button>
<button id="btn2">执行交互队列</button>
<script>
/*
1,addDelay()执行完成时(2s后),计时器的回调函数 A,会被添加到延时队列。
2,接着 addInteraction() 执行完成时(2s后),会将按钮点击事件的回调函数 B,会被添加到交互队列。
3,此时,点击【执行交互队列】,交互队列中的 B 会先于延时队列中的 A 执行。
*/
const init = document.querySelector("#btn1");
const btnInteraction = document.querySelector("#btn2");
init.addEventListener("click", function () {
addDelay();
addInteraction();
// “添加交互队列” 被打印后,需要立即点击【执行交互队列】
console.log("-----");
});
function addDelay() {
console.log("添加延时队列");
setTimeout(() => {
console.log("执行延时队列");
}, 0);
// 等 2s 为了保证 addDelay 执行完后,计时器的回调函数添加到延时队列
delay(2000);
}
function addInteraction() {
console.log("添加交互队列");
btnInteraction.addEventListener("click", function () {
console.log("执行交互队列");
});
// 等 2s 为了保证 addInteraction 执行完后,按钮点击事件的回调函数添加到交互队列
delay(2000);
}
function delay(duration) {
const start = Date.now();
while (Date.now() - start < duration) {}
}
</script>
</body>
效果:
另外,添加任务到微队列,主要使用 Promise
和 MutationObserver
// 立即把 fn 添加到微队列。
Promise.resolve().then(fn)
几个测试题:
1,结果: 2,1
setTimeout(() => {
console.log(1);
}, 0);
console.log(2);
2,结果:5,4,3,1,2
function a() {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
});
}
setTimeout(() => {
console.log(3);
Promise.resolve().then(a);
}, 0);
Promise.resolve().then(function () {
console.log(4);
});
console.log(5);
3,结果:5,1,2,3
function a() {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
});
}
setTimeout(() => {
console.log(3);
}, 0);
Promise.resolve().then(a);
console.log(5);
搞清楚了上面的内容后,下面的问题也就比较简单了。
单线程是异步产生的原因。
事件循环是异步的实现方式。
因为 js 运行在渲染主线程中,所以是单线程的语言。而渲染主线程有许多的工作,包括渲染页面和执行全局 js。
如果使用同步的方式,大概率会造成渲染主线程阻塞,进而导致消息队列中的很多其他任务无法及时执行。表现为页面无法及时更新,给用户造成浏览器卡死的现象。
所以浏览器采用异步的方式来避免这个问题。具体做法:
当有某些任务产生后,计时器,网络通信,事件监听等,渲染主线程会交给其他对应线程去处理,自身继续执行后面的代码。当其他线程处理完成后,会将实现传递的回调函数包装为任务,发送到消息队列末尾,等待渲染主线程调度执行。
在这种异步模式下,浏览器不会被阻塞,最大限度的保证了单线程的流畅运行。
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,会开启一个不会结束的 for 循环for(;;){}
,每次循环从消息队列中取出第1个任务执行,其他线程将产生的任务加入到消息队列末尾即可。
过去把消息队列简单的分为微队列和宏队列。现在已经无法满足现代浏览器的复杂环境了。
根据 W3C 的解释,每个任务都有类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。而不同队列的优先级也不一样。
一次事件循环中,由浏览器来决定调取那个队里中的任务。同时必须有一个优先级最高的微队列,必须优先调度执行。
不能。
1,受事件循环的影响,计时器的回调函数所在的延时队列优先级较低,这样即便倒计时结束,也得等某些队列的任务执行完成,所以会带来偏差。
2,W3C 的标准,浏览器实现的倒计时,如果嵌套超过5层,默认会有 4ms 的最少时间,这也是可能的偏差。
3,操作系统的计时函数本身就有少量误差,js 计时器调用的就是它,所以也会有偏差。
4,计算机硬件没有原子钟,无法做到精准计时。
原子钟(英语:Atomic clock)是一种时钟,它以原子共振频率标准来计算及保持时间的准确。原子钟是世界上已知最准确的时间测量和频率标准,也是国际时间和频率转换的基准,用来控制电视广播和全球定位系统卫星的讯号。
以上。
参考:渡一教育。