深入理解 Javascript 执行机制 [ Event Loop 事件循环 ]

740 阅读5分钟

前言

但凡是做前端的,应该都知道 Javascript 有多么重要,纵观目前的主流技术栈,像什么 Vue, React,Node 都是基于 Javascript 的,那这里必须提到一个点 Javascript 执行机制 只有了解的够深,才能够运用的得心应手,好了,进入主题

扩展前置

单线程

Javascript 的单线程 - 引用思否的说法: JavaScript的一个语言特性(也是这门语言的核心)就是单线程。什么是单线程呢?简单地说就是同一时间只能做一件事,当有多个任务时,只能按照一个顺序一个完成了再执行下一个。

为什么JS是单线程

JS最初被设计用在浏览器中,作为浏览器脚本语言,JavaScript的主要用途是与用户互动以及操作DOM,如果浏览器中的JS是多线程的,会带来很复杂的同步问题

比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准 ?

所以为了避免复杂性,JavaScript从诞生起就是单线程,为了提高CPU的利用率,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个标准并没有改变JavaScript单线程的本质;

事件循环 Event Loop

在 Javascript 执行中,分为同步任务和异步任务,同步任务和异步任务分别进入不同线程,同步的进入主线程,异步的进入 Event Table (事件列表) 并注册函数 。 当指定的异步任务完成时,Event Table 会将这个函数移入 Event Queue (事件队列)。 当主线程内的任务执行为空时,会去 Event Queue (任务队列) 读取对应的函数,进入主线程执行。 上述过程会不断重复,也就是我们常说的 Event Loop (事件循环) 。

通过一张图了解一下

在这里插入图片描述

实践出真理

同步模式

大白话的描述可能不是那么容易理解,我通过一段代码来表达下

console.log(1)
function hs () {
    console.log(3)
}
function hs2 () {
    console.log(2)
    hs()
}
hs2()
console.log(4)

// 结果相信大家都知道 1,2,3,4 

那它是怎么执行的呢,按照上述 Event Loop 事件循环规则来讲,其实就是直接进入主线程,依次自上而下执行,故输出:1,2,3,4 , 比较简单,重点在于 异步 , 往下看

异步模式

同样,用一段代码来表达一下

console.log(1)
setTimeout(function hs () {
    console.log(2)
},2000)
setTimeout(function hs2 () {
    console.log(3)
    setTimeout(function hs3 () {
     console.log(4)
    },3000)
},1000)
console.log(5)

结果相信很多童鞋也都能说出来 1,5,324

那它又是怎么执行的呢 ? 按照上述 Event Loop 事件循环规则,我们捋一下逻辑 (抛开全局调用栈)

  1. Js 单线程自上而下执行,走到 console.log(1),入栈 > 执行调用栈 打印 1 > 出栈
  2. 然后,走到第一个 setTimeout 执行入栈操作 > 执行调用栈(在事件列表 Event Table 中注册事件 2s 后执行 hs 函数)> 出栈 注意,没有任何代码执行,只是在事件列表中注册了对应事件
  3. 紧跟着继续往下走,走到第二个 setTimeout 执行入栈操作 > 执行调用栈(在事件列表 Event Table 中注册事件 1s 后执行 hs2 函数)> 出栈 跟步骤2同理
  4. 最后走到 console.log(5) 同理,入栈 > 执行调用栈 打印 5 > 出栈
  5. 结束,此时整个示例代码,调用栈中没有代码可执行了,控制台依次打印了 1,5

调用栈是没有了,可是我们的事件列表中还有呀,步骤2,3

  1. 所以 Js 会怎么做呢,它会将事件列表 Event Table 中的注册事件根据执行先后依次移入事件队列 Event Queue 中,移入事件队列中干什么,主角来了 Event Loop 事件循环, 那么 Event Loop 什么时候执行 ? 看步骤 5 , 调用栈中没有可执行的任务了,此时会从事件队列中读取对应的函数执行,ok
  2. 通过 Event loop 事件循环,从事件队列 Event Queue 中读取对应的函数进入执行栈
  3. 就是 1s 后执行 hs2 函数,入栈 > 执行调用栈· 打印 3 (在事件列表 Event Table 中注册事件 3s 后执行 hs3 函数) > 出栈
  4. 就是 2s 后执行 hs 函数,入栈 > 执行调用栈 打印 2 > 出栈
  5. 结束,此时调用栈里又没有代码可执行了,同理,再去事件列表中找找看
  6. 哎呀,有一个 3s 后执行函数 hs3,移入事件队列,通过事件循环,入栈 > 调用执行栈 打印 4 > 出栈
  7. over

这 12 步骤,详细的描述了事件循环 Event Loop 的执行规则,也清晰的表达了上述代码的执行逻辑,细心看两遍,准会,哈哈

over, 我也动动小手画了一张图,看看是否能帮助小伙伴们多理解一点

image.png

宏任务与微任务

其实在异步任务里还分有宏任务 macro-task 和微任务 micro-task

  • macro-task(宏任务) :包括整体代码 script,setTimeout,setInterval
  • micro-task(微任务) : Promise.then,MutationObserver(监听DOM),node里边的process.nextTick

通过一个实例了解一下

setTimeout(() => console.log(2),2000)
new Promise((resolve,reject) => {
    resolve(1)
}).then(c => console.log(c))
console.log(3)

// 输出 3 ,1 ,2

看输出结果显而易见,同一轮循环中微任务的优先级高于宏任务,事实上任务队列分为两种,宏任务任务队列和微任务任务队列,当存在多个微任务的情况下,会依次执行微任务,当微任务队列被清空,才会去执行宏任务注意:同一轮事件循环中

实际上一句话就可以概括:就是当主线程同步任务执行完毕后,从任务队列开始执行异步任务的时候,会先清掉本轮循环中的微任务队列,再去执行本轮循环中的宏任务,如此类推

小小鼓励,大大成长,欢迎点赞