js事件循环机制

235 阅读6分钟

进程&线程

进程(process)是资源分配的最小单位

线程(thread)是CPU调度的最小单位

做个简单的比喻:

进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)

  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)

  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)

  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)

  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)

浏览器是多进程

浏览器中打开一个网页相当于新起了一个进程,当然,浏览器出于自己的优化,有时会将多个进程合并成同一个进程(比如打开多个空白页)

浏览器的多进程包括:

image.png image.png

浏览器的渲染进程是多线程的主要有:

image.png

1.GUI渲染线程

负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

2.JS引擎线程

也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

3.事件触发线程

归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助) 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

4.定时触发线程

setInterval与setTimeout线程

5.异步http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求

总结:

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起, GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

同理JS如果执行时间过长就会阻塞页面。

js是单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

js执行栈

每个函数调用都有自己的执行上下文。当代码执行流进入函数时,函数的执行上下文被推到一个执行上下文栈上。 在函数执行完之后,执行上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。执行栈遵循先进后出原则

1670d2d20ead32ec.gif

任务队列:同步任务&异步任务

所有的任务可以分为同步任务和异步任务

同步任务:立即执行的任务,同步任务在主线程上排队执行,形成一个执行栈

异步任务:异步执行的任务,比如ajax网络请求,setTimeout等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调

js Event Loop

  • 所有任务都在主线程上运行,执行JS代码的时候就是往执行栈中放入函数
  • 同步任务直接执行遇到异步任务会把这个事件挂起继续执行执行栈中的其他任务
  • 当异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列--事件队列(Event Queue)中
  • 被放入事件队列的任务不会立即执行而是等到执行栈中的所有任务都执行完毕
  • 当主线程处于空闲状态后主线程去查找事件队列中是否有任务。如果有主线程会从中取出排在第一位的事件并把这个事件对应的回调放入执行栈中,然后执行同步代码
  • 如此反复形成一个无限循环,这个过程就被称为事件循环(Event Loop)

可以参考下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。

image.png

以及:

image.png

微任务&宏任务

不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)

  • 微任务(microtask):Promise.then、process,nextTick(Node.js)、MutationObserver
  • 宏任务(macrotask):setTimeout、setInterval、I/O、事件、postMessage、setImmediate、requestAnimationFrame、UI渲染、script

每次主线程执行栈为空的时候,引擎会优先处理微任务队列,处理完微任务队列中的所有任务,再处理宏任务

最后

经典题目一试

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
// script start   async2 end  Promise  script end   async1 end  promise1  promise2 setTimeout

参考