JavaScript在浏览器环境中的异步
参考:
https://blog.csdn.net/qq_26222859/article/details/77622222
https://www.cnblogs.com/aaron—blog/p/10903118.html
单线程与多线程
JavaScript 默认情况下是单线程运行的(除了用H5 Web Workers),但运行js脚本的环境可以是多线程的
众所周知JavaScript是单线程运行的脚本语言,即: 逐行执行语句,上面的语句没有执行完会阻塞下面语句的执行,但在不同的环境下,却可以实现异步的操作(如浏览器环境下,node环境下)。
浏览器的多线程
在JavaScript引擎中负责解析和执行JavaScript代码的线程只有一个。但是除了这个主进程以外,还有其他很多辅助线程。那么诸如onclick
回调,setTimeout
,Ajax
这些都是怎么实现的呢?即浏览器搞了几个其他线程去辅助JavaScript线程的运行。
浏览器有很多线程,例如:
- GUI渲染线程 - 用于更新页面
- JavaScript引擎线程 - 用于解析JavaScript代码
- 定时器触发线程 - 浏览器定时计数器并不是 js引擎计数
- 浏览器事件线程 - 用于解析BOM渲染等工作
- http线程 - 主要负责数据请求
- EventLoop轮询处理线程 - 事件被触发时该线程会把事件添加到待处理队列的队尾
- 等等等
浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。
浏览器的异步逻辑
下面这张图可以说明浏览器的异步逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/9c859c9d28002c60796231fcda4b9203.png)
- 首先,js引擎解析js代码(称为同步过程,即把整个页面的js代码从头到尾执行下来),当处理到与其他线程相关的代码,就会分发给其他线程(比如onclick, setTimeout,setInterval,ajax请求, 对DOM进行写操作等等…此时的分发过程还是同步过程)
- 其他线程处理完之后(其他线程处理请求的过程就是异步过程)会把需要js引擎执行的任务放入callback queue里(也叫任务队列、消息队列、事件队列),等js线程的同步任务执行完了再从callback queue里按顺序执行其他线程返回来的任务。
- 其他线程执行完毕后放进callback queue里的事件,却需要等待js引擎执行完毕当前stack里的任务,空闲下来,才会被执行。
总结起来,JS线程就像是领导,而其他线程相当于员工。
领导拿到一个项目,先把整个计划过一遍,把任务下达给其他线程(同步过程),这个出谋划策的过程,JS线程(领导)不会傻等这个员工执行完了再给下一个员工分配任务,而是跳过这个任务,继续分配下一个任务,不然哪个员工没干好岂不是坏了整个团队的进度;
同样地其他线程(员工)拿到任务就开始专注处理自己的事情,接下来领导使唤别人与我无关,毕竟要早点交差么…
员工的事情干完了,发邮件给领导,这个邮件就在领导的 callback queue里躺着,等领导忙完了手上的事情,打开邮箱,嚯,来反馈了,再按照FIFO(first in first out)的顺序一件件处理反馈。
js引擎与GUI引擎是互斥的
虽然不同线程之间是互相独立的,但当GUI引擎需要更新界面的时候,却会受到JS线程的阻塞。
了解过 H5 Web Workers
的朋友应该知道,在 H5 Web Workers
出现之后,JS也可以“开挂” 实现多线程运行,当然这不在本文的讨论范畴内。这里只提一点, H5 Web Workers
的一大用处就是让js引擎中的耗时大的计算不影响界面的响应。
例如,如果js引擎需要处理一段耗时很长的代码:递归计算斐波那契数列的第2000项…呵呵, 由于线程阻塞,此时页面是无响应的,比如:
在
<input>
标签里键入无响应,js实现的轮播图不再播放等
所以此时可以把递归计算斐波那契数列的函数交给Workers
分线程,当然这不是本篇的主题。据这个例子是想说明:
- js引擎与GUI引擎是互斥的
也就是说GUI引擎在渲染时会阻塞js引擎计算,反过来也一样。
原因很简单,不同线程的具体语句之间执行顺序是不一定的,如果在GUI渲染的时候,js改变了dom,那页面到底听谁的?这就会造成渲染不同步。
这里插句题外话,无论
H5 Web Workers
如何神通广大,都无法改变一个事实: 要实现对DOM的操作,还是只能在js主线程上进行,而不能分到workers分线程上。原因也是:不同线程的具体语句之间执行顺序是不一定的,要避免不同线程对DOM修改造成冲突。
接着上面领导和员工的比方,GUI页面就好比呈现给甲方的最终结果,DOM渲染引擎按照JS引擎(领导)的要求完成了渲染方案,最终要JS引擎拍板,反馈到GUI页面上。
借用一下别人的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<table border=1>
<tr><td><button id='do'>Do long calc - bad status!</button></td>
<td><div id='status'>Not Calculating yet.</div></td>
</tr>
<tr><td><button id='do_ok'>Do long calc - good status!</button></td>
<td><div id='status_ok'>Not Calculating yet.</div></td>
</tr>
</table>
<script>
function long_running(status_div) {
var result = 0;
for (var i = 0; i < 1000; i++) {
for (var j = 0; j < 700; j++) {
for (var k = 0; k < 300; k++) {
result = result + i + j + k;
}
}
}
document.querySelector(status_div).innerHTML = 'calclation done' ;
}
document.querySelector('#do').onclick = function () {
document.querySelector('#status').innerHTML = 'calculating....';
long_running('#status');
};
document.querySelector('#do_ok').onclick = function () {
document.querySelector('#status_ok').innerHTML = 'calculating....';
window.setTimeout(function (){ long_running('#status_ok') }, 0);
};
</script>
</body>
</html>
我们希望能看到计算的每一个过程,我们在程序开始,计算,结束时,都执行了一个dom操作,插入了代表当前状态的字符串,Not Calculating yet.和calculating…和calclation done.计算中是一个耗时的3重for循环. 在没有使用settimeout的时候,执行结果是由Not Calculating yet 直接跳到了calclation done.这显然不是我们希望的.而造成这样结果的原因正是js的事件循环单线程机制.dom操作是异步的,for循环计算是同步的.异步操作都会被延迟到同步计算之后执行.也就是代码的执行顺序变了.calculating…和calclation done的dom操作都被放到事件队列后面而且紧跟在一起,造成了丢帧.无法实时的反应.这个例子也告诉了我们,在需要实时反馈的操作,如渲染等,和其他相关同步的代码,要么一起同步,要么一起异步才能保证代码的执行顺序.在js中,就只能让同步代码也异步.即给for计算加上settimeout.
setTimeout(0): 手动异步
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
alert(1);
setTimeout("alert(2)", 0);
alert(3);
</script>
</body>
</html>
上面这个代码的输出顺序是1->3->2,原因就是1和3是JS引擎中的同步过程,优先执行,而2是异步过程,要等JS自己的同步过程执行完了,再被执行。