之前在理解Event Loop时,写下了自己的一个疑惑~
可以点这里,请大佬翻阅指点~
经过一番的了解,我先抛出自己Event Loop的理解,最后会解释自己提出的疑惑~
众所周知,
Javascript是一个单线程执行的编程脚本语言,而单线程意味同一时间只能做一件事;然后JS引擎解析JS代码,是一个从上到下的过程。(PS:包括整个HTML、css的解析),比如下面这段代码,
html
<script src="script1">
// 为方便示意,直接展示出外链代码
console.log('外-1')
</script>
<script src="script2">
// 为方便示意,直接展示出外链代码
console.log('外-2')
</script>
<script>
console.log("内-1")
</script>
<script>
console.log("内-2")
</script>
都是同步任务,那不用说,输出结果如下:
js
外-1
外-2
内-1
内-2
但世界并不会一直如此美好,比如在script2中,插入一行alert(1),如下:
html
<script src="script1">
// 为方便示意,直接展示出外链代码
console.log('外-1')
</script>
<script src="script2">
// 为方便示意,直接展示出外链代码
console.log('外-2')
alert(1)
</script>
<script>
console.log("内-1")
</script>
<script>
console.log("内-2")
</script>
则JS在执行到alert(1)这行,就会堵在那等你确认,后续代码才会被执行;
所以,为解决类似这种堵塞问题,JS加入了异步执行机制。所谓异步,按照朴灵大佬的说法:
一般而言,操作分为:发出调用和得到结果两步。发出调用,立即得到结果是为同步。发出调用,但无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。同步就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)。
而,解决这个异步执行的机制,就是Event Loop。
上一个通用的JS执行图,如下:
而以上这个图差不多是2014年中,国外大佬Philip Roberts演讲《Help, I'm stuck in an event-loop》,PPT中的。
图中大致意思是,JS从上到下执行,将同步任务加入到主线程执行栈执行;
遇到有异步任务,如setTimeout定时器、ajax远程请求、DOM操作等,会通知相关线程去执行,在得到结果后,会将关联的回调函数放进回调函数队列(Callback Queue),也可以换另一种说法,叫事件函数队列(Event Queue)。
而Event Loop主要机制,就是不停轮询,也可以说不停检测,主线程是否为空,如果为空,则将Callback Queue中的函数放入主线程执行栈中,去执行。
如下代码:
html
<script src="script1">
setTimeout(()=>{
console.log('setTimeout-外-1')
},0)
// 为方便示意,直接展示出外链代码
document.addEventListener('DOMContentLoaded',function () {
console.log('文档DOM树构建完成');
});
window.onload=function () {
console.log('浏览器窗口加载完成!')
}
console.log('外-1')
</script>
<script src="script2">
// 为方便示意,直接展示出外链代码
setTimeout(()=>{
console.log('setTimeout-外-2')
},0)
console.log('外-2')
</script>
<script>
console.log("内-1")
</script>
<script>
console.log("内-2")
</script>
理论输出顺序如下:(后文解释为什么要加个理论...)
js
外-1
外-2
内-1
内-2
文档DOM树构建完成
浏览器窗口加载完成!
setTimeout-外-1
setTimeout-外-2
目前看,执行的都很好,但是,Javascript 在 2013年3月提出的ECMAScript 6 草案(正式发布时间为2015年6月)中,提出了一个更优雅的异步解决方案,Promise。
按说,已有之前的Event Loop这种机制,执行这个Promise也一样,以事件形式添进主线程执行栈就好。
但在代码正常执行中,还是有点小蹊跷。如下代码:
html
<script src="script1">
setTimeout(()=>{
console.log('setTimeout-外-1')
},0)
// 为方便示意,直接展示出外链代码
document.addEventListener('DOMContentLoaded',function () {
console.log('文档DOM树构建完成');
});
window.onload=function () {
console.log('浏览器窗口加载完成!')
}
console.log('外-1')
</script>
<script src="script2">
// 为方便示意,直接展示出外链代码
new Promise((relove)=>{
console.log('promise-外-2')
relove()
}).then(()=>{
console.log('promise-then-外-2')
})
setTimeout(()=>{
console.log('setTimeout-外-2')
},0)
console.log('外-2')
</script>
<script>
console.log("内-1")
</script>
<script>
console.log("内-2")
</script>
执行结果如下:
js
外-1
promise-外-2
外-2
promise-then-外-2
内-1
内-2
文档DOM树构建完成
浏览器窗口加载完成!
setTimeout-外-1
setTimeout-外-2
由运行结果看,Promise并没有如之前那种预期,在最后输出内容。而是在setTimeout和DOMContentLoaded和行内代码之前,输出了内容。
这就不符合之前预期了,但这种情况在ECMAScript 6中翻遍了,也没有解释。
所以,根据Promise运行的一些结果,在技术社区中,创造出了一个词,为microtask,中文现在翻译为微任务。
其主要解释是说,除去主线程执行栈,还有一个微小的任务队列,当主线程执行栈执行完任务后,才会去执行微小任务队列中的任务,这样,Event Loop才判定主线程执行栈为空,接着再执行Event Queue中的任务。
可以根据执行结果发现,在浏览器中,执行这个微小队列的时机,就是在每一个script块中的同步任务都结束时。以下给出示例代码:
HTML
<script src="./script1.js">
// 为方便示意,直接展示出外链代码
console.log("内-111");
setTimeout(() => {
console.log("setTimeout-内-111");
}, 0);
</script>
<script src="./script2.js">
// 为方便示意,直接展示出外链代码
console.log("内-222");
setTimeout(() => {
console.log("setTimeout-内-222");
}, 0);
setTimeout(() => {
console.log("setTimeout-内-333");
}, 0);
new Promise((resolve) => {
console.log("内-Promise1");
resolve();
})
.then(function () {
console.log("内-promise2");
})
.then(function () {
console.log("内-promise3");
});
</script>
<script>
console.log('外-111')
setTimeout(() => {
console.log('外-setTimeout-111')
}, 0);
</script>
<script>
console.log('外-222')
setTimeout(() => {
console.log('外-setTimeout-222')
}, 0);
setTimeout(() => {
console.log('外-setTimeout-333')
}, 0);
new Promise(resolve => {
console.log('外-Promise1')
resolve()
})
.then(function () {
console.log('外-promise2')
})
.then(function () {
console.log('外-promise3')
})
</script>
以上代码在Firefox(121.0.1 (64 位)中执行结果如下:
js
内-111
内-222
内-Promise1
内-promise2
内-promise3
外-111
外-222
外-Promise1
外-promise2
外-promise3
setTimeout-内-111
setTimeout-内-222
setTimeout-内-333
外-setTimeout-111
外-setTimeout-222
外-setTimeout-333
符合以上的理论执行。
如果看过我前一篇写的,就知道,在谷歌浏览器中执行,为如下顺序:
js
内-111
setTimeout-内-111
内-222
内-Promise1
内-promise2
内-promise3
外-111
外-222
外-Promise1
外-promise2
外-promise3
setTimeout-内-222
setTimeout-内-333
外-setTimeout-111
外-setTimeout-222
外-setTimeout-333
为何出现这种情况?
这个就涉及到JS在浏览器中,script的写入方式有四种:
- URL中以Javascrip:协议方式执行(已经很少有人这么用,唯一记得很早很早之前,装扮QQ空间,有用到过这种);
- 在DOM标签中编写(随着框架的流行,也很少在项目中使用);
- 在script标签中编写;
- 在script以外链形式导入;
首先以上结果,都是本地运行的,而且反复执行都是上面结果。所以,同样文件被缓存,同样代码,同样系统,唯一区别就是不同浏览器。自己猜测的一个解释就是,谷歌浏览器开启下载外链JS文件的线程,时间上慢于火狐浏览器。
因为开启下载文件,也是一个Evnet,所以导致第一个script外链的JS代码,已经全部执行完,才再去读取解析后面代码。
JS解析本身就是从上到下解析,如果中间遇到外链代码,都是会停下来解析,而停止解析,自然导致执行栈为空。
之所以这么猜测,就是因为在谷歌浏览器中多次执行,放入多个外链script代码,总是在第一个出现这个情况。而如果将这些代码放在web服务容器中,使用强制刷新ctrl+F5,就会出现上述情况。而如果只用了缓存,则谷歌浏览器的执行先后是和火狐一致的。
而且,Javascript的引擎不太一样,对于Node来说是V8、对Chrome来说是V8、对Safari来说JavaScript Core,对Firefox来说是SpiderMonkey。
各个引擎之间,对于何时将事件结果加入队列时机,不一致。
如上面这段代码:
html
<script src="script1">
setTimeout(()=>{
console.log('setTimeout-外-1')
},0)
// 为方便示意,直接展示出外链代码
document.addEventListener('DOMContentLoaded',function () {
console.log('文档DOM树构建完成');
});
window.onload=function () {
console.log('浏览器窗口加载完成!')
}
console.log('外-1')
</script>
<script src="script2">
// 为方便示意,直接展示出外链代码
setTimeout(()=>{
console.log('setTimeout-外-2')
},0)
console.log('外-2')
</script>
<script>
console.log("内-1")
</script>
<script>
console.log("内-2")
</script>
上面说的理论执行结果,是在火狐浏览器中执行的:
js
外-1
外-2
内-1
内-2
文档DOM树构建完成
浏览器窗口加载完成!
setTimeout-外-1
setTimeout-外-2
而谷歌浏览器中执行顺序为:
js
外-1
setTimeout-外-1
外-2
内-1
内-2
setTimeout-外-2
文档DOM树构建完成
浏览器窗口加载完成!
想要完整的理解各大浏览器的执行特性,确实需要进一步多多学习!
这就是我理解的Event Loop,它其实就是一个解决异步的机制。而很多高赞写的Event Loop,个人觉的太模式化,好像整个Javascrip执行都是通过这个Event Loop来调配的。
另外,还有一个瞎猜,Promise属于ECMAScript 6的范畴,实现呢,应该在JS引擎中,是否因为这个,所以其优先级会比其它外部线程的任务要高?当然,这个只是猜测,没有什么论证~
谢谢看到这里,欢迎各位指点~