欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript中的事件循环机制:你不得不懂的JS原理

程序员文章站 2022-07-01 19:19:40
...

事件循环

学过JS的都知道,JS是单线程的,即使html5中提出了woker,但它依旧在主线程的控制之下,只能进行计算任务,而不能操作dom等,所以worker并没有改变JS是一个单线程这一机制。单线程即是后一个任务必须要等待前一个任务执行完毕才能执行,如果执行像setTimeout延迟器,亦或者异步任务等,都不会消耗cpu,就会有空等的情况,为了更好的协调事件、脚本、UI渲染等行为,于是有了事件循环机制。

在了解事件循环之前,先了解几个概念:

JS:一门计算机语言,提供了表达程序逻辑的语法和实现基本功能的API

浏览器:JS语言的真实运行环境,又称之为JS的宿主环境

JS执行引擎:JS宿主环境(例如浏览器)中的一个功能模块,用于解析并执行JS

进程:当一个应用程序运行时,需要使用内存和CPU资源,这些资源需要向操作系统申请。操作系统以进程的方式来分配这些资源,一个进程就代表着一块独立于其他进程的内存空间。一个应用程序要运行,必须至少有一个进程启动。进程的最大特点是独立,一个进程不能随意的访问其他进程的资源。这就保证了多个程序在操作系统上运行互不干扰。

JavaScript中的事件循环机制:你不得不懂的JS原理

线程:可能要同时执行多个任务,每个任务需要在一个线程上运行,线程与线程之间相对独立,但可以共享应用程序的进程数据。

JavaScript中的事件循环机制:你不得不懂的JS原理

浏览器中的线程

  • JS执行线程:负责执行执行栈的最顶部代码

  • GUI渲染线程:负责渲染页面

  • 事件监听线程:负责监听各种事件

  • 计时线程:负责计时

  • 网络http线程:负责网络通信

单线程与多线程

我们之所以称JS为单线程的语言,是因为它的执行引擎只有一个线程,并且不会在执行期间开启新的线程。而并非浏览器是单线程的

单线程的应用程序的优点

  • 易于学习和理解:所有代码都是按照顺序从上到下执行的
  • 易于掌控程序:由于代码都按照顺序执行,不会出现中断,也没有共享资源的争夺问题,极大的降低了开发难度。
  • 更加合理的利用计算机资源:创建新的线程和销毁线程都会耗费额外的CPU和内存资源,没有良好的线程设计,将导致程序运行效率低下。而单线程的应用不受此影响

任何一个程序在执行期间都可能会开启多个任务,比如:

var count = 0
var btn = document.getElementsByClassName("btn")[0];
console.log(count);

setInterval(() => {
    count++
    console.log(count)
}, 1000);


btn.onclick = function(){
    count++
    console.log(count)
}

这段代码开启了三个任务:

任务1:程序启动时开始进行一些操作

任务2:开启一个计时器,每隔一段时间去做一些事

任务3:监听按钮是否被点击,当按钮被点击后,去做一些事

我们分别来看一下,单线程和多线程的对于相同的任务,有什么区别吧。

多线程

JavaScript中的事件循环机制:你不得不懂的JS原理

可以看到,多线程是一个任务开启一个线程,如果以多线程的方式运行,会导致程序代码在某些时候会有重叠执行的情况出现,如果这些代码凑巧在使用共享数据,将难以控制最终的运行结果。

单线程

JavaScript中的事件循环机制:你不得不懂的JS原理

可以看出,JS单线程执行,也是配合了浏览器中的其他线程,但是所有的JS代码都在单个线程中执行,不会出现多个任务同时执行的情况,自然就不会出现资源争夺的问题。

同步代码:程序启动后,在JS执行线程上立即执行的任务代码。下面的代码都是同步代码

function print(callback){
    console.log(1)
    callback()
}
print(function(){
    console.log(2)
})
console.log(3)

异步代码:收到宿主环境(浏览器)的其它线程通知,即将在JS执行线程上执行的代码,例如计时器回调函数中的代码,事件中的代码,Promise回调的代码,网络请求的代码都是异步的。JS中的异步代码往往放到一个函数中,该函数成为异步函数(该函数是异步的)。

执行栈:为了保证JS代码有序的执行,JS执行引擎使用执行栈来组织JS代码。每当调用一个函数时,都会在执行栈中创建一个执行上下文,上下文中提供了函数执行需要的环境,创建了上下文之后,再执行函数。是不是感觉说的不是人话了,哈哈哈,看个例子加配图就明白了。我把以下代码在执行栈运行图画了出来。

console.log("1")

function a(){
    console.log("a")
    b()
}
function b(){
    console.log("b")
}

a()

只要是JS执行引擎执行代码,在执行栈中就会创建全局上下文,等JS执行完所有代码后,销毁全局上下文,如上面代码,执行完最后一行a()函数后,销毁全局上下文,代码执行结束。JS引擎永远执行的是执行栈的最顶部

JavaScript中的事件循环机制:你不得不懂的JS原理
JavaScript中的事件循环机制:你不得不懂的JS原理
事件循环:是JS处理异步函数的具体方法。文章的重点来啦

事件循环是JS处理异步函数的具体方法:

  1. JS执行引擎执行 执行栈 中的代码
  2. 遇到一些特殊代码交给浏览器的其他线程处理
  3. 将执行栈中的代码全部执行完毕
  4. 从事件队列中取出第一个微任务,无微任务取出第一个宏任务,放入执行栈,然后重复第1步

JavaScript中的事件循环机制:你不得不懂的JS原理
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

  • 宏任务(队列)macroTask:计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
  • 微任务(队列)microTask:Promise的回调, MutationObserver(用于监听某个DOM对象的变化)

当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

我们来举个例子:

console.log("1")
setTimeout(function print(){
   console.log("a") 
}, 0);
console.log("2")

这段代码的执行过程,下方配图:

  1. 创建全局上下文
  2. 创建console.log上下文,输出 1 销毁console.log上下文
  3. 创建setTimeout函数上下文,通知web api中的计时线程,计时0秒后执行print()函数。此时执行栈销毁setTimeout函数上下文。web api计时0秒后,将延迟事件加入宏队列
  4. 创建console.log上下文,输出 2 销毁console.log上下文。销毁全局上下文
  5. 此时执行栈为空,执行事件队列中的任务。创建print函数上下文,创建console.log上下文,输出 a。销毁console.log上下文,销毁print函数上下文。
  6. JS执行完毕

JavaScript中的事件循环机制:你不得不懂的JS原理
再附一个含有Promise回调的超级恶心的事件循环的例子:

setTimeout(()=>{
    console.log(1)
    a()
},0)
const pro= new Promise((resolve)=>{
    console.log(2)
    resolve(3)
})
pro.then(res=>{
    console.log(res)
})
function a(){
    setTimeout(() => {
        console.log(4)
    }, 0);
    console.log(5)
}
a()
console.log(6)

按照上面的步骤,很容易就能判断出来。输出结果为:

2 5 6 3 1 5 4 4

最后的最后再附一个表情包!
JavaScript中的事件循环机制:你不得不懂的JS原理

相关标签: 大web前端