js事件模型的简单理解

487 阅读4分钟

概念

js是单线程的,即js代码只能在一个线程上运行,也就是说,js同时只能处理一个js任务。这是因为,如果js代码的主要用途是与用户互动和操作,如果被执行在不相同的线程上,那么一个线程在DOM添加内容,一个在删除内容,那会乱套,所以js一开始就是单线程的。

浏览器

既然js执行是单线程的,那么为啥遇到异步操作也没有等待?因为浏览器或node(宿主环境)是多线程的,即浏览器有其他一些线程去辅助js线程的运行, 主要有以下:

  1. GUI渲染线程
  2. JS引擎线程
  3. 定时器触发线程
  4. 浏览器事件线程
  5. http请求线程
  6. EventLoop轮询处理线程
  7. ...

其中, 1, 2, 6 为常驻线程

进程与线程

进程

我们打开电脑的任务管理器可以看到正在运行的程序,可以认为一个进程就是一个程序

比如我们打开了一个网页,就相当开启了一个进程,打开三个,则开启了三个进程

一个**进程**的执行,需要有多个**线程**的互相配合比如打开网页,就需要以上几个线程配合工作

线程

网页线程可以大概分为以下几列

  • 类别A: GUI渲染线程

  • 类别B: JS引擎线程

  • 类别C:EventLoop轮询处理线程

  • 类别D: 其他线程,有定时器触发线程,http异步线程,浏览器事件线程等等

其中A与B是互斥的线程,也就是GUI引擎在渲染的时候会阻塞js引擎计算。因为如果GUI渲染的时候,js改变了DOM,那么会造成渲染不同步。

类型B

js引擎线程,称之为主线程,主线程是运行同步代码的地方 1、var a = 1
2、setTimeout()
3、ajax()
4、console.log()

第1、4是同步代码,会放在主线程中运行,23会放在其他线程(工作线程)运行 主线程运行代码时,会生成一个执行栈(先进后出),执行完毕就出栈(ps:递归函数如果太深,会抛出栈溢出Error)

消息队列(任务队列)

可以理解为一个静态的队列存储结构,非线程,只做存储,里面存的是异步操作成功后的回调函数,先进先出
还可以细分为宏任务队列和微任务队列,后面讲

类别D

主线程在遇到setTimeout代码,会将其交给定时器触发线程执行
遇到ajax代码,交给http异步线程执行
遇到click等dom事件,交给浏览器事件线程执行

这几个线程的工作:

1、执行主线程扔过来的异步代码

2、保存回调函数,异步代码执行成功后,通知EventLoop轮询处理线程 过来取对应的回调函数,派发进任务队列

类型C

EventLoop轮询处理线程在主线程与其他线程之间扮演着搬运工的角色,哪里需要搬哪里

js事件模型.png

从主线程开始看

注意: 只有主线程的同步代码都执行完了,才会去队列看看有啥要执行的

小区别

在异步线程D,还有一些小区别:

1、对于setTimeout代码,定时器触发线程在接收到代码的时候就开始计时,时间到了就通知EventLoop将callback放入任务队列,等带主线程闲置,才执行callback,所以定时器是“不准”的

2、ajax代码,http异步线程立即发送请求,请求成功/失败后通知EventLoop将callback放入任务队列

3、dom.onclick代码,浏览器事件线程会先监听dom,直到dom被点击了,才通知EventLoop将callback放入任务队列

宏任务和微任务

异步任务又分为宏任务跟微任务

宏任务:宏任务是javascript中最原始的异步任务,包括setTimeout,setInterval,Ajax等,在主线程中按照代码的顺序,依次进入工作线程挂起,根据异步任务到达的时间节点依次进入任务队列,等待函数执行栈空了,就执行,先进先出

包括: I/O, setTimeout, setInterval, setImmediate(node) requestAnimationFrame(浏览器)

微任务:微任务是随着ECMA标准提出新的异步任务,在原有异步任务中提出【微任务】概念。每个宏任务执行前,优先清除当前事件循环未执行的微任务。每个宏任务内部可以注册当次任务的微任务,微任务也是按照进入任务队列的顺序执行的

包括:process.nextTick(node)、MutationObserver(浏览器)、Promise.then catch finally


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

requestAnimationFrame(function(){ 
    console.log('UI update')
}) 

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

new Promise(function executor(resolve) {  
     console.log('promise 1') 
     resolve() 
     console.log('promise 2')
 }).then(function() {
         console.log('promise then') 
     }) 
     
 console.log('end')
 
 // 执行结果
 promise 1 
 promise 2
 promise then
 
 UI upadte 这个要看浏览器的刷新率
 timer1
 timer2
document.addEventListener('click', function(){  
    Promise.resolve().then(()=> console.log(1));  
    console.log(2); 
}) 

document.addEventListener('click', function(){    
    Promise.resolve().then(()=> console.log(3));             console.log(4); 
})

// 执行结果
2
1 (onclick也是个宏任务,所以在执行前需要想清除本次循环的微任务队列)
4
3