js 事件循环机制:

js事件循环机制:示例
- MutationObserver
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="ES5" content="ES5规范引入了异步的微任务microtask,本轮同步任务结束后,通过EventLoop事件循环机制,得以在本轮的所有同步方法执行完后得以调度执行,而且是执行到本轮中的所有微任务都执行完毕后,才会进入下一轮的EventLoop">
</head>
<body>
<div id='demo'>
<ul>
<li>111111</li>
</ul>
</div>
</body>
<script type="text/javascript">
console.log(!!MutationObserver); // 监测浏览器是否支持
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var list = document.querySelector('ul');
var Observer = new MutationObserver(function(mutations, instance){
console.log("begin::::我是异步回调函数,是个microtask类型的微任务,在本轮的同步代码执行完毕之后,才由EventLoop事件循环机制开始被调度执行的!");
console.log(mutations);
console.log(instance);
mutations.forEach(function(mutation){
console.log(mutation);
});
console.log("end::::我是异步回调函数,是个microtask类型的微任务,所有的微任务都已在本轮中执行完毕后,才由EventLoop进入下一轮的macrotask宏任务调度执行");
});
Observer.observe(list, {
childList: true,
subtree: true
});
console.log("====begin::我是同步代码,在本轮中,我得到执行,而且执行完本轮中的所有同步代码:::,首次的本轮是html网页从上至下的直至</html>处的所有js代码");
list.appendChild(document.createElement('div')); //第一次DOM变动 产生MutationRecord对象,压入异步回调函数的入参mutations数组
list.appendChild(document.createTextNode('foo'));//第二次DOM变动 产生MutationRecord对象,压入异步回调函数的入参mutations数组
list.removeChild(list.childNodes[0]); //第三次DOM变动 产生MutationRecord对象,压入异步回调函数的入参mutations数组
list.childNodes[0].appendChild(document.createElement('div')); //第四次DOM变动 产生MutationRecord对象,压入异步回调函数的入参mutations数组
console.log("====end::我是同步代码,在本轮中,我得到执行,而且执行完本轮中的所有同步代码:::");
</script>
</html>
- XHR ajax
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="XHR" content="ajax异步请求响应行为"/>
<!--
浏览器环境:
A:可不止一个线程,其是有多个线程并发执行的。但有一点,dom解析渲染引擎和js引擎是互斥的。
A.1:可开启多个异步下载图片/js文件/视频等的异步线程
A.2:一个dom解析线程
A.3:一个监控dom变化和dom事件/bom事件/手工js事件/等事件的事件监控线程:事件一旦发生,监控线程立马往相应的macrotask任务队列中投放一个任务源(这个任务源包括事件本身[事件的时间/人物/地点],事件的处理器[事件回调函数]),待jsEventLoop来执行
A.4:一个js主线程,从上至下的解析执行js代码,并且伴随着EventLoop事件循环机制,以期达到javascript能”并发执行“。再次强调下,js是单线程的,永远没有并发执行,引入了EventLoop机制,是为了模仿多线程的并发执行而已。
B:html页面加载的时候,是从上至下的顺序解析执行的:
B.1:当碰到<html><head><meta>标签时,dom解析渲染引擎进行词法分析,形成domParseTree
B.2:当碰到<link rel='stylesheet' href='x.css'>标签时,浏览器进行css文件的下载并解析成cssRuleTree,此时是独占的,浏览器阻塞后续事宜
B.3:当碰到<script>标签时,浏览器进行js文件的下载,这里有三种下载方案:
B.3.1:<script src="x.js">时,浏览器下载该js文件,阻塞后续事宜,下载完毕后,立马调用js引擎进行执行该js代码
B.3.2:<script src="x.js" async>时,浏览器开启新的线程异步下载该js文件,下载该文件的同时浏览器以主线程继续往下执行解析dom,当下载js完毕后,立马调用js引擎,停用dom引擎,js引擎执行完js文件内的代码后,浏览器再次调度dom引擎继续解析执行
B.3.3:<script src="x.js" defer>时,浏览器开启新的线程异步下载该js文件,下载该文件的同时浏览器以主线程继续往下执行解析dom,直至该dom解析到</html>元素后,如果此时该js文件已经下载完毕,则调度js引擎执行js代码;如果此时异步线程还没下载完该js文件,则浏览器等待直到异步线程下载完该js文件,通过线程间的通讯告知浏览器已下载完毕标识后,浏览器拿到该标识后,才会调度js引擎进行该js文件内的js代码执行。
C:dom继续解析,解析到<body><div><p>等标签时,继续生成domParseTree。这时为了提高页面响应速度,现代浏览器都会进行边解析边渲染的Frame创建过程,也就是domParseTree + cssRuleTree = domRenderTree(这就是最终页面渲染出的样子),解析出来一段,就及时的渲染到页面一块区域。dom继续解析,如果发现后续节点的变化引起了页面布局的变化(比如:后续节点的样式有改动位置,大小,浮动等原因导致的文档布局的变动),那么此时浏览器进行reflow回流,重新从头<body>处开始来一遍渲染;如果后续节点的样式仅仅是颜色,背景等这些不会影响到布局的变化,那么浏览器只会进行repaint重绘,这个代价要比reflow小的多。所以程序的好坏,到底会不会引起reflow和repaint,是值得深究的。
D:dom解析过程中,如果碰到了<img>元素,那么会启动新线程来进行图片的异步下载,而主线程会继续往下执行dom解析。当图片下载完成后,图片下载的异步线程通过线程间的通讯告知浏览器,已下载完毕,浏览器进而调度dom进行渲染。结合上述C:的情况分析,所以为什么说,<img>元素提前给好width和height这两个属性,目的就是为了防止reflow,而只进行repaint的代码会小很多。
E:js引擎、jsEventLoop、macrotask、microtask、同步任务、异步任务
E.1:js代码:代码中声明的变量和函数会自动提前,提前到所有执行语句执行和函数调用之前的。
E.2:jsEventLoop:js是单线程的,那么如何达到并发执行的呢?靠的就是EventLoop事件循环机制。
E.2.1:同一事件循环:本轮循环中,所有的js同步代码除了:ajax的回调函数、setTimeout回调函数、之外的所有代码都会在本轮中得以执行。
事件回调函数的困惑:到底是本轮循环中就会得到实现,还是下次循环中才会得到实现呢?这个感觉有点模棱两可。
就如下面的示例一样,如果说,本轮循环中,所有的事件都是回调函数,不参与本轮处理只能参与下一轮处理的话,那就不应该提前打印出
onreadystatechange 事件回调函数的即时调用,打印xhr.readyState:1 在打印 console.log("这里的log并不是最先打印出来的"); 本轮同步方法之前
onloadstart 事件回调函数的即时调用,打印onloadStart 在打印 console.log("这里的log并不是最先打印出来的"); 本轮同步方法之前
那如果说,事件回调函数一定是在本轮循环处理过程之中就得到调用,那又说不过去
onreadystatechange 事件回调函数的即时调用,打印xhr.readyState:4 在打印 console.log("这里的log并不是最先打印出来的"); 本轮同步方法之后
所以说:事件回调函数的困惑就在这里。那么目前对于下方代码的解释如下:
-->
<script>
function getXHR(){
var xhr=null;
if(window.XMLHttpRequest){
xhr=new XMLHttpRequest();
} else if (window.ActiveXObject){
try{
xhr=new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try {
xhr=new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
alert("您的浏览器暂不支持Ajax");
}
}
}
return xhr;
}
var xhr=getXHR();
xhr.onreadystatechange=function(){
console.log("xhr.readyState:"+this.readyState);
}
xhr.onloadstart=function(){
console.log("onloadStart");
}
xhr.onload = function(){
console.log("onload");
}
//同步代码open被js主线程调用后,被浏览器监控线程并发的监控到readystate从0->1事件,则立刻往异步宏任务中添加事件源.
//js根据EventLoop机制,运行open代码结束后,栈为空,去寻求microtask队列为空,继续去UIrending不需要,本轮结束。
//js根据EventLoop机制,继续进入下一轮的循环,寻找macrotask队列中有任务,取出readystate从0->1事件源进行处理,处理完,打印出xhr.readyState:1,继续microtask为空,继续UIrending不需要,本轮结束。
//js根据EventLoop机制,继续进行下一轮循环,macrotask为空,microtask为空,UIrending不需要,好的,这里是关键,本轮循环结束后,啥也没干成,空跑了一圈轮询,那么js引擎就会暂停循环机制,试图往下寻找同步代码继续执行:
//如果后续还有js代码,就继续执行
//如果后续没有js代码了,就在这里无限空循环,等待将来有js同步代码的到来。
xhr.open("GET","",true);
//此句代码执行,继续往下
xhr.setRequestHeader("Cache-Control",3600);
//此句代码send被js主线程调用后,被浏览器监控线程并发的监控到onloadstart事件,则立刻往异步宏任务中添加事件源.
//js根据EventLoop机制,运行send代码结束后,栈为空,去寻求microtask队列为空,继续去UIrending不需要,本轮结束。
//js根据EventLoop机制,继续进入下一轮的循环,寻找macrotask队列中有任务,取出onloadstart事件源进行处理,处理完,打印出onloadstart,继续microtask为空,继续UIrending不需要,本轮结束。
//js根据EventLoop机制,继续进入下一轮循环,macrotask为空,microtask为空,UIrending不需要,好的,这里是关键,本轮循环结束后,啥也没干成,空跑了一圈轮询,那么js引擎就会暂停循环机制,试图往下寻找同步代码继续执行:
//如果后续还有js代码,就继续执行
//如果后续没有js代码了,就在这里无限空循环,等待将来有js同步代码的到来。
xhr.send(null);
var timer=setTimeout(function(){ //setTimeout(cbFn(),ms,i) 这里的cbFn铁定是进入到下一轮的事件循环当中,本代理执行执行后,继续往下执行
console.log("setTimeout");
},0);
//到此打印出来的信息,根据上面的分析是滞后
console.log("这里的log并不是最先打印出来的");
</script>
</head>
<body>
</body>
</html>
- Promise
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="ES5" content="ES5规范引入了异步的微任务microtask,本轮同步任务结束后,通过EventLoop事件循环机制,得以在本轮的所有同步方法执行完后得以调度执行,而且是执行到本轮中的所有微任务都执行完毕后,才会进入下一轮的EventLoop">
</head>
<body>
<!--javascript当中的并发执行和EventLoop机制主要是用来分部执行一个大的方法块或者方法链式调用问题的:-->
<!-- ES5新引入的Promise.then() 总是想在本轮内macortask执行完毕后得以调用。
但是有个前提,调用then()方法时的Promise必须此时是一个已经变成 resolve 状态的 Promise ,
那么这里就有说法了:
A:js根据EventLoop机制,本轮同步代码结束后,进而想调用Promise.then()方法,但是发现此时的Promise不是处于resolve状态,那么js并不会阻塞在这里,而是继续往下寻找UIrending是否需要,发现不需要,就本轮结束掉,进入下一轮的事件循环机制:
B.1: 下一轮的事件循环机制开始,当setTimeout(cbFn(){},0)延迟0ms时,知晓macrotask队列有任务,则取出该任务执行完毕,继续搜寻microtask,发现有microtask,此时的Promise已经在setTimeout(cbFn(){resolve()},0)得以处理,是resolve状态,所以microtask得以执行完毕,继续UIrending需要,渲染出“Promise 已填充完毕 (<small>异步代码结束”至页面。本轮结束,继续进行下一轮事件循环机制...
B.2: 下一轮的事件循环机制开始,当setTimeout(cbFn(){},Math.random() * 2000 + 1000)延迟1s~3s时,知晓macrotask队列有任务,但延迟时间还没到,所以不执行cbFn(){},注意 此时js引擎并不会阻塞在这里等待1s~3s的延迟到位,(这一点怎么验证呢,可以通过下述示例的快速点击按钮事件来观看结果)而是继续搜寻microtask队列,又开启了A:这么一个现象,A现象轮完,又进入下一轮的事件循环机制:如果页面有快速点击了,就会得到鼠标点击事件响应,又是一轮循环;如果等待1s~3s的延迟期间,没有鼠标点击,那么由于不停的循环,总会执行到1s~3s后的setTimeout(cbFn(){},Math.random() * 2000 + 1000)的cbFn得以执行,进而Promise处于resolve状态,进而microtask的队列也都能得以执行
************************************************
所以说:javascript(的EventLoop)是非阻塞式的。。。
************************************************
-->
<!-- ES5新引入的MutationObserver(cbFn(mutations,_self){}) :引入MutationObserver的目的是,本轮事件循环中,收集所有的dom变化MutationRecord
,压缩进入MutationObserver(cbFn(mutations,_self){})构造器里的回调函数入参mutations里面,然后本轮同步代码都执行完毕后,异步回调cbFn微任务,用来处理页面内所有的dom变化 mutations[MutationRecord1,MutationRecord2,MutationRecord3,...],Vue就是利用的这点技术,只不过Vue在收集mutations数组的时候,又前进了一步,排重了重复的数据,只考虑了本轮变化的数据的终态,此变化数据的中间过程都被终态覆盖-->
<!-- 以及传统的setTimeout(cbFn(){},ms,i) 这个铁定是下一轮事件循环机制才能得到调度的机会,至于能否调度到它,还要看它的延迟时间到了没,还要看它和其它宏任务队列去竞争js引擎时,谁进入队列的时间最老 -->
<input type='button' value='btnPromise' onclick='testPromise()'/>
<div id='log'></div>
<script>
'use strict';
var promiseCount = 0;
function testPromise() {
let thisPromiseCount = ++promiseCount;
let log = document.getElementById('log');
log.insertAdjacentHTML('beforeend', thisPromiseCount + ') 开始 (<small>同步代码开始</small>)<br/>');
// 新构建一个 Promise 实例:使用Promise实现每过一段时间给计数器加一的过程,每段时间间隔为1~3秒不等
let p1 = new Promise(
(resolve, reject) => {// resolver 函数在 Promise 成功或失败时都可能被调用
log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Promise 开始 (<small>异步代码开始</small>)<br/>');
setTimeout(// 创建一个异步调用,延时0ms,也就是下一轮事件循环时,已准备好被调度
function() {
resolve(thisPromiseCount); // 填充 Promise
}, 0); //Math.random() * 2000 + 1000
/*这里的执行,和上方延迟0ms的执行,页面测试时,快速点击几下按钮,观察结果,谈谈观后感。
setTimeout(// 创建一个异步调用,延时1~3s,如果从上一轮结束到本轮事件循环开始,已经过了1~3s,那么此异步任务才具备准备好被调度的资格
function() {
resolve(thisPromiseCount); // 填充 Promise
}, Math.random() * 2000 + 1000); //1~3s的延迟
*/
}
);
// Promise 不论成功或失败都会调用 then
// catch() 只有当 promise 失败时才会调用
p1.then(
function(val) {// 记录填充值
log.insertAdjacentHTML('beforeend', val + ') Promise 已填充完毕 (<small>异步代码结束</small>)<br/>');
})
.catch( (reason) => { console.log('处理失败的 promise ('+reason+')'); } ); // 记录失败原因
log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Promise made (<small>同步代码结束</small>)<br/>');
}
</script>
</body>
<script type="text/javascript">
console.log(!!Promise); // 监测浏览器是否支持
console.log("begin:::我是同步代码");
Promise.resolve(123)
.then(function(result){
console.log("1:::"+result);
return 'abc';
})
.then(function(result){
console.log("2:::"+result+"----这里我不在return value,所以下方的then回调函数的入参result为undefined");
console.log("2:::因为本轮的return值是作为本轮最终新对象Promise的resolve结果");
console.log("2:::每一个then回调调用函数的入参都是上一个Promise的resolve值作为实参传入的");
})
.then(function(result){
console.log("3:::"+result);
})
.catch(function(result){
console.log('上一步的then回调没发生异常时,我不会被执行!');
});
console.log("end:::我是同步代码");
</script>
</html>
- setTimeout
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="setTimeout" content="setTimeout是macrotask宏任务,下一轮EvnetLoop开始执行,一定是在上一轮事件循环执行完毕后才执行!">
</head>
<body>
<script>
const s = new Date().getSeconds();
setTimeout(function() {
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500); // 这里设置的是500,表示是至少延时500ms的意思,如果上一轮事件循环没有执行完毕或者超过了500ms的执行周期的话,那么这里的500ms就是个废。本例子这里输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
console.log('这是开始');
setTimeout(function cb() {
console.log('这是来自第一个回调的消息');
});
console.log('这是一条消息');
setTimeout(function cb1() {
console.log('这是来自第二个回调的消息');
}, 0);
console.log('这是结束');
</script>
</body>
</html>
- CustomEvent
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="CustomEvent" content="js代码自定义一个事件、为该事件绑定监听器、某种情况下人为触发该事件">
</head>
<body>
<input id="input" type='button' value='eventBtn' onclick="test();"/>
</body>
</html>
<script type="text/javascript">
var objj = document.getElementById('input');
objj.addEventListener('myEvent',function(event){ //事件绑定到objj元素上,事件绑定的监听器来处理该事件
alert(event.detail.title)
},true);
function test(){
var myEvent = new CustomEvent('myEvent', { //js代码自定义一个事件
detail: { title: 'This is title!'},
});
objj.dispatchEvent(myEvent); //js代码在某种情况下人为触发该事件,所有的dom元素继承关系 -->Element -->Node -->EventTarget -->Object
//所以所有的dom元素都具有addEventListener removeEventListener dispatchEvent 等继承来的事件相关方法
//触发事件的objj称之为事件源 非IE浏览下是EventTarget ie8版本及其以下是srcElement
}
</script>
本文详细探讨了JavaScript的事件循环机制,包括MutationObserver、XHR/AJAX、Promise的处理以及setTimeout的执行原理,同时也提到了CustomEvent的应用,全面解析JS异步执行的本质。

被折叠的 条评论
为什么被折叠?



