首先我们都知道js是单线程,之前一直很困惑,明明js是单线程,为什么还可以处理异步函数。现在终于知道,虽然js是单线程的,但是浏览器或者node(宿主环境)中会存在其他的线程来辅助js引擎线程的运行。
今天我们来讨论一下当js在浏览器中运行时,js引擎线程的运行过程以及浏览器中的辅助线程如何帮助js引擎线程运行。
1、 浏览器
浏览器中有很多线程去辅助js引擎线程的运行,这些线程包括:
定时器触发线程(setTimeout)
http异步线程
浏览器事件线程(onclick)
GUI渲染线程
js引擎线程
事件轮询线程(Event Loop)
其中浏览器事件线程与事件轮询线程并不是官方的说法。不过这里为了便于理解,就先这样定义。接下来我们主要讨论定时器触发线程(setTimeout)、http异步线程、事件轮询线程(Event Loop)、js引擎线程、浏览器事件线程。
2、线程与进程
首先我们来了解一下线程与进程的概念。
我们打开任务管理器,可以看到正在运行的进程,可以认为,一个进程相当于一个应用程序。一个进程的运行可以有多个线程的相互配合,比方说QQ进程的运行,需要有接受消息线程,文件传输线程等线程的配合。而我们打开一个网页就相当于开启了一个进程,打开3个网页就相当于开启了3个进程,一个网页进程的正常运行,也需要许多线程的配合,一些主要的线程包括:
类型A:GUI渲染线程
类型B:js引擎线程
类型C:事件轮询线程
类型D:定时器触发线程、http异步线程、浏览器事件线程
我们现在只考虑类型B,类型C,类型D之间的关系。
3、js引擎线程
js引擎线程就是执行js代码的线程,也就是主线程。一般来说,如果是同步代码,就可以在主线程中直接运行,如果是异步代码,就交给其他线程运行,异步函数执行成功后的回调函数会放在消息队列中,在适当的时候放入主线程中执行。比如:
1 var a = 2;
2 setTimeout()
3 ajax()
4 console.log()
第1,2行代码会直接在主线程中运行,而第2行代码会交给定时器触发线程(setTimeout)处理,第3行代码会交给http异步处理线程,异步处理线程主要做两件事:
处理主线程丢过来的代码
保存好异步函数执行成功后的回调函数
主线程在运行代码时,会形成一个执行栈,处理函数的嵌套问题。
4、定时器触发线程(setTimeout)、http异步线程、浏览器事件线程(onclick)
主线程在遇到异步代码时,会交给对应的异步线程去处理,比如:
1 var a = 2;
2 setTimeout(fun A)
3 ajax(fun B)
4 console.log()
5 dom.onclick(func C)
第1,4行是同步代码,直接在主线程中运行,第2行是异步代码,交给定时器触发线程去执行,第3行是异步代码,交给http处理线程去执行,而第5行也是异步代码,交给浏览器事件线程去执行。
主线程把setTimeout、ajax、dom.onclick分别给三个线程,他们之间有些不同
对于setTimeout代码,定时器触发线程在接收到代码时就开始计时,时间到了将回调函数扔进队列。
对于ajax代码,http 异步线程立即发起http请求,请求成功后将回调函数扔进队列。
对于dom.onclick,浏览器事件线程会先监听dom,直到dom被点击了,才将回调函数扔进队列。
5、消息队列
可以理解为一个静态的队列存储结构,非线程,只做存储,里面存的是一堆异步成功后的回调函数字符串,肯定是先成功的异步的回调函数在队列的前面,后成功的在后面。
注意:是异步成功后,才把其回调函数扔进队列中,而不是一开始就把所有异步的回调函数扔进队列。比如setTimeout 3秒后执行一个函数,那么这个函数是在3秒后才进队列的。
6、事件轮询线程(Event Loop)
事件轮询线程就是主线程、其他的异步线程、消息队列三者之间沟通的桥梁,看一下如下例子:
setTimeout(() => {
console.log(1)
}, 2000)
setTimeout(() => {
console.log(2)
}, 3000)
一开始,消息队列是空的,第1个定时器会交给定时器处理线程进行处理,2秒之后会将它的回调函数放入消息队列当中,同理,第2个定时器会交给定时器处理线程进行处理,3秒之后会将它的回调函数放入消息队列当中,如图过程:
顺时针来看,这是大概的步骤,当主线程的任务执行完之后,会不断的询问消息队列中是否有任务,如果有任务,就放到主线程中去执行。
7、实例
var a = 111;
setTimeout(function() {
console.log(222)
}, 2000)
fetch(url) // 假设该http请求花了3秒钟
.then(function() {
console.log(333)
})
dom.onclick = function() { // 假设用户在4秒钟时点击了dom
console.log(444)
}
console.log(555)
// 结果
555
222
333
444
步骤1:
主线程只执行了var a = 111;和console.log(555)两行代码,其他的代码分别交给了其他三个线程,因为其他线程需要2、3、4秒钟才成功并回调,所以在2秒之前,主线程一直在空闲,不断的探查队列是否不为空。
此时主线程里其实已经是空的了(因为执行完那两行代码了)
步骤2:
2秒钟之后,setTimeout成功了
步骤3:
步骤4:
注意:
图里的队列里都只有一个回调函数,实际上有很多个回调函数,如果主线程里执行的代码复杂需要很长时间,这时队列里的函数们就排着,等着主线程啥时执行完,再来队列里取
所以从这里能看出来,对于setTimeout,setInterval的定时,不一定完全按照设想的时间的,因为主线程里的代码可能复杂到执行很久,所以会发生你定时3秒后执行,实际上是3.5秒后执行(主线程花费了0.5秒)
8、结尾
这是我在了解了Js线程、EventLoop、事件循环、消息队列相关知识后写下的笔记。原文是掘金上的,链接为:
作者:晴天633
链接:https://juejin.cn/post/6844903752621637645
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。