概念
js是单线程的,即js代码只能在一个线程上运行,也就是说,js同时只能处理一个js任务。这是因为,如果js代码的主要用途是与用户互动和操作,如果被执行在不相同的线程上,那么一个线程在DOM添加内容,一个在删除内容,那会乱套,所以js一开始就是单线程的。
浏览器
既然js执行是单线程的,那么为啥遇到异步操作也没有等待?因为浏览器或node(宿主环境)是多线程的,即浏览器有其他一些线程去辅助js线程的运行, 主要有以下:
- GUI渲染线程
- JS引擎线程
- 定时器触发线程
- 浏览器事件线程
- http请求线程
- EventLoop轮询处理线程
- ...
其中, 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轮询处理线程在主线程与其他线程之间扮演着搬运工的角色,哪里需要搬哪里
从主线程开始看
注意: 只有主线程的同步代码都执行完了,才会去队列看看有啥要执行的
小区别
在异步线程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