我理解的Event Loop,先忘掉微任务、宏任务概念!请指点~

之前在理解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的写入方式有四种:

  1. URL中以Javascrip:协议方式执行(已经很少有人这么用,唯一记得很早很早之前,装扮QQ空间,有用到过这种);
  2. 在DOM标签中编写(随着框架的流行,也很少在项目中使用);
  3. 在script标签中编写;
  4. 在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引擎中,是否因为这个,所以其优先级会比其它外部线程的任务要高?当然,这个只是猜测,没有什么论证~

谢谢看到这里,欢迎各位指点~

相关推荐
一颗花生米。3 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐013 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19953 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&4 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
一路向前的月光9 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   9 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web9 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr10 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常12 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ12 小时前
html+css+js实现step进度条效果
javascript·css·html