js为什么是单线程?
主要是因为最开始javascript
是单纯的服务于浏览器的一种脚步语言(那时候没有nodejs
)。浏览器是为了渲染网页,通过dom
与用户交互,如果一个线程需要给dom
执行click
事件,而另一个进程要删除这个dom
,这2个动作可能同时进行,也可能先后进行(像java,c#
等语言中会引入锁的概念,这样会变得异常复杂),那么就会造成很多不可预料的错误。
浏览器是多线程的
浏览器打开一个tab,就会单独开一个进程,这个进程包含多个线程,参考:JS运行机制
主要包含的线程有:
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
上面列出的线程之间,有一个重要的规则是:GUI渲染线程与JS引擎线程互斥,那么我们可以得出以下结论JS阻塞页面加载,那么在js
运行的这段时间内,GUI
的渲染会停止,这段时间内的界面交互,DOM
的重绘与回流会停止,会被保存到待执行队列中,直到js
线程空闲,才会执行这些队列。
我们用下面的一段代码和运行结果来说明这个机制:
<html>
<head>
<style>
.box {
width: 200px;
height: 200px;
margin-top: 100px;
background: #f09;
animation: bounce 2s linear 0s infinite alternate;
background-image: linear-gradient(45deg, #3023AE 0%, #f09 100%);
}
@keyframes bounce {
0% {
border-radius: 40% 60% 72% 28% / 70% 77% 23% 30%;
}
100% {
border-radius: 75% 25% 24% 76% / 13% 15% 85% 87%;
}
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
// 计算斐波那契数列,这个数列从第3项开始,每一项都等于前两项之和。
function recurFib(n) {
if (n < 2) {
return n;
} else {
return recurFib(n - 1) + recurFib(n - 2)
}
}
window.onload = function () {
setTimeout(function () {
console.time("运算耗时:")
// 计算n为40的结果
console.log('结果:', recurFib(40))
console.timeEnd("运算耗时:")
}, 2000)
document.getElementsByClassName("box")[0].addEventListener('click', function () {
console.log('click')
})
}
</script>
</html>
可以看到,一开始网页和动画正常运行,但是开始执行计算斐波那契数列后,动画就停止了,页面也停止响应鼠标的click
事件了,直到recurFib(40)
计算出结果后,动画才开始继续执行,而期间积攒的click
事件也在一起被执行。这就解释了GUI渲染线程与JS引擎线程互斥。由于这个弊端HTML5
提出Web Worker标准。
利用Web Worker开启一个子线程
Web Worker 有以下几个使用注意点。
以上规则引用阮一峰老师的: Web Worker 使用教程
创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
JS引擎线程与worker线程间通过特定的方式通信(postMessage API
,需要通过序列化对象来与线程交互特定的数据)。
下面我们用worker
的相关api
来解决上面卡顿的问题。
<!--index.html主线程-->
<html>
<head>
<style>
.box {
width: 200px;
height: 200px;
margin-top: 100px;
background: #f09;
animation: bounce 2s linear 0s infinite alternate;
background-image: linear-gradient(45deg, #3023AE 0%, #f09 100%);
}
@keyframes bounce {
0% {
border-radius: 40% 60% 72% 28% / 70% 77% 23% 30%;
}
100% {
border-radius: 75% 25% 24% 76% / 13% 15% 85% 87%;
}
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
window.onload = function () {
// 创建一个子线程worker实例
var worker = new Worker('./test.js');
setTimeout(function () {
// 通信:向子线程发送消息
worker.postMessage('start')
}, 2000)
worker.addEventListener('message', function(res) {
// 通信:收到子线程消息
console.log('result:',JSON.stringify(res.data));
// 关闭worker线程
worker.terminate();
})
document.getElementsByClassName("box")[0].addEventListener('click', function () {
console.log('click')
})
}
</script>
</html>
// test.js子线程代码
// 通过监听message来接受主线程中的消息
addEventListener('message', function(res) {
// 子线程向主线程中发生消息
// 计算斐波那契数列,这个数列从第3项开始,每一项都等于前两项之和。
if(res.data === 'start') {
// 开始运算
console.log('收到主线程消息,开始运算')
function recurFib(n) {
if(n < 2){
// 主动关闭子线程
// this.close()
return n ;
}else {
return recurFib(n-1)+recurFib(n-2)
}
}
console.time("运算时间:")
// 计算n为40的结果
var count = recurFib(40)
console.timeEnd("运算时间:")
// 向主线程发送消息
console.log('运算完毕,发送消息给主线程!')
this.postMessage(count);
}
})
运行结果:
可以看到整个运行过程动画没有卡顿,也能响应click
事件,所以在我们遇到大型计算的时候,请单独开启一个worker
子线程来解决js
线程阻塞GUI
线程的问题。上文中只涉及到一部分worker API
。关于worker
更详细更具体的用法可以参见: Web Worker 使用教程
兼容性
可以看到除了Opera Mini浏览器,连IE都能使用了,所以兼容性问题不大。
总结
- 由于
javaScript
的最初设计特点,采用了单线程的运行机制。 - 浏览器是多个线程相互协作来工作的,但是GUI渲染线程与JS引擎线程互斥。
-
js
线程在运行时,会锁死GUI
渲染线程,为了利用多核CPU的计算能力,HTML5
提出Web Worker标准。 -
Web Worker
的使用有一些限制,比如说:同源限制,DOM
限制,文件限制等,但能解决在js
需要大量计算工作时,页面卡顿的问题。 -
Web Worker
实际上是js
线程的一个子线程,理论上js
还是单线程的。