1. 浏览器中的事件循环

533 阅读4分钟

当我们书写的javascript代码,通过浏览器进行加载到渲染页面,再到页面交互,清楚我们代码中的执行顺序谁先谁后对我们来说尤为重要.
这部分知识就涉及到浏览器的事件循环

1. 进程和线程

  • 进程: 我们这样理解,计算机运行的程序 项目 应用 都可以看成一个进程,运行着我们所启用的应用。当应用关闭也意味着,进程在我们计算机中退出. 在明显一点,我们的任务管理器/活动监视器每一个条目就是一个进程。
  • 线程:进程中又包含了线程 一个进程中最少有一个或多个线程,线程是计算机调用的最小单位。
  • 我们在打开浏览器页卡都是一个进程,这些进程中包含了很多的线程:
    1. js引擎线程 解析和执行js代码 也称为主线程
    2. 事件触发线程 事件触发Dom事件 发布订阅事件 js回调等
    3. 定时器触发线程 专门处理定时器的 定时器触发 定时器回收
    4. 异步Http请求线程 处理资源中的http请求的
    5. GUI渲染 在资源请求到后 会进行页面资源的渲染, 发生css合并 Dom树的渲染 Dom的回流和重绘等
  • 这些线程之间的相互配合,渲染出我们浏览器的页面显示和交互效果

2. 异步和同步

  1. 同步: 代码自上而下执行,上面代码没有执行完毕,下面的代码不会被执行
  2. 异步: 异步是相对同步而言的。上面代码执行过程中不会 阻塞代码的执行。
  3. 像在浏览器的渲染过程中js主线程执行栈会执行同步代码和异步代码 同步代码按照顺序自伤而下开始执行, 遇到异步代码 当前的主线程会吊起另一个线程 定时器就开启定时器线程 http请求就开启http请求线程...。 定时器webAPI在触发等待事件会将当前的定时器函数存储在内存中的红黑树中,在定时器时间到达的时候取出来 压入当前的主线程执行栈中 开始执行定时器中的代码。
  4. 在当前的执行任务中 如果当前执行栈中同步代码发生了阻塞,那么后续的异步执行代码将不会被执行 除非等待当前的同步任务执行完毕 才会按照原异步任务加入队列的顺序开始进行下一个异步任务,直到异步任务执行完毕后,事件循环执行完毕。
       // 同步代码不执行完毕。 异步代码不会被执行
      setTimeout(() => {
        // 因为死循环 异步代码不会被执行
        document.body.style.background = 'red'
      }, 0);
      while(true) {
    
      }
    

宏任务和微任务

  1. 宏任务是有宿主环境提供的

  2. 微任务是有语言本身进行提供的

  3. 在上一个所说的等待加入执行栈的任务其实是一个宏任务。用于做异步回调的逻辑执行。 1. 常见的微任务:Promise.then() MutationObserver queueMicrotask node中的process.nextTick() 2. 常见的宏任务: 事件 http请求 定时器 ui渲染 setImmidiate requestAnimationFrame等

事件循环调用栈图

图片1.png

    1. 事件循环和事件循环多列是有事件线程进行维护的,在执行栈中每次执行的都是宏任务的代码.
    1. 当前的宏任务代码执行微任务和宏任务,分别添加到宏任务和微任务队列的队尾,待当前宏任务中的同步代码执行完毕以后,会首先清空微任务队列.
    1. 然后在进行GUI页面渲染,待渲染完毕后会在宏任务中取出待执行的宏任务压入执行栈,继续当前的宏任务操作

常见面试题

   document.body.style.background = 'red'
   console.log(1)
   Promise.resolve().then(() => {
    console.log(2)
    document.body.style.background = 'yellow'
  })
  console.log(3)
  黄色还是红色到黄色的过渡?
  • 会一直显示是黄色:因为微任务的代码实在GUI渲染之前执行。事件环调用栈图可见
   btn.addEventListener('click', function() {
       console.log('listener1')
       Promise.resolve().then(() =>{
         console.log('micro task1')
       })
     })
     btn.addEventListener('click', function() {
       console.log('listener2')
       Promise.resolve().then(() =>{
         console.log('micro task2')
       })
     })
     btn.click()
  1. btn.click()执行时js调用栈在执行的时候js线程会对addEventListener会合并执行,不牵涉到宏任务,所以输出时listener1 listener2 micro task1 micro task2
  2. 当我们注释 btn.click()进行点击按钮触发 由于浏览器中点击事件时宏任务,所以会牵涉到事件循环队列,这时候输出就是 listener1 micro task1 listener2 micro task2
    console.log(1)
     async function async1() {
       console.log(2)
       await console.log(3)
       console.log(4)
     }
     setTimeout(() => {
       console.log(5)
     }, 0);

     const promise = new Promise((resolve) => {
       console.log(6)
       resolve(7)
     })
     promise.then(res => {
       console.log(res)
     })
     async1()
     console.log(8)
  • await的之后的执行过程可以看成时Promise.resolve()包裹,await语句后续执行逻辑是放在Promise中的then微任务中。
  • 所以 await console.log(3) 相当于Promise.resolve(console.log(3)).then(() =>{})的操作. console.log函数会立即执行.
  • 输出结果: 1 6 2 3 8 7 4 5