代码是种艺术,甚于蒙娜丽莎的微笑
同步与异步的概念
- 同步:必须等上面的任务或代码执行完成后才能执行
- 异步:无须等待上面的任务或代码执行完就可以执行,可以和上面的任务并行执行
单线程与多线程概念
- 单线程:同一时刻只能做一件事【同步】
- 多线程:同一时刻可以做多件事【异步】
JS借助浏览器多线程实现异步
JavaScript就其本身而言是单线程的,即只会有一个线程从上往下运行JavaScript代码
但是在日常开发中,我们经常能写出多线程的异步代码。比如:定时器,ajax请求等常见功能
但js的宿主环境(比如浏览器,Node)是多线程的,宿主环境通过某种方式(事件驱动)使得js具备了异步的属性。
即我们js引擎无法实现多线程,无法实现异步,但是我们可以借助浏览器内核来实现。这也就是我们平常写的定时器,ajax代码能异步执行的原因
JS+浏览器实现多线程异步过程
- 浏览器内核可以同时运行多个线程进行异步执行
- 这些线程在浏览器内核的控制下相互配合以保持同步。
- 浏览器内核至少会有三个常驻线程
Js引擎线程【执行js代码】
页面渲染线程【执行网页渲染操作】
浏览器事件触发线程 - 浏览器内核中还会有一些临时线程,如:http请求线程
关于任务队列(事件队列)
js是单线程语言,浏览器只分配给js一个主线程(js引擎线程),用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行,就形成了任务队列
所以js引擎线程一直在做一个工作,那就是不断的从任务队列里提取任务,放到主线程里执行
浏览器内核帮助js实现异步流程
①说明
前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死
所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程等,这些任务是异步的
②异步过程
以定时器为例说明
setTimeout(function(){
console.log(666666)
},50);
当我们执行这段代码的时候,浏览器会单独开一个线程,进行执行。然后当50ms到了后,浏览器内核就会把定时器线程的回调函数放到js引擎线程里面的任务队列里进行执行。从而实现了在浏览器内核的控制下js引擎线程和其他线程的通信
以ajax请求为例说明
如果我们写了一个ajax请求,那么当我们执行这段代码的时候,浏览器会单独开一个线程,进行执行。当请求完成时,在浏览器内核的控制下,请求线程就会把请求完成的回调函数推入js引擎线程进行执行
③总结
整个程序就是通过这样的一个个事件驱动起来的,通过回调函数来传递沟通。所以说,js是一直是单线程的,浏览器才是实现异步的那个家伙
举例来验证
①定时0秒执行,并未立即执行
setTimeout(function(){
console.log(0);
},0);
console.log(1);
上面这段代码在浏览器中运行的结果是2,1
解释:当浏览器中的Js引擎线程遇到定时器时,会开辟一个线程来运行定时器,此时,任务依次执行,会执行下一条语句,先输出console.log(1),之后当开辟的线程中的定时器执行完毕以后,会返回一个定时器的回调函数在Js的引擎线程中,所以输出的结果是2,1
②定时器会不完全准时触发
var time = new Date();
setTimeout(function () {
var time1 = new Date();
console.log(time1 - time + "毫秒后执行了此代码")
}, 500)
while (new Date() - time < 1000);//在一秒内,无限执行此循环
上面的代码在1000毫秒后才执行,首先给time赋值当前的时间,之后Js引擎线程遇到定时器,给定时器开辟一个线程,定时器的时间为500毫秒,500毫秒后,定时器返回回调函数给Js引擎线程,但是此时Js引擎线程正在执行最后一句while条件判断,定时器的回调函数只能等待,所以在一秒后循环结束,执行定时器的回调函数
③ajax监听函数的位置可以随意切换
function myajax() {
// 通过onreadystatechange 方法即可请求监听状态
var xhr = new XMLHttpRequest();
xhr.open("get", "http://www.blogzl.com/zl_other_module/ajaxTest/getTest.php");
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) { //判断请求是否完成
if (xhr.status == 200) { //判断请求是否成功
var val = xhr.responseText; //获取请求返回的数据
console.info(JSON.parse(val));//编码处理,转为中文,显示在控制台上
}
}
}
}
上面的代码中,我调换了ajax监听函数的位置在send()请求发送之前,代码还是能够正常请求到数据,这是因为在执行监听函数时Js引擎线程会为监听函数开辟一个线程,所以能够正常请求
④DOM事件的监听
当我们给某个DOM元素绑定了点击事件后,点击DOM元素,它就会响应
这说明我们的点击行为时刻都是被监听着的,但是这却又没有阻塞其他代码的运行,这就说明了事件的监听行为并没有处于js引擎线程中,而是在浏览器内核开出的其他线程里面。所以我们可以知道,当我们点击dom元素后,能立即执行响应的相应的事件函数是因为在事件监听线程里面把回调函数主动的推入到了我们的js主线程中的任务队列中,从而被js引擎线程进行执行
注意事项:渲染线程和js主线程是互斥
在浏览器中,DOM页面渲染和js代码的执行,是单独的的线程,这里就要考虑到一个问题
界面渲染线程是单独开辟的线程,是不是DOM一变化,界面就立刻重新渲染?
如果DOM一变化,界面就立刻重新渲染,效率必然很低,所以浏览器的机制规定界面渲染线程和js主线程是互斥的,主线程执行任务时,浏览器渲染线程处于挂起状态。【即页面被冻结】
演示示例:
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=GBK" />
<title> 计算质数 </title>
</head>
<body>
质数(prime number)又称素数,有无限个。质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数。
<p>已经发现的所有质数:<div id="result"></div>
</p>
<script type="text/javascript">
var n = 1;
search:
while (n < 99999) {
// 开始搜寻下一个质数
n += 1;
for (var i = 2; i <= Math.sqrt(n); i++) {
// 如果除以n的余数为0,开始判断下一个数字。
if (n % i == 0) {
continue search;
}
}
document.getElementById('result').innerHTML += (n + ", ");
}
</script>
</body>
</html>
上图写错了,是浏览器的渲染线程被挂起,页面没有内容显示
HTML5-web Worker的使用
web Worker是H5中的新技术,在前面我们已经知道了,如果我们写了定时器或者ajax请求等耗时操作时,浏览器内核会主动的开一个线程来执行他们,以保证不会阻塞当前js引擎线程的执行
那么现在就有一个问题了,如果我们没有使用定时器或者ajax,但是我们的操作仍然耗时,比如上面的就十万以内的质数的操作
这时我们能不能主动的让浏览器给我开一个线程出来执行,而不是被动的等待其执行完成,从而解决页面阻塞的问题?
①使用0秒定时器实现多线程异步(不推荐)
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=GBK" />
<title> 计算质数 </title>
</head>
<body>
质数(prime number)又称素数,有无限个。质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数。
<p>已经发现的所有质数:<div id="result"></div>
</p>
<script type="text/javascript">
setTimeout(function () {
var n = 1;
search:
while (n < 99999) {
// 开始搜寻下一个质数
n += 1;
for (var i = 2; i <= Math.sqrt(n); i++) {
// 如果除以n的余数为0,开始判断下一个数字。
if (n % i == 0) {
continue search;
}
}
document.getElementById('result').innerHTML += (n + ", ");
}
}, 0)
</script>
</body>
</html>
这里因为将耗时的操作代码放到一个定时器中,为其开启了一个线程,所以当它的回调函数还没有回到Js引擎线程中的时候,引擎线程中的代码已经执行完毕,会先渲染页面,输出内容,之后一段时间后,定时器的回调函数回到引擎线程中,页面再次渲染
你们可以直接粘贴代码体验一下
②使用H5的webworker实现多线程异步(推荐)
这里我们也可以使用h5的新技术,webworker来实现异步操作
流程如下:
1. 将耗时任务放到一个单独的js文件中
这里将我们的求质数的耗时代码放入task.js中
var n = 1;
search:
while (n < 99999) {
// 开始搜寻下一个质数
n += 1;
for (var i = 2; i <= Math.sqrt(n); i++) {
// 如果除以n的余数为0,开始判断下一个数字。
if (n % i == 0) {
continue search;
}
}
// 当前的n就是质数
postMessage(n); //推送消息,也就是将结果推到主线程中
}
webwork线程向js引擎线程传计算出的质数值
2. 在js引擎线程中开启一个webworker线程
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>开启线程</title>
<script>
var worker = new Worker('task.js'); //开启一个线程
</script>
</head>
<body>
<p>已经发现的所有质数:</p>
<div id="result" style="width: 1500px; height:auto; border:1px solid red; word-wrap: break-word; "></div>
<script>
worker.onmessage = function (event) {
document.querySelector("#result").innerHTML += event.data + ",";
}
</script>
</body>
</html>
这里需要注意,传来的event是一个对象,而不是数据
在js引擎线程和worker线程间互相通信的语句
//推送消息
postMessage(n);
//接收消息
worker.onmessage = function (event) {
document.getElementById("result").innerHTML += event.data + ",";
}
示例:JS引擎线程主动向webwork线程传递数据
index.html页面代码
<!DOCTYPE html>
<html>
<head>
<title></title>
<script>
var worker = new Worker('task.js');
// 向Worker线程提交数据。
worker.postMessage("我是来自主线程的消息");
// 接收Worker线程数据。
worker.onmessage = function (event) {
alert(event.data)
}
</script>
</head>
<body>
test
</body>
</html>
task.js页面代码
onmessage = function (event) {
// 将数据提取出来。
var data = event.data
// 当前的n就是质数
postMessage(data + '66666')
}
多个线程嵌套
可以在worker线程里面再开启一个线程,如下
// 再次启动Worker线程
var sub = new Worker('subworker.js')
// 把需要处理的数据传入启动的Worker线程中
sub.postMessage("hello,i am task.js");
sub.onmessage = function (event) {
console.log(event.data,66666)
}
然后进行通信
onmessage = function (event) {
// 将数据提取出来。
var data = event.data
// 当前的n就是质数
postMessage(data + '66666')
}
终止worker
有些情况下,我们可能需要强制终止执行中的Web Worker。Worker对象提供了terminate()来终止自身执行任务,被终止的Worker对象不能被重启或重用,我们只能新建另一个Worker实例来执行新的任务
方法:worker.terminate()
webworker用法总结
前台页面
• 通过 new Worker( js) 加载一个JS文件来创建一个worker并返回一个worker实例
• 通过worker.postMessage( data ) 方法来向worker发送数据
• 通过worker.onmessage方法来接收worker发送过来的数据
• worker.terminate() 可以终止worker
后台JS
• 通过postMessage( data ) 方法来向主线程发送数据。
• 绑定onmessage方法来接收主线程发送过来的数据。
webworker注意事项
通过webworker,我们可以在前端做一些小规模分布式计算之类的工作,当然对过Web Worker有以下一些使用限制:
1.Web Worker无法访问DOM节点;
2.Web Worker无法访问全局变量或是全局函数;
3.Web Worker无法调用alert()或者confirm之类的函数;
4.Web Worker无法访问window、document之类的浏览器全局变量;
不过Web Worker中的Javascript依然可以使用setTimeout(),setInterval()之类的函数,也可以使用XMLHttpRequest对象来做Ajax通信。也可以使用console.log()来打印日志
参考
① https://www.cnblogs.com/woodyblog/p/6061671.html js的单线程和异步
② https://www.cnblogs.com/tianheila/p/6420587.html js异步实现机制
③ https://www.cnblogs.com/mopagunda/p/5442763.html web worker实现多线程
④ https://www.cnblogs.com/panmy/p/5764507.html js worker 多线程
⑤ https://www.jianshu.com/p/80f19480fce4 后台运行js–worker线程