• 学习笔记—EventLoop事件循环


    日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。

    EventLoop

    EventLoop即事件循环,是指浏览器或 Node 的一种解决 JavaScript 单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

    JavaScript是单线程语言。在当前页面进程中,JS是主线程,而我们常说的异步操作,属于异步线程

    在这里我们就要先清楚两个名词,进程线程

    • 进程:是计算机分配任务的最小单位。计算机分配任务时,会按照 进程 进行任务分配。
    • 线程:一个进程中包含多个线程。

    对于前端来说,我们接触最多的浏览器,就是由多个进程组成的。

    浏览器进程

    关于浏览器的进程,包含以下几点

    • 每个标签页是一个进程,他们互不影响。假设我们当前某个页面出现问题卡住了(如死循环等),其他页面不会被当前页面影响。

    • 每个浏览器有一个主进程,也就是我们的用户界面。

    • 渲染进程,每个标签页里,都有一个渲染进程,而 渲染进程中又包含多个进程

    • 网络进程,发送网络请求以及处理网络请求等。

    • GPU进程3D绘制进程,用来提高页面的加载速度,以及加载复杂的页面特效。

    • 第三方插件 等等...

    渲染进程

    渲染进程中包括多个线程

    • GUI渲染线程 ,用来对页面进行渲染,也可以叫做 UI渲染线程
    • JS引擎线程 ,用来加载页面中的JS语法,他和 GUI渲染线程 互斥,也就是说他们两个只能同时加载一个线程
    • 事件循环线程 ,也就是 EventLoop。他也是一个独立的线程。JS执行时可能会调用异步方法,而这些方法就是 EventLoop 调度执行的。
    • clicksetTimeoutajax 等也是一个独立线程。

    搞懂 EventLoop ,需要先明白 堆栈队列 等概念,后续有时间的话我会更新一篇文章专门对此概念进行详述,感谢大家的关注。

    宏任务和微任务

    • 宏任务scriptajax请求ui 渲染setTimeoutsetIntervalpostMessageMessageChannelSetImmediateevent事件
    • 微任务promisemutationObserverprocess.nextTick

    image.png

    JS在执行时,会 自上向下执行,也就是一个 执行栈,其中包括 **同步代码 **和 异步代码

    遇到同步代码时,如果是函数会创建一个 执行上下文,并放入执行栈中,执行完毕后再出栈

    遇到异步代码时,我们需要将异步任务放入到一个 事件队列(或消息队列)[ 队列的特点是先进先出 ] 中(内部会调用浏览器提供的Api来将任务放入队列)。

    当我们执行上下文 全部执行完毕 后。事件队列 会不停的 对队列进行扫描,并将队列里 符合规则(执行完毕或到达时间) 的任务放到执行上下文中并执行。而进行扫描的队列就是 事件循环线程,他会对 执行栈 进行循环检测 ,当 执行栈为空 的情况下,才会将 符合规则的一个任务放到执行上下文中并执行(注:每次只放入一个)

    上述的这些异步任务,我们还可以称其为 宏任务 。其中包括一些特殊的异步任务,我们可以称为 微任务

    每一次循环检测的时候,都会创建一个 宏任务队列(可能会有异步任务,也可能会没有)。而每一个宏任务队列中,都包含一个微任务队列。这个微任务队列会在每一次循环检测时,将结果进行清空。

    换言之就是,每次循环检测会创建一个宏任务队列,这个队列中包含一个微任务队列。宏任务队列清空并输出结果之前,会先对微任务队列进行任务扫描,并清空。然后才会扫描符合规则的宏任务,并将任务放到执行栈中。

    如果存在UI渲染等情况,则会在每次循环的微任务队列清空完毕后,对页面进行渲染

    代码题

    通过以下几道代码题可以更好的帮我们理解上述概念。

    1. 微任务和UI渲染

      document.body.style.background = 'red';
      console.log(1)
      Promise.resolve().then(()=>{
      	console.log(2)
      	document.body.style.background = 'yellow';
      })
      console.log(3);
      

      结果是 1、3、2、yellow,且屏幕不会出现闪烁的情况。

      因为页面的ui渲染会在每次微任务队列清空之后执行,所以不会出现闪烁的情况。

      要验证此结论,可以通过添加一个宏任务事件来进行测试。

      document.body.style.background = 'red';
      console.log(1)
      Promise.resolve().then(() => {
      	console.log(2)
      	document.body.style.background = 'yellow';
      })
      setTimeout(() => {
      	document.body.style.background = 'green';
      }, 0);
      console.log(3);
      

      在多次刷新页面后,可以看到有一个小小的闪烁的情况。

      通过这个测试,我们也可以得到一个更严谨的言论。UI渲染(GUI渲染)会在达到渲染时机时,才会出现闪烁的情况。

    2. 事件任务

      button.addEventListener('click',()=>{
      	console.log('listener1');
      	Promise.resolve().then(()=>console.log('micro task1'))
      })
      button.addEventListener('click',()=>{
      	console.log('listener2');
      	Promise.resolve().then(()=>console.log('micro task2'))
      })
      button.click(); // click1() click2()
      

      结果是 listener1、listener2、micro task1、micro task2

      首先我们先明确一个概念,那就是 button.click() 相当于将两个事件当成普通函数来执行,也就等价于以下代码

      let fn1 = () => {
      	console.log('listener1');
      	Promise.resolve().then(() => console.log('micro task1'))
      }
      let fn2 = () => {
      	console.log('listener2');
      	Promise.resolve().then(() => console.log('micro task2'))
      }
      fn1(); fn2()
      

      所以就可以理解成,先执行 执行上下文 中的函数,再执行事件循环队列中的异步事件。

      但是如果我们不使用button.click(),而是直接使用点击事件呢?

      <button id="button">按钮</button>
      <script>
      button.addEventListener('click', () => {
      	console.log('listener1');
      	Promise.resolve().then(() => console.log('micro task1'))
      })
      button.addEventListener('click', () => {
      	console.log('listener2');
      	Promise.resolve().then(() => console.log('micro task2'))
      })
      </script>
      

      结果是 listener1、micro task1、listener2、micro task2

      上述代码中的点击事件属于宏任务,所以我们相当于向宏任务队列中添加了两个宏任务,根据宏任务队列的执行原则,会先清空微任务队列。所以会输出上面的结果。

    3. 定时器任务

      Promise.resolve().then(() => {
      	console.log('Promise1')
      	setTimeout(() => {
      		console.log('setTimeout2')
      	}, 0);
      })
      setTimeout(() => {
      	console.log('setTimeout1');
      	Promise.resolve().then(() => {
      		console.log('Promise2')
      	})
      }, 0);
      

      结果是 Promise1、setTimeout1、Promise2、setTimeout2

      上述代码在执行时,会先执行最外层的 script脚本(宏任务),会先将外层的微任务Promise1放入微任务队列宏任务setTimeout1放入宏任务队列。在 执行script宏任务 时,会 清空微任务队列,也就是会先将 Promise1输出 。然后将里面的 宏任务setTimeout2再次放入宏任务队列。这时根据宏任务队列顺序,需要 执行宏任务setTimeout1,将 setTimeout1输出,然后再将微任务Promise2放入微任务队列。这时 执行宏任务setTimeout2,先 清空微任务队列,输出Promise2,随后再将 setTimeout2输出

    4. 常见面试题

      console.log(1);
      async function async () {
          console.log(2);
          await console.log(3);
          console.log(4)
      }
      setTimeout(() => {
      	console.log(5);
      }, 0);
      const promise = new Promise((resolve, reject) => {
          console.log(6);
          resolve(7)
      })
      promise.then(res => {
      	console.log(res)
      })
      async (); 
      console.log(8);
      

      结果是 1、6、2、3、8、7、4、5

      思路依旧是根据事件循环线程的输出思路,将宏任务放入宏任务队列,微任务放入微任务队列。宏任务队列执行时,会先清空并输出现有微任务队列中的结果,然后再将宏任务中的结果输出。所以最终结果就是上述结果。


    本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
    您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
    最后感谢您的支持!

  • 相关阅读:
    DBCA创建数据库ORA-01034 ORACLE not available
    Linux shell 内部变量
    ext4文件系统制作
    curses-键盘编码-openssl加解密【转】
    Linux 中的键盘映射【转】
    C 语言 字符串命令 strstr()的用法 实现将原字符串以分割串分割输出【转】
    Linux下使用popen()执行shell命令【转】
    linux下获取按键响应事件【转】
    linux select函数:Linux下select函数的使用详解【转】
    OTA升级中关于update.zip包的一些总结【转】
  • 原文地址:https://www.cnblogs.com/moxiaoshang/p/15495580.html
Copyright © 2020-2023  润新知