努力让学习成为一种习惯,自信来源于充分的准备。
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享。
前言
这里是浏览器工作原理系列之事件循环机制的第一篇,这个系列会从事件循环起步,直到渲染原理,优化技巧等。打通浏览器相关知识体系,感兴趣的小伙伴可以持续关注
作为前端,我们日常工作中接触频率最高的便是浏览器,我们编写的页面代码均需要交给浏览器处理执行。那么理解浏览器的工作调度机制事件循环至关重要。理解了事件循环,对于我们不管是开发、问题排查还是性能优化都能够起到帮助。那么接下来就让我们开启一段浏览器的探索之旅吧!
以下所有浏览器相关的知识点都基于
Chrome
进程、线程
什么是进程?什么线程?进程是资源分配的最小单位,线程是CPU调度的最小单位。可能大家都听过这个解释。但是感觉不太通俗易懂。我们换种方式一步步探究下(这里我们不会过于深入,只需要做到能够简单理解即可)
进程
// test.js
function test() {
console.log('这是一段程序')
}
test()
上面是我们日常写的一段程序。命名为test.js保存在硬盘中。执行的时候,操作系统为该程序会创建一块内存用来存放代码、运行中的数据和一个执行任务的主线程。方便CPU快速读/写数据。这个在内存中的可执行程序实例(或者说程序的运行时环境)就叫做进程。进程作为一个任务单元,当CPU要执行一个任务,就会启动一个进程
对于前端来说,我们最需要记住进程的特性即是它的隔离性,每个进程都会占用一块独立的内存空间,这意味着进程间是互不干扰的。A进程崩溃了不会影响B进程。有好的一面就有不好的一面:进程间的通信成本较高
每一个应用程序至少包含一个进程,chrome浏览器就是多进程架构(最初是单进程,后续演变成多进程,这个后面会讲到)
进程可以根据其对计算资源和I/O资源的需求分为两种主要类型:计算密集型和I/O密集型
| 特性 | 计算密集型进程 | I/O密集型进程 |
|---|---|---|
| 定义 | 主要依赖CPU进行大量计算的进程 | 主要依赖输入/输出操作的进程 |
| 特点 | 需要大量计算资源 | 大部分时间等待I/O操作完成 |
| CPU使用率 | 高 | 低 |
| 示例 | 科学计算、图像处理、视频编码 | 文件传输、数据库查询、网络请求 |
总的来说:进程是一个程序的运行实例。当用户启动一个应用程序的时候,操作系统会为其分配一块连续的内存,用来存放程序代码、运行中的数据、执行程序的主线程。这样的一个程序运行时环境就是进程(这里其实有点像一段js代码编译后会生成执行上下文。而执行上下文就是一段js代码执行时的环境(里面也包括其它执行时所需要的东西))
线程
有了进程,程序直接运行在进程里面不就好了吗,为什么还要进一步细分呢
试想我们使用一个文本编辑器的程序。自然需要一个进程来存放文本数据以及程序代码。此时通常会有三个处理任务:
- 处理与用户交互的监听键盘按下事件的任务
- 用户输入文档不断更新,处理布局重新渲染的任务
- 每隔一段时间,将最新文档保存到硬盘中的任务
这三个任务都是对同一个文档进行读写,肯定是在同一个进程中。但是如果只有一个进程处理这些任务。效率不高且体验不好。更好的方法派出更为轻量级的三个线程来并行处理,如下图
注意上面图中的例子是不是严格意义上的并行取决于电脑cpu的数量。如果是单核CPU,哪怕不同线程任务切换再快,也并不是真正意义上的并行
线程是并行的最小单位,线程的特点是数据共享,线程间的通信成本较低。多线程可以并行处理多个任务。效率高。但是最大的问题在于,只要有一个线程崩溃,整个进程就会崩溃
两者对比
一图胜千言
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 操作系统分配资源的基本单位 | 进程中的一个执行单元 |
| 资源占用 | 独立的内存空间,通信复杂 | 共享进程的内存和资源,通信简单 |
| 创建和销毁 | 开销较大,需要分配和回收独立资源 | 开销较小,线程共享进程资源 |
| 调度 | 粒度较大,切换需保存和恢复状态 | 粒度较小,切换开销低 |
| 并发性 | 可以并发执行,但协作复杂 | 更容易实现并发,因共享同一内存空间 |
另外也可以参考阮一峰的这篇文章进程与线程的一个简单解释,形容的非常贴切易懂
异步、同步、串行、并行、并发
这几个概念对于后续我们理解浏览器的事件循环机制非常的重要,但同时非常容易混淆,这里我们整体来捋一下
同步
var a = 2
function sum(b,c){
return b+c
}
function sumAll(b,c){
var d = 10
result = sum(b,c)
return a + result + d
}
sumAll(3,6)
以上代码便是一段典型的同步代码,代码的执行顺序是"由上至下",后面代码必须等前面代码执行完成才可以执行
注意这里只是为了方便说明同步的例子,由于js存在变量提升特性,代码并不是严格从上往下执行。更精确的说,同步代码的执行顺序是按照
函数(执行上下文)调用栈的调用顺序来执行(有关变量提升的细节可以参考我的这篇文章:你真的理解变量提升吗)
对于本篇文章的主题。这里的核心点是:同步是针对同一个线程的概念
异步
有一个比较常见的观点是:异步任务是不能够立刻获取结果的任务。有点道理但是不准确
setTimeout是一个典型的异步场景
function test() {
console.log(1)
}
setTimeout(test, 1000)
console.log(2)
上面这段代码中,js引擎执行setTimeout,此时主线程会向定时器线程发送一个定时事件。1s后定时完成定时器线程再通知主线程
需要特别注意:定时结束后,异步任务(这里指test函数)并不一定会立刻执行,需要等待
js宿主环境调度执行(这个调度规则就是事件循环机制)定时结束后,只是异步任务的处理权交还给了
主线程,但具体什么时候执行说不定。打个比方你设定了闹钟早上6:30起床,但是实际你可能会再睡5分钟。甚至直接把闹钟关掉(手动把定时器清除)。当然你也可能相当自律,闹钟一响就立马起床了。这也是定时器不准确的原因之一
比如下面的代码即便定时器结束,回调函数也不会立刻执行
const now = performance.now()
setTimeout(() => {
console.log('11 :>> ', 11);
}, 1000);
while(performance.now() - now < 3000) {}
注意:为了重点表达异步概念。有关主线程与定时器线程的“交互”只是简单的描述,这里先卖个关子,在后续事件循环会详细介绍
我们需要明白一个核心点:异步是针对不同线程的概念。当我们执行一段异步代码,实际就是向其它线程派发某个事件,待将来某个时刻事件得到了响应,其它线程会通知主线程。换言之:所有需要其它线程参与的任务都可以称之为异步任务。但是主线程只会按部就班的执行一个个给它分配的任务。换言之都是同 步执行的(包括js本身也是单线程,它是同步执行代码的),理解这个点非常的关键
串行/并行/并发
依旧拿我们前面文本编辑器的例子来说(为了方便阅读,把图再复制一遍)
上面的文本编辑器程序进程。如果是单核CPU,一个cpu同一时间只能做一件事情。但是当cpu处理速度过快,人们从感知上会认为这三个线程所对应的任务是同时处理,这就是并发,它单纯的代表同一时间段执行多项任务的能力(这里的同时只是人们感知上的同时),它可以通过时间分配的方式实现(感兴趣的小伙伴可以自行了解,这里就不深入介绍了)
如果是多核CPU,那么三个线程的任务便可以做到真正意义上的同时处理。这就是并行,而串行则代表异步任务之间的执行有先后关系,需要等这个异步任务执行完才可以执行下个异步任务(注意这里不要和同步搞混)
串行/并行都是针对不同线程的概念,并发是针对cpu的概念
总结
这里简单的做一个总结:
| 特性 | 定义 | 针对概念 |
|---|---|---|
| 同步 | 程序代码严格按照函数执行上下文调用栈的顺序执行 | 针对同一个线程 |
| 异步 | 当需要其他线程参与的任务都是异步任务,JS 线程不会等待异步任务的结果,会继续执行接下来的代码 | 针对多个不同线程 |
| 串行 | 多个异步任务按顺序执行 | 针对多个不同线程 |
| 并行 | 多个异步任务同一时刻进行 | 针对多个不同线程 |
| 并发 | 单个 CPU 采用合适的方式高效处理多个线程任务 | 针对单个 CPU |
最后
本篇文章主要介绍了一些计算机基础但是极容易混淆的概念,正确清晰的理解这些概念将极大帮助后续我们正确高效清晰的理解浏览的事件循环机制。js是单线程这句话更准确的说法是js引擎(v8)在渲染主线程中。js引擎无所谓同步异步,它都是按照调用栈的顺序执行js代码。异步机制极大帮助了渲染主线程能够并发的处理各种任务
这里留一道思考题:阻塞/非阻塞是怎么回事?它们和同步/异步是什么样的关系?有没有必然的联系?
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享。
如果你有疑问或者出入,评论区告诉我,我们一起讨论