什么是进程?什么是线程?
- 进程:程序运行的专属内存空间,进程之间相互独立,即使要通信,也要经过双方同意。例如qq和微信运行时在内存中占用的空间分别是qq进程和微信进程
- 线程:线程是进程的组成部分,一个进程至少有一个线程,进程开启后会有一个主线程。
浏览器的进程模型
浏览器是一个多进程多线程的应用程序
为了减少互相影响和连环崩溃的几率,浏览器启动后,会自动启动多个进程,例如:
- 浏览器进程:负责界面展示、用户交互、子进程管理等
- 网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务
- 渲染进程:渲染进程启动后会开启一个渲染主线程,负责执行html、css、js代码,默认情况下,每个标签页都会开启一个新的渲染进程,保证不同标签页之间不会相互影响
渲染主线程如何工作
渲染主线程是浏览器中最繁忙的线程,它的任务包括但不限于:
- 解析html
- 解析css
- 计算样式
- 布局
- 处理图层
- 每秒把页面画60次
- 执行全局js代码
- 执行事件回调函数
- 执行计时器回调函数 ……
为什么浏览器的渲染进程不启用多个线程来处理这些任务?为什么js设计成单线程的?
- 早期的浏览器功能较为简单,js设计成单线程模式能够快速开发,更易于维护
- 线程之间的通信较为复杂,造成性能开销和死锁、竟态问题。比如a线程在操作xx节点,b线程要删除xx节点,那此时交互的处理就会变得极为复杂
单线程的问题?
- 主线程正在执行一个函数,此时用户点击了按钮,主线程是应该停下来执行用户的操作还是继续执行
- 主线程正在执行一个函数,此时定时器到达了时间,主线程是应该执行定时器的回调还是继续执行
- 用户点击按钮的同时,定时器到达了时间,先处理哪个呢
主线程承担着渲染页面的任务,一旦被某个操作打断,等这个操作完成后再往下执行,这个等待的过程就是阻塞
如何避免阻塞?--异步
由于js的单线程的,这个线程承担着渲染页面、执行js的工作,如果使用同步的方式,极有可能导致主线程产生阻塞,从而导致消息队列中的其他任务无法执行。
主线程不得不等待当前任务执行完再去执行下一个任务,白白地消耗时间,页面也无法及时更新,造成页面卡死的现象。
所以浏览器采用异步的方式来避免这种情况,比如计时器、网络请求、用户操作,这些任务就是异步任务,它们可以等待同步任务执行完再执行,最大限度保证单线程的流畅运行。
同样都是异步任务,哪个先执行?哪个后执行?--消息队列
首先,任务没有优先级,在消息队列中先进先出
消息队列具有优先级
W3C的最新解释:
- 每个任务都有任务类型,同一个类型的任务必须在一个队列中,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
- 浏览器必须准备好一个微队列,微队列中的任务优先于其他任务
随着浏览器的复杂度急剧提升,W3C不再使用宏任务的说法,在目前Chrome的实现中,包含以下队列:
- 延时队列:setTimeout、setInterval,优先级中
- 交互队列:addEventListener,优先级高
- 微队列:promise.then,优先级最高
所以,任务的执行顺序为:同步任务-->微队列-->交互队列-->延时队列
这么多任务,如何有条不紊的在单线程中执行?--事件循环
事件循环又叫消息循环,是浏览器渲染主线程的工作方式。在chrome源码中,初始化时开启一个永不结束的for循环,每次循环从消息队列中取出第一个任务执行,其他线程只需要在合适的时候将任务添加到消息队列的末尾。过去把消息队列分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据W3C的官方解释,每个任务都有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同的消息队列优先级不同,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务优先级最高。
计时器准确性问题
计时器无法做到准确计时,原因:
- 计算中没有原子钟,无法做到精确计时
- 操作系统的计时函数本身存在少量偏差,js计时器最终调用的是操作系统的函数,继承了这些偏差
- 按照w3c的标准,浏览器实现计时器,如果嵌套超过5层,会有4ms的最少时间,这样在计时时间少于4ms时又带来了偏差
- 受事件循环的影响,计时器的回调只能在主线程闲暇时执行,又带来了偏差
js模拟多线程
h5提出的worker可以模拟多线程,但是其他线程都是由主线程控制的,并且不能获取和操作DOM,所以worker的本质还是单线程的
1、先看一段比较耗时的操作,在主线程下的耗时表现
function fb(n) {
if (n === 1 || n === 2) return 1
return fb(n - 1) + fb(n - 2)
}
console.time('fb执行时间1')
fb(40)
console.timeEnd('fb执行时间1')
console.time('fb执行时间2')
fb(40)
console.timeEnd('fb执行时间2')
console.time('fb执行时间3')
fb(40)
console.timeEnd('fb执行时间3')

2、使用worker
- 同级新增worker1.js、worker2.js、worker3.js,将耗时操作分离出去
function fb(n) {
if (n === 1 || n === 2) return 1
return fb(n - 1) + fb(n - 2)
}
console.time('fb执行时间worker1')
const result = fb(40)
console.timeEnd('fb执行时间worker1')
self.postMessage('worker1干完活了')
- 主线程下接收worker的计算结果
const worker1 = new Worker('worker1.js')
const worker2 = new Worker('worker2.js')
const worker3 = new Worker('worker3.js')
worker1.onmessage = (e) => {
console.log(e.data)
}
worker2.onmessage = (e) => {
console.log(e.data)
}
worker3.onmessage = (e) => {
console.log(e.data)
}

可见,三次计算的耗时和一次计算耗时差不多,当这种计算情况越多,使用worker的效果越明显。
js线程
js是单线程的,但是可以通过轮转时间片模拟多线程:
- 任务1 任务2
- 切分任务1 任务2
- 随机排列这些任务片段,组成队列
- 按照队列顺序将任务片段送进js线程
- js线程执行一个一个的任务片段 blog.csdn.net/fuhanghang/…