Javascript事件循环机制
通过学习JavaScript,我们都知道它是一门单线程语言,也就是说,在同一时刻,最多也只有一个代码段在执行,但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。也就是我们所说的事件循环机制。
事件循环机制.png
从上图我们可以看出,js主线程它是有一个执行栈的,所有的js代码都会在执行栈里运行。在执行代码过程中,如果遇到一些异步代码(比如setTimeout,ajax,promise.then以及用户点击等操作),那么浏览器就会将这些代码放到一个幕后线程中去等待,不阻塞主线程的执行,主线程继续执行栈中剩余的代码,当幕后线程(background thread)里的代码准备好了(比如setTimeout时间到了,ajax请求得到响应),该线程就会将它的回调函数放到任务队列中等待执行。而当主线程执行完栈中的所有代码后,它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。如果当前任务队列为空的话,它就会一直循环等待任务到来。因此,这叫做事件循环。
事件循环过程:
javascript在执行的时候,按顺序从上往下执行,当遇到异步代码的时候,浏览器会把这些异步代码放到其他执行模块中去执行,不阻塞主线程的执行,当执行完毕后,将回调函数放在任务队列中(taskquene)中,当主线程执行完栈中的所有代码后,然后会检查任务队列中是否有任务要执行,如果有,就将该任务放到 执行栈中执行,如果为空,则一直循环等待任务的到来。
因为任务队列有两种形式:
macrotask 和 microtask 是异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver
1.检查Macrotask 队列是否为空,若不为空,则进行下一步,若为空,则跳到3
2.从Macrotask队列中取队首(在队列时间最长)的任务进去执行栈中执行(仅仅一个),执行完后进入下一步
3.检查Microtask队列是否为空,若不为空,则进入下一步,否则,跳到1(开始新的事件循环)
4.从Microtask队列中取队首(在队列时间最长)的任务进去事件队列执行,执行完后,跳到3;
现在我们已经知道了什么是事件循环机制,接下来讲几个我们需要注意的点。
(1)setTimeout的时间设置为0,会立即执行吗?
答案很显然,不会,无论我们设置多少的延迟时间,setTimeout总是会在 主线程的任务执行完z之后输出。
<script>
console.log(1);
setTimeout(function(){console.log(2);}, 0);
console.log(3);
</script>
最后的输出结果:
事件循环机制setTimeout.png
可能有些浏览器可能会有一个最小延迟时间,有的是 15ms,有的是 10ms,所以会造成一些错觉,所以我们再看一下下面这个例子:
<script>
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
//具体数字不定,这取决于你的硬件配置和浏览器
for(var i = 0; i < 9999; i ++){
console.log(i);
}
console.log("script end");
</script>
这个数字我试了下99999999,结果电脑就......
(2)Ajax请求是否异步
ajax请求内容的时候是异步的,当请求完成后,会触发请求完成的事件,然后把回调函数放入callback queue,等到主线程执行该回调函数时还是单线程的。
这里简单说一下Ajax异步请求的过程:
首先说明一下Ajax是局部刷新页面而不是整体加载页面,能够减轻服务器压力,提高用户体验。
如何使用Ajax?
第一步:创建xmlhttprequest对象,var xmlhttp =new XMLHttpRequest();XMLHttpRequest对象用来和服务器交换数据
var xhttp;
if (window.XMLHttpRequest) {
//现代主流浏览器
xhttp = new XMLHttpRequest();
} else {
// 针对浏览器,比如IE5或IE6
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
第二步:使用xmlhttprequest对象的open()和send()方法发送资源请求给服务器。
xmlhttp.open(method,url,async) method包括get 和post,url主要是文件或资源的路径,async参数为true(代表异步)或者false(代表同步)
xhttp.send();使用get方法发送请求到服务器。
xhttp.send(string);使用post方法发送请求到服务器。
post 发送请求什么时候能够使用呢?
(1)更新一个文件或者数据库的时候。
(2)发送大量数据到服务器,因为post请求没有字符限制。
(3)发送用户输入的加密数据。
get例子:
xhttp.open("GET", "ajax_info.txt", true);
xhttp.open("GET", "index.html", true);
xhttp.open("GET", "demo_get.asp?t=" + Math.random(), true);xhttp.send();
post例子
xhttp.open("POST", "demo_post.asp", true);
xhttp.send();
post表单数据需要使用xmlhttprequest对象的setRequestHeader方法增加一个HTTP头。
post表单例子
xhttp.open("POST", "ajax_test.aspx", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("fname=Henry&lname=Ford");
async参数
(1)async=true 当服务器准备响应时将执行onreadystatechange函数。
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
document.getElementById("demo").innerHTML = xhttp.responseText;
}
};
xhttp.open("GET", "index.aspx", true);
xhttp.send();
(2)async=false 则将不需要写onreadystatechange函数,直接在send后面写上执行代码。
xhttp.open("GET", "index.aspx", false);
xhttp.send();
document.getElementById("demo").innerHTML = xhttp.responseText;
第三步:使用xmlhttprequest对象的responseText或responseXML属性获得服务器的响应。
使用responseText属性得到服务器响应的字符串数据,使用responseXML属性得到服务器响应的XML数据。
例子如下:
document.getElementById("demo").innerHTML = xhttp.responseText;
服务器响应的XML数据需要使用XML对象进行转换。
例子:
xmlDoc = xhttp.responseXML;
txt = "";
x = xmlDoc.getElementsByTagName("ARTIST");
for (i = 0; i < x.length; i++) {
txt += x[i].childNodes[0].nodeValue + "<br>";
}
document.getElementById("demo").innerHTML = txt;
第四步:onreadystatechange函数,当发送请求到服务器,我们想要服务器响应执行一些功能就需要使用onreadystatechange函数,每次xmlhttprequest对象的readyState发生改变都会触发onreadystatechange函数。
onreadystatechange属性存储一个当readyState发生改变时自动被调用的函数。
readyState属性,XMLHttpRequest对象的状态,改变从0到4,0代表请求未被初始化,1代表服务器连接成功,2请求被服务器接收,3处理请求,4请求完成并且响应准备。
status属性,200表示成功响应,404表示页面不存在。
ajax第四步图解.png在onreadystatechange事件中,服务器响应准备的时候发生,当readyState==4和status==200的时候服务器响应准备。
例子:
function loadDoc() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
document.getElementById("demo").innerHTML = xhttp.responseText;
}
};
xhttp.open("GET", "ajax_info.txt", true);
xhttp.send();
}
//函数作为参数调用
<!DOCTYPE html>
<html>
<body>
<p id="demo">Let AJAX change this text.</p>
<button type="button"
onclick="loadDoc('index.aspx', myFunction)">Change Content
</button>
<script>
function loadDoc(url, cfunc) {
var xhttp;
xhttp=new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
cfunc(xhttp);
}
};
xhttp.open("GET", url, true);
xhttp.send();
}
function myFunction(xhttp) {
document.getElementById("demo").innerHTML = xhttp.responseText;
}
</script>
</body>
</html>
(3)界面渲染线程是单独开辟的线程,是不是DOM一变化,界面就立刻重新渲染?
如果DOM一变化,界面就立刻重新渲染,效率必然很低,所以浏览器的机制规定界面渲染线程和主线程是互斥的,主线程执行任务时,浏览器渲染线程处于挂起状态。
好,再写一个例子总结说明一下事件循环机制
test:
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
运行结果.png
理解:
第一次事件循环:
console.log(1)被执行,输出1
settimeout1执行,加入macrotask队列
setinterval1执行,加入macrotask队列
settimeout2执行,加入macrotask队列
promise2执行,它的两个then函数加入microtask队列
console.log(9)执行,输出9
关于Promise,Promise的回调函数不是传入的,而是使用then来调用的。因此,Promise中定义的函数应该是马上执行的,then才是其回调函数,放入microtask中。
根据事件循环的定义,接下来会执行新增的microtask任务,按照进入队列的顺序,执行console.log(7)和console.log(8),输出7和8;
当然这里肯定有一个疑问,为什么不执行macrotask队列中的任务?这是因为一开始js主线程中跑的任务就是macrotask任务,而根据事件循环的流程,一次事件循环只会执行一个macrotask任务,因此,执行完主线程的代码后,它就去从microtask队列里取队首任务来执行。
microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: settimeout1,setinterval1,settimeout2
第二次事件循环:
从macrotask队列里取位于队首的任务(settimeout1)并执行,输出2
microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: setinterval1,settimeout2
第三次事件循环:
从macrotask队列里取位于队首的任务(setinterval1)并执行,输出3,然后又将新生成的setinterval1加入macrotask队列
microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: settimeout2,setinterval1
第四次事件循环:
从macrotask队列里取位于队首的任务(settimeout2)并执行,输出10,并且执行new Promise内的函数(new Promise内的函数是同步操作,并不是异步操作),输出11,并且将它的两个then函数加入microtask队列
从microtask队列中,取队首的任务执行,直到为空为止。因此,两个新增的microtask任务按顺序执行,输出12和13,并且将setinterval1清空
此时,microtask队列和macrotask队列都为空,浏览器会一直检查队列是否为空,等待新的任务加入队列。
当然,