本文仅是我个人的见解,如理解有误,还望帮忙及时指出,方便及时更正。
正文开始~
进程与线程
这里先贴上阮大神的文章:进程与线程的一个简单解释
我是这样理解的:
- 一个进程就好比工厂的一个车间,一个线程就好比车间里的一个工人,对应一个进程由一个或多个线程组成
- 一个车间有它的独立资源,对应系统分配的独立内存
- 每一个车间是相互独立的,对应每个进程之间是相互独立的
- 每个车间有一或多个工人协同完成任务,对应多个线程在进程中协同完成任务
- 每个车间的空间是工人们共享的,对应一个进程的内存空间是每个线程都可以共享
- 车间里的房间大小不一,能容纳的工人数也不一样,当别的工人占用该房间时,其他人就不能使用。对应当一个线程使用某些内存时,其他线程必须等它结束,才能使用这块内存
- 为了防止多个线程之间产生冲突,就有了很多协调机制,这里就不赘述了
最后,再用官方的话解释下:
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
浏览器是多进程的
稍微深入了解了进程与线程后,就得对我们js最初运行的环境——浏览器——有点新的认识了。
- 浏览器之所以可以运行,是因为操作系统给它分配了CPU和内存
- 浏览器是多进程的
- 一般来说,一个标签页就是一个独立的浏览器进程
上张图:
从上图我们可以看出,浏览器是多进程的。
另外,由于浏览器的优化,有些进程会合并,所以一个便签页对应一个进程并不是绝对的。
浏览器多进程有很多好处,比如当我们打开很多个网页,就相当于打开了多个进程,其中一个网页的卡顿不会对别的网页造成影响,让用户的体验更佳。
浏览器内核(渲染进程)
终于到了重点!前面讲了那么多进程,然而对于前端开发工作人员,最重要的就是渲染进程
js的执行,页面的渲染等操作都在这个进程中进行,而它是多线程的
一个浏览器内核通常包括以下线程:
- GUI 渲染线程
- 负责页面的渲染,包括重绘
- 它与JS引擎线程是互斥的,当JS引擎执行时GUI渲染线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中直到JS引擎空闲时立即被执行
- JavaScript引擎线程
- 负责处理js程序,运行js代码
- 同样,它与GUI渲染引擎也是互斥的,所以当js代码执行时间过长时,就会造成页面阻塞
- 定时触发器线程
- setTimeout和setInterval所在的线程
- 浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此通过单独线程来计时并触发定时是更为合理的方案
- 事件触发线程
- 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理
- 异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理
执行栈与任务队列
事件循环
javascript是单线程的,这是由于它的诞生就是浏览器脚本语言,就是为了与用户交互以及操作DOM。如果它是多线程的话,当多个线程同时操作DOM时,浏览器应该以哪个线程为准呢?所以,它生来就是单线程。
当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象;而栈中则存放着一些基础类型变量以及对象的索引。我们这里说的执行栈和上面这个栈的意义却有些不同。我们知道,当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),也叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。
单线程就意味着在同一时间只有一个任务能被执行,所有的任务需要排队,只有前一个任务执行完毕后面的任务才能被执行。如果因为计算量比较大,CPU忙不过来,也还能忍受;可是大部分情况下CPU是空闲的,比如ajax读取数据很慢,必须等结果出来才继续往下执行。这样性能就很低,于是便有了同步任务和异步任务。同步任务是指在主线程上排队的任务,当前一个任务执行完毕就会执行后一个任务;异步任务是指不进入主线程,而是进入“任务队列”,只有“任务队列”通知主线程某个异步任务可以执行了,它才会进入主线程。
可总结如下:
- 所有同步任务都在主线程上执行,形成执行栈
- 所有异步任务都在“任务队列”上,只要异步任务有了结果,就在异步任务中添加一个事件
- 当执行栈中所有任务都执行完毕,主线程处于闲置状态时,从“任务队列”的队首读取事件加入到执行栈执行
- 一直循环以上步骤,这个过程就叫做“事件循环(Event Loop)”
下图可以很好的展示这个情况:
图中的stack就是我们说的执行栈,WebAPIs代表一些异步事件,callback queue代表事件队列。
micro task 和 macro task
我们先看一段代码:
以上代码的执行结果是什么呢?不卖关子了,直接上结果吧
|
|
你答对了吗?没答对也不要紧,下面我们来分析一下
之前介绍的事件循环只是大概的一个过程,实际上不同的异步任务,它们的执行优先级也不一样。异步任务分为两类:微任务(micro task)和宏任务(macro task)
macro task:每次执行栈执行的代码就是一个宏任务
- 主代码块,setTimeout,setInterval等
micro task:当前宏任务执行结束后立即执行的任务
- Promise,MutaionObserver,process.nextTick等
我们知道,在一个事件循环中,异步事件返回结果后会添加一个事件到任务队列。实际上,会根据这个异步事件的类型,会被添加到对应的宏任务队列或者微任务队列上。当执行栈为空时,主线程会查看微任务队列是否有事件存在。如果不存在,再去宏任务队列取出事件加入到当前执行栈;如果存在,则依次执行队列中的事件,直到队列为空,再去执行宏任务队列中的事件。我们只需要记住:微任务队列中的事件优先级大于宏任务队列,微任务永远在宏任务之前执行
定时器
前面我们说过定时器(setTimeout,setTimeInterval)并不是由JS引擎计数的,而是在由单独线程计数。定时器功能主要由setTimeout() 和 setInterval()两个函数来完成,这两个函数内部运行机制完全一样,唯一的区别在于前者指定的代码只执行一次,而后者反复执行。这里主要用前者举例。
|
|
以上代码的执行结果永远是2,1;因为第二行代码是同步任务,在主线程中,而第一行是异步任务,在任务队列(宏任务队列)中;只有主线程任务全部执行完毕后才会执行任务队列中的回调。setTimeout(fn, 0)就意味着fn会尽可能早的执行,但它永远在同步任务、微任务队列以及现有的宏任务队列(已经完成的异步任务)之后才会执行。
HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。
注意:setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
到这里,我们再看一下这段代码:
现在就可以解释上面代码的执行结果了。