我们大家都知道,js是单线程的,但是大家有没有想过,为什么js是单线程的?这里先引出一个概念,进程和线程。
进程
通俗来讲就是程序运行的时候需要有它自己的专属内存空间,我们可以把这个内存空间简单理解为进程;
官方说法是:指计算机中正在运行的一个程序实例。每个进程拥有独立的内存空间、运行环境和系统资源,如文件描述符、信号处理器等。可以将进程看作是程序在计算机上的一次执行过程,它是操作系统分配资源和调度的基本单位。在操作系统中,每个进程都有唯一的进程标识符(PID),用于区分不同的进程。
线程
通俗来讲就是相当于“人”,在一个部门里面,每个人都有自己要做的事,来维持部门的运作,这里面的人,都是一个线程;
官方说法是:进程中的一个执行单元,它与其他线程共享相同的内存空间和系统资源。线程是操作系统调度的最小单位,也是程序并发执行的一种方式。与进程相比,线程的创建、切换和销毁开销较小,可以在更小的时间内完成任务,因此线程通常被用于实现程序的并发和多任务处理。在操作系统中,每个线程都有唯一的线程标识符(TID),用于区分不同的线程。
一个进程里面至少要有一个线程,也就是主线程,进程之间相互独立,即使要进行通信,也需要双方同意;如果程序要同时执行多块代码,主线程就会启动更多线程来执行代码,所有一个进程里面可以有多个线程
浏览器
我们使用的浏览器,其实是一个多进程,多线程的应用程序。它内部工作十分复杂,为了防止崩溃,它会在运行时启动多个进程,避免相互影响。
而在浏览器的众多进程中,我们只需关注一个:渲染进程。
意思是,当浏览器执行渲染进程时,会开启一个渲染主线程,它负责执行解析HTML,CSS,JS代码,渲染页面,加载事件等等操作。
默认情况下,浏览器会为每一个标签页开启一个渲染主线程,以此保证页面之间互不影响。
渲染主线程
我们来看看渲染主线程是如何工作的: 它是浏览器中最繁忙的线程,需要它处理的任务有很多:
- 解析HTML
- 解析CSS
- 计算样式
- 布局
- 处理图层
- fps刷新
- 执行全局js代码
- 执行事件处理函数
- 执行计时器的回调函数
那么既然这个渲染主线程这么忙,为什么我们不用多个线程来处理这些事情呢?
实际上,要处理这么多任务,如果开启多线程,那么会遇到一个致命的问题:如何调度任务?
比如:
- 我正在执行一个js函数,还未执行完时,用户点击了事件按钮,这时我应该去停止当前任务去执行新的任务吗?
- 我正在执行一个js函数,当某些计时器的时间在我执行完之前结束了,我该立即去执行它的回调函数吗?
因此,无法使用多线程,而渲染主线程想出来一个解决办法:排队
也就是说,在开始的时候,
- 渲染主线程会进入一个无限循环
- 每次执行时,会检查消息队列里面是否有任务存在,如果有,就取出第一个任务执行,执行完就进入下一个循环,如果没有,就进入休眠状态
- 其他所有线程可以随时向消息队列添加任务,新任务会在消息队列的末尾,等待主线程的循环调用
这整个过程,被称为事件循环
什么是异步?
在js代码运行的过程中,我们会遇到一些无法立即拿到返回值或者立即处理的任务,比如:
- 计时器完成后的回调
- 网络请求返回的结果
- 用户操作后需要执行的任务
如果让渲染主线程等待这些任务的执行完毕,那么就会导致主线程长时间处于堵塞状态,从而导致页面“卡死”,因此,渲染主线程无论如何不能阻塞!
因此,浏览器选择使用异步来解决这个问题。
怎么理解异步?
js是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个,同时它又承担着许多工作,比如渲染页面,执行代码等等。
如果使用同步的方式,极可能导致阻塞,从而导致消息队列中的许多任务无法执行,这样一来,会消耗大量的时间,导致页面无法及时更新,造成“卡死”现象。
所以浏览器采用异步的方式来避免出现上述情况,具体做法是,当出现计时器任务,promise回调,网络请求等任务时,主线程会将这些任务交给其他线程来完成,自身立即结束任务的执行,继续执行后续代码。
当其他线程完成任务时,会将事先传递的回调函数包装成新的任务放入消息队列的末尾排队,等待主线程的调度执行,这样一来,就保证了单线程的流畅性,浏览器也不会阻塞。
js的事件循环
根据w3c的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在同一个队列中执行,不同类型的任务可以在不同队列中执行,在一次事件循环中,浏览器可以根据实际情况从不同队列中自行选择任务执行
- 浏览器必须要有一个微队列,微队列中的任务必须优先其他所有任务执行
在chrome浏览器中,跟开发相关的有这三类队列:
- 延时队列:用于存放计时器到达后的回调任务 (setTimeout等) 优先级(中)
- 交互队列:用于存放用户操作后产生的时间处理任务 (addeventListener等) 优先级(高)
- 微队列:用于存放用户最快需要执行的任务 (promise等) 优先级(最高)
那么总结一下:
事件循环又叫消息循环,是浏览器渲染主线程的工作方式。 它是一个不会结束的for循环,每次循环都从消息队列中取出第一个任务执行,而其他线程只需要在执行完的时候将得到的回调包装成新任务添加到消息队列末尾即可。
在以前将异步任务简单地分为宏任务和微任务,但是现在根据w3c最新标准:
每个任务都有一个任务类型,同一个类型的任务必须在同一个队列中执行,不同类型的任务可以在不同队列中执行,在一次事件循环中,浏览器可以根据实际情况从不同队列中自行选择任务执行,浏览器必须要有一个微队列,微队列中的任务必须优先其他所有任务执行。
衍生:js中的计时器能做到精确计时吗?为什么
肯定是不能的,因为受到事件循环的影响,计时器的回调函数只能在渲染主线程空闲的时候运行,因此在等待时,就发生了偏差。
最后
总结两句话:
单线程是异步产生的原因,事件循环是异步的实现方式。