区分进程与线程
很多新手对于进程和线程的关系都分不清,这很正常。
先从概念说起
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
举个例子
进程就像一个公司,公司有它独立的资源、空间
公司之间是相互独立的
线程是隶属于该公司下面的员工
一家公司下面有一个或者多个员工
员工之间共享资源、空间
完善一下概念
公司的资源、空间 -> 系统分配的内存(独立的一块内存)
公司之间的相互独立 -> 进程之间相互独立
多个员工合作完成任务 -> 多个线程在进程中协作完成任务
公司内有一个或多个员工 -> 一个进程由一个或多个线程组成
员工之间共享资源、空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
浏览器是多进程的
- 浏览器是多进程的
- 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
- 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了 (所以每一个Tab标签对应一个进程并不一定是绝对的)
浏览器有哪些进程
-
Browser进程:浏览器的主进程(负责协调、主控),只有一个
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
-
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
-
GPU进程:最多一个,用于3D绘制等
-
浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
- 页面渲染,脚本执行,事件处理等
前端重点:浏览器渲染进程
- GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
-
JS引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
-
事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
-
定时触发器线程
- 传说中的setInternal与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML5标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
-
异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
浏览器中的Event Loop
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列然后渲染,将队列中的事件放到执行栈中依次执行
- 主线程从任务队列中读取事件,这个过程是循环不断的
node中的Event Loop
我们先来张图看看node是如何工作的
- 我们写的js代码会交给v8引擎进行处理
- 代码中可能会调用nodeApi,node会交给libuv库处理
- libuv通过阻塞i/o和多线程实现了异步io
- 通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。
事件循环进阶:macrotask与microtask
上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,譬如下面这题:
console.log('开始');setTimeout(function() { console.log('延时器');}, 0);Promise.resolve().then(function() { console.log('promise1');}).then(function() { console.log('promise2');}).then(function() { console.log('promise3');}).then(function() { console.log('promise4');});console.log('结束');复制代码
现在,它的执行顺序是这样的
开始结束promise1promise2promise3promise4延时器复制代码
为什么呢?因为Promise里有了一个一个新的概念:microtask
或者,进一步,JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task
它们的定义?区别?简单点可以按如下理解:
- macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
- 每一个task会从头到尾将这个任务执行完毕,不会执行其它
- 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(`task->渲染->task->...`)复制代码
-
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
- 也就是说,在当前task任务后,下一个task之前,在渲染之前
- 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
- 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前) 分别很么样的场景会形成macrotask和microtask呢?
-
macrotask:主代码块、setTimeout、setInterval、Promise的构造函数是同步的、setImmediate、I/O、UIrendering(可以看到,事件队列中的每一个事件都是一个macrotask)
-
microtask:Object.observe (已废弃),MutationObserver(不兼容,已废弃),MessageChannel(vue中 nextClick 实现原理)等
浏览器和 node 环境执行顺序不同,浏览器是先把一个栈以及栈中的微任务走完,才会走下一个栈。node 环境里面是把所有栈走完,才走微任务
node环境
在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:
- timers:执行满足条件的setTimeout、setInterval回调。
- I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
- idle,prepare:可忽略
- poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
- check:执行setImmediate的回调。
- close callbacks:关闭所有的closing handles,一些onclose事件。
几个队列
除上述循环阶段中的任务类型,我们还剩下浏览器和node共有的microtask和node独有的process.nextTick,我们称之为Microtask Queue和NextTick Queue。 我们把循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue。
循环之前
在进入第一次循环之前,会先进行如下操作:
- 同步任务
- 发出异步请求
- 规划定时器生效的时间
- 执行process.nextTick()
开始循环
按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:
- 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
- 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
- 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
- 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
- 进入下轮循环。
可以看出,nextTick优先级比promise等microtask高。setTimeout和setInterval优先级比setImmediate高。
注意
- 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。
- setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。
参考文章
- 强烈推荐
- 强烈推荐
- 有英文基础的同学可以看看