JS执行原理

162 阅读4分钟

前言

今天刷到一道事件循环Event Loop面试题,我发现实际运行的顺序与我所写出的答案有一些不一致。

控制台打印输出时11 先于 8

我不禁思考:script标签在js执行的优先级是怎样的 ? 查阅资料后,对JS代码运行机制有了新的认识,下面和大家分享一下。

面试题源码

<script>
        console.log(1);
        async function fnOne() {
            console.log(2);
            await fnTwo(); 
            console.log(3);
        }
        async function fnTwo() {
            console.log(4);
        }
        fnOne();

        setTimeout(() => {
            console.log(5);
        }, 2000);

        let p = new Promise((resolve, reject) => { 
            console.log(6);
            resolve();
            console.log(7);
        })

        setTimeout(() => {
            console.log(8)
        }, 0)

        p.then(() => {
            console.log(9);
        })
        console.log(10);
    </script>
    <script>
        console.log(11);
        setTimeout(() => {
            console.log(12);
            let p = new Promise((resolve) => {
                resolve(13);
            })
            p.then(res => {
                console.log(res);
            })
            console.log(15);
        }, 0)
        console.log(14);


    </script>

知识梳理

首先我们要先清楚,Event Loop(以下称为事件循环)是什么?

js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则称之为事件循环。这里我们不涉及nodejs, 事件循环是浏览器内核解析执行js代码的一种执行规则

js代码分为同步代码异步代码 两大类;异步代码又分为微任务宏任务

常见异步任务有 :

任务宏/微 任务环境
事件宏任务浏览器
网络请求(Ajax)宏任务浏览器
fs.readFile() 读取文件宏任务Node
setTimeout() 定时器宏任务浏览器 / Node
Promise.then()微任务浏览器 / Node
async/await微任务浏览器 / Node

简单阐述事件循环的执行机制

  1. 进入到script标签,默认进入到第一次事件循环。

  2. 遇到同步代码,立即执行。

  3. 遇到异步宏任务,放入到宏任务队列里。

  4. 遇到异步微任务,放入到微任务队列里。

  5. 当前事件队列所有同步执行完毕,开始执行异步队列代码

  6. 先执行异步微任务代码。

  7. 执行异步宏任务代码。

  8. 宏任务执行完毕,本次事件循环结束,执行下一次事件队列。

  9. 重复步骤2。 以此反复直到清空所以宏任务,这种不断重复的执行机制,就叫做事件循环

面试题解析

  1. 先执行同步代码 snipaste20220523_205517.jpg

  2. 然后执行微任务队列

snipaste20220523_205804.jpg

  1. 执行性宏任务队列

snipaste20220523_210303.jpg

snipaste20220523_210559.jpg

最终执行的输出顺序为 : 1 2 4 6 7 10 3 9 11 14 8 12 15 13 5

snipaste20220523_212125.jpg

关于script优先级测试

我一直在思考script的特殊性,如果只是简单的宏任务就不会插队执行了,我进行了如下测试

<script>
    console.log(1)
    new Promise((resolve,reject)=>{
        resolve(2)
    }).then(res=>{
        console.log(res)
    })
    console.log(3)     
</script>

<script>
    console.log(4)
</script>

snipaste20220523_211301.jpg 输出顺序为1 3 2 4 由此得出结论:script标签优先级低于异步微任务

为了比较script标签定时器的优先级我又进行如下测试

<script>
    console.log(1)
    setTimeout(() => {
        console.log(2);
    }, 0)
    console.log(3)     
</script>

<script>
    console.log(4)
</script>

1.jpg 输出顺序为1 3 4 2 由此得出结论:script标签优先级高于定时器

应用

为了更加清晰的理解,我又分析了一道事件循环的练习题。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    console.log(1)
    new Promise((resolve, reject) => {
      resolve(2)
    }).then(res => {
      console.log(res)
    })
    setTimeout(() => {
      console.log(3)
    })
    console.log(4)
  </script>

  <script>
    console.log(5)
  </script>

  <script>
    new Promise((resolve, reject) => {
      resolve(6)
    }).then(res => {
      console.log(res)
    })
    setTimeout(() => {
      console.log(7)
    })
    console.log(8)
  </script>

  <script>
    console.log(9)
    setTimeout(() => {
      console.log(10)
    })
  </script>
</body>

</html>

做题思路

  1. 先将页面所有script标签当做宏任务。

snipaste20220524_103730.jpg

  1. 根据js执行机制,开始解析第一个宏任务(第一个script标签)。
<!-- h1 -->
  <script>
    console.log(1)
    new Promise((resolve, reject) => {
      resolve(2)
    }).then(res => {
      console.log(res)
    })
    setTimeout(() => {
      console.log(3)
    })
    console.log(4)
  </script>
  1. h1宏任务里的定时器,是一个异步宏任务,而现在我们的宏任务列表里面已经有h1 h2 h3 h4 四个宏任务了;此处将定时器暂时命名为h5宏任务,放入到宏任务队列里。

snipaste20220524_104720.jpg

  1. h1宏任务执行完毕,进入下一个宏任务h2
 <!-- h2 -->
  <script>
    console.log(5)
  </script>
  1. h2宏任务执行完毕,进入下一个宏任务h3
  <!-- h3 -->
  <script>
    new Promise((resolve, reject) => {
      resolve(6)
    }).then(res => {
      console.log(res)
    })
    setTimeout(() => {
      console.log(7)
    })
    console.log(8)
  </script>
  1. h3宏任务里面的定时器是一个宏任务,命名为h6放入宏任务队列里。

snipaste20220524_105336.jpg

  1. h3宏任务执行完毕,进入下一个宏任务h4
<!-- h4 -->
  <script>
    console.log(9)
    setTimeout(() => {
      console.log(10)
    })
  </script>
  1. h4宏任务里面的定时器是一个宏任务,命名为h7放入宏任务队列里。

snipaste20220524_105737.jpg

  1. 依次执行h5 h6 h7

  2. 输出结果

snipaste20220524_105946.jpg

总结

  • promise本质是一个同步的代码(容器),只有等同步走完了,才会走微任务then()。
  • promise嵌套promise : 先走嵌套的then,后走外层then (promise嵌套哪个then在上面就走哪个then)
  • await下面的才是微任务, 右边的代码还是立即执行
  • await微任务可以转换成等价的promise微任务分析
  • script标签本身是一个宏任务
    • 同步特点: 当页面出现多个script标签的时候, 浏览器会把script标签作为宏任务来解析,解析不代表执行
    • 宏任务特点: 等微任务执行完毕之后,才会执行宏任务。
    • 可以把script标签当做一个优先级最高的定时器。 比微任务后执行,比定时器先执行