事件循环详解(含面试题)

234 阅读7分钟

1. 进程和线程

  • 线程和进程是操作系统中的两个概念:

    • 进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式
    • 线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中
  • 简单理解:

    • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
    • 线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;所以我们也可以说进程是线程的容器
  • 一个形象的例子:

    • 操作系统类似于一个大工厂
    • 工厂中里有很多车间,这个车间就是进程
    • 每个车间可能有一个以上的工人在工厂,这个工人就是线程

    进程-线程.png

  • 操作系统如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作

    • 因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换
    • 当进程中的线程获取到时间片时,就可以快速执行编写的代码
    • 对于用户来说是感受不到这种快速的切换的

2. JavaScript单线程

  • JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node

  • 为什么JavaScript是单线程的?

    • 这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。
    • 举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征
  • 浏览器是单进程吗?

    • 目前多数的浏览器其实都是多进程的,当打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;在这个进程中还有很多线程,还有ui渲染线程,js引擎线程,http请求线程等。
    • 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
  • JavaScript的代码执行是在一个单独的线程中执行的:

    • 这就意味着JavaScript的代码,在同一个时刻只能做一件事,如果这件事是非常耗时的,就意味着当前的线程就会被阻塞
    • 所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:
      • 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作
      • 比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可

3. 事件队列/循环

  • 在执行JavaScript代码的过程中,如果有一个setTimeout的函数调用,这个函数会被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行
  • 事件队列
    • 事件队列是一种数据结构,可以存放要执行的任务。它符合队列先进先出”的特点 image.png
  • 事件循环(event loop)

    因为 js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候, 如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。 当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去判断宏任务队列中的任务。

4. 宏任务/微任务

  • 在事件循环中维护着两个队列(宏任务队列和微任务队列),都是异步任务
  • 宏任务列队
    • 用来保存待执行的宏任务(回调)
    • 如: ajax、setTimeout、setInterval、DOM监听、UI Rendering
  • 微任务列队
    • 用来保存待执行的微任务(回调)
    • 如: Promisethen回调、 Mutation Observer API、queueMicrotask()
  • 事件循环对于两个队列的优先级
    1. main script中的代码优先执行(编写的顶层script代码);
    2. 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行,如果有就将所有的微任务一个一个取出来执行(即微任务优先级比宏任务高,且与微任务所处的代码位置无关
      • 也就是宏任务执行之前,必须保证微任务队列是空的;
      • 如果不为空,那么就优先执行微任务队列中的任务(回调)
  • 如何调度:
    • promise中的回调函数立刻执行,then中的回调函数会推入微任务队列中,等待调用栈所有任务执行完才执行

    • async函数里的内容是放入调用栈执行的,await的下一行内容是放入微任务执行的

    • 调用栈执行完成后,会不断的轮询微任务队列,即使先将宏任务推入队列,也会先执行微任务(即:执行下一个宏任务之前, 会清空微任务队列)

      console.log("script start")
      
      // 定时器
      setTimeout(() => {
        console.log("setTimeout0")
      }, 0)
      setTimeout(() => {
        console.log("setTimeout1")
      }, 0)
      
      // Promise中的then的回调也会被添加到队列中
      console.log("1111111")
      new Promise((resolve, reject) => {
        console.log("2222222")
        console.log("-------1")
        console.log("-------2")
        resolve()
        console.log("-------3")
      }).then(res => {
        console.log("then传入的回调: res", res)
      })
      console.log("3333333")
      
      console.log("script end")
      
      // 执行结果
      // script start
      // 1111111
      // 2222222
      // -------1
      // -------2
      // -------3
      // 3333333
      // script end
      // then传入的回调: res undefined
      // setTimeout0
      // setTimeout1
      

5. 面试题: Promise/async/await

  • 宏任务/微任务

    console.log("script start")
    
    setTimeout(function () {
      console.log("setTimeout1");
      new Promise(function (resolve) {
        resolve();
      }).then(function () {
        new Promise(function (resolve) {
          resolve();
        }).then(function () {
          console.log("then4");
        });
        console.log("then2");
      });
    });
    
    new Promise(function (resolve) {
      console.log("promise1");
      resolve();
    }).then(function () {
      console.log("then1");
    });
    
    setTimeout(function () {
      console.log("setTimeout2");
    });
    
    console.log(2);
    
    queueMicrotask(() => {
      console.log("queueMicrotask1")
    });
    
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then3");
    });
    
    console.log("script end")
    
    // 执行结果
    // script start
    // promise1
    // 2
    // script end
    // then1
    // queueMicrotask1
    // then3
    // setTimeout1
    // then2
    // then4
    // setTimeout2
    
  • await

    console.log("script start")
    
    function requestData(url) {
      console.log("requestData")
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log("setTimeout")
          resolve(url)
        }, 2000);
      })
    }
    
    // 1.promise调用then调度
    // function getData() {
    //   console.log("getData start")
      
    //   requestData("aaa").then(res => {
    //     console.log("then-res:", res)
    //   })
      
    //   console.log("getData end")
    // }
    
    // 2.await/async
    async function getData() {
      console.log("getData start")
      const res = await requestData("aaa") // 此处先执行requestData(),在执行await
      // await 后面都可以看作callback里面的内容即异步
      // 类似,event loop,setTimeout
      
      // 此处及以后的代码相当于放在 callback 中,最后执行
      console.log("then-res:", res)
      console.log("getData end")
    }
    
    getData()
    
    console.log("script end")
    
    // 代码执行结果
    // 1. promise
    
    // script start
    // getData start
    // requestData
    // getData end
    // script end
    // setTimeout
    // then-res: aaa 
    
    
    // 2. async 
    
    // script start
    // getData start
    // requestData
    // script end
    // setTimeout
    // then-res: aaa
    // getData end
    
  • 面试题2

    async function async1 () {
      console.log('async1 start')
      await async2(); // async2执行后没有返回值,相当于Promise.resolve(undefined),后续代码加入到微任务队列中
      console.log('async1 end')
    }
    
    async function async2 () {
      console.log('async2')
    }
    
    console.log('script start')
    
    setTimeout(function () {
      console.log('setTimeout')
    }, 0)
    
    async1();
    
    new Promise (function (resolve) {
      console.log('promise1')
      resolve();
    }).then (function () {
      console.log('promise2')
    })
    
    console.log('script end')
    
    /**
     * script start
     * async1 start
     * async2
     * promise1
     * script end
     * async1 end
     * promise2
     * setTimeout
    */
    

6. 总结

  • event loop 和 DOM 渲染的关系

    • Call Stack 空闲,即同步代码执行完
    • 尝试 DOM 渲染,DOM 结构改变重新渲染
    • 触发 Event Loop,
  • 为什么微任务比宏任务执行更早

    • 宏任务: DOM 渲染后触发,如 setTimeout

    • 微任务:DOM 渲染前触发,如 Promise.then()

  • 微任务和宏任务的根本区别

    • 宏任务:ES6 语法规定

    • 微任务:浏览器规定

  • js执行过程

    1. Call Stack空闲,即同步代码执行完
    2. 执行当前微任务
    3. 尝试DOM渲染,DOM结构改变重新渲染
    4. 触发Event Loop
    5. 执行当前宏任务