本文是继🌟 JavaScript事件循环:单线程的异步魔法秀 🎪 补充进程和线程 完整的Evevt Loop 大家可以先看看上篇文章,再来看这篇,会有更好的理解
第一章:浏览器公司——多进程架构揭秘
想象浏览器是一家高科技公司,每个标签页就是独立的分公司(渲染进程)。当你在Chrome任务管理器看到这样的场景:
这其实是浏览器王国的"组织架构图":
- 浏览器主进程:CEO办公室(管理所有子公司)
- GPU进程:设计部(处理3D渲染)
- Network Service:物流中心(处理网络请求)
- 标签页进程:独立分公司(每个标签页都是独立王国)
为什么这样设计?就像分公司独立运营:
- 一个分公司着火(崩溃),其他照常营业
- 每个分公司有独立预算(内存分配)
- 总公司统一协调资源(进程间通信)
这就是多进程架构的精髓——安全隔离与资源自治!
我们一个tap页面就是一个进程,每个页面都是相互独立的,当一个页面崩了,不至于影响其他页面
第二章:进程与线程——公司里的部门架构
进程:是操作系统中一个正在运行的程序,是分配资源的最小单元,每个进程都有自己的地址空间、内存、文件描述符等资源。 = 独立分公司
- 有自己的办公大楼(内存空间)
- 独立的银行账户(CPU时间片)
- 专属工牌(进程ID)
线程:是进程中的一个执行单元,是 cpu 调度的最小单元,共享进程的资源,但有自己的独立执行流。 = 部门员工
- 开发部(JS引擎线程)
- UI设计部(GUI渲染线程)
- 物流部(网络请求线程)
- 计时部(定时器线程)
进程:线程=1:n,一个进程可以有多个线程,多个线程紧密配合,完成工作,就像发快递一样,有分拣的工人,运输的工人,快递小哥上门送件,发快递这个任务就是进程,各种工人合作完成任务就是线程
当你在控制台输入:
console.log("启动分公司!");
整个工作流程就像:
- CEO(浏览器进程)批准成立新分公司
- 分配办公场地(创建渲染进程)
- 组建开发团队(JS引擎线程)
- 招聘UI设计师(GUI渲染线程)
每一个tap页都是一个子进程,在子进程有很多的线程
-
GUI渲染线程
负责渲染页面,布局和绘制 页面需要重绘和回流时,该线程就会执行 与js引擎线程互斥,防止渲染结果不可预期
-
JS引擎线程
负责处理解析和执行javascript脚本程序 只有一个JS引擎线程(单线程) 与GUI渲染线程互斥,防止渲染结果不可预期
-
事件触发线程
用来控制事件循环(鼠标点击、setTimeout、ajax等) 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
-
定时触发器线程
setInterval与setTimeout所在的线程 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的 计时完毕后,通知事件触发线程
-
异步http请求线程
浏览器有一个单独的线程用于处理AJAX请求 当请求完成时,若有回调函数,通知事件触发线程
知道了这些常见的线程,我们来聊聊为什么JavaScript是单线程的
当时创建JavaScript这门语言的时候,还不流行多线程架构,硬件支持并不好,其次多线程需要加锁,会增加代码的复杂性,而我们的JavaScript早期是设计来交互的简单脚本语言
第三章JS引擎线程和GUI渲染线程的爱恨情仇
-
互斥原则:两人不能同时工作
-
工作流程:
- JS小伙修改DOM(装修图纸)
- 通知GUI设计师:"图纸改好了!"
- JS小伙离开工位(执行栈清空)
- GUI设计师进场重绘页面
- 设计师离场后JS小伙才能继续工作
JS是可以创建元素的,并且修改DOM的属性,如果JS引擎线程和GUI渲染线程是可以一起运行的,那么渲染线程前后获得的元素就可能不一致了。十分消耗性能,我们刚刚渲染绘制好,你又修改了DOM,又得重绘重排
所以为了不必要的麻烦JS引擎线程和GUI渲染线程是互斥的,当一方运行,一方就得挂起,当JS引擎线程执行时GUI渲染线程会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行。
第四章:事件循环——开发部的任务管理系统
这就是事件循环的核心奥秘!开发部小伙的工位旁有三个神奇装置:
其实下面图是展示不全的,任务队列是有宏任务队列和微任务队列的
- 执行栈(办公桌)
- 同步任务直接放在桌上立即处理
- 只能堆放一个任务(单线程)
- 宏任务队列(普通收件箱)
- 普通客户的需求(
setTimeout、onclick) - 按先来后到排队
- 微任务队列(VIP加急信箱)
- VIP客户的紧急需求
- 插队优先处理
执行栈先把所有的同步任务执行执行完毕,遇到微任务,会通知事件触发线程把微任务加到微任务队列当然微任务也有自己的VIP用户比如queueMicrotas会先执行这个,遇到宏任务不是立马放到宏任务队列,比如setTimeout(()=>{},2),setTimeout(()=>{},0)虽然先遇到setTimeout2,但是其事件是2,而setTimeout0,我们会先通知事件触发线程,然后哪个时间先到,事件触发线程会先把哪个放入宏任务队列
异步任务处理的跨部门协作
当开发部遇到耗时任务时:
场景1:点外卖(网络请求)
fetch('/api/data').then(res => {
console.log('外卖到了!');
});
- 开发部小伙给物流部打电话(创建HTTP线程)
- 继续处理其他工作(不阻塞主线程)
- 外卖送达后,物流部把收据放进宏任务信箱
场景2:定时提醒(setTimeout)
setTimeout(() => {
console.log('3分钟到了!');
}, 3 * 60 * 1000);
- 开发部小伙通知计时部(定时器线程)
- 计时部盯着闹钟(独立计时)
- 时间到后把提醒放进宏任务信箱
场景3:DOM装修监听(MutationObserver)
const observer = new MutationObserver(() => {
console.log('DOM装修有变!');
});
observer.observe(container, {childList: true});
- 开发部修改DOM(同步执行)
- 装修变更通知放进微任务信箱(VIP通道)
- 优先于宏任务处理
第五章事件循环的完整工作流程
结合示意图看完整流程:
-
从执行栈开始处理同步任务
-
遇到异步任务交给专属线程:
- 定时器 → 计时部
- 网络请求 → 物流部
- DOM事件 → 事件管理部
-
执行栈清空后,立即处理微任务队列
-
检查是否需要渲染(调用GUI渲染部)
-
从宏任务队列取一个任务处理
-
重复步骤1-5(Event Loop)
第六章:微任务与宏任务的世纪对决
通过代码看区别:
// 宏任务 (普通客户)
setTimeout(() => console.log('宏任务1'));
// 微任务 (VIP客户)
Promise.resolve().then(() => console.log('微任务1'));
// 混合场景
setTimeout(() => {
console.log('宏任务2');
Promise.resolve().then(() => console.log('宏中的微'));
});
黄金法则:
- 每执行一个宏任务后
- 必须清空所有微任务
- 才能执行下一个宏任务
就像银行柜员:处理完一个普通客户后,必须服务完所有VIP才能叫下一位普通客户
结语:精密协作的艺术
浏览器架构的精妙之处在于:
- 多进程隔离 → 分公司独立运营保安全
- 多线程协作 → 部门专业分工提效率
- 单线程JS → 避免资源争抢简化开发
- 事件循环 → 任务优先级智能调度
下次当你的setTimeout"迟到"或Promise"插队"时,请想起这个比喻:不是代码不听话,而是浏览器公司各部门正在精密协作,确保整个系统高效稳定地运行!