likes
comments
collection
share

穿越JavaScript事件循环event-loop:异步的微妙舞曲

作者站长头像
站长
· 阅读数 15

前言

JavaScript是一种广泛应用于网页开发和移动应用开发的编程语言,而event-loop是JavaScript中一个重要的概念。在本文中,我们将深入探讨JavaScript中event-loop的知识点,包括其定义、作用和实际应用。通过本文的阐述,读者将能够更好地理解JavaScript中event-loop的工作原理,从而更好地应用它来解决实际的开发问题。

正文

进程和线程

我们需要知道JS是一个单线程语言,所以先来解释一下什么是进程和线程。

进程: CPU运行指令和保存上下文所需时间。进程是计算机中正在运行的程序的实例。每个进程都有自己的内存空间、数据、代码和系统资源。进程之间是相互独立的,它们不能直接访问其他进程的数据。每个进程都有自己的地址空间,因此进程之间的通信需要通过特定的机制,比如管道、共享内存或消息队列。举个例子,一般来说我们手机开一个软件就像是一个进程,在电脑浏览器里开一个web就像是开一个进程。

线程: 进程中的更小的单位,描述了一段指令执行所需的时间线程是进程中的执行单元,一个进程可以包含多个线程。不同于进程,线程共享同一进程的地址空间和系统资源,因此线程之间可以直接访问同一进程的数据。线程之间的通信更加方便,可以直接通过共享内存等方式进行数据交换。一样的例子开一个web页面,需要多个线程配合才能完成页面的展示1. 渲染线程(GPU)2. http请求线程 3. js引擎线程等等

但是我们要知道渲染线程(GPU)和 js引擎线程 是互斥的,为什么呢?因为两个线程都能操作一个数据结构,这样的话如果同时操作,js在通过代码改变元素中的样式的时候,浏览器需要重新渲染,所以为了避免数据竞争和不一致的渲染结构,两者是不会同时进行的。

同步和异步

我们都知道js里面是分同步和异步代码的,要是v8在执行代码时,碰到了异步代码它就会把异步代码放到一边,先执行后续代码而不会等待这个操作完成,等同步代码执行完成再去执行那段代码,这也就构成了js的单线程。举个例子

let a = 1
console.log(a);
setTimeout(() => {
    console.log(a);
}, 1000);
a = 2

在这段代码里,我们先执行前两行,然后碰到setTimeout函数,设置一个定时器,等待1秒后执行回调函数;然后执行第六行,等第六行执行完成后,等待一秒,定时器触发,执行回调函数。 所以输出结果为1 2,

再将这个例子改一下

let a = 1
console.log(a);

setTimeout(() => {
    console.log(a);
}, 1000);

for (let i = 0;i < 10000; i++){ // 假设执行要1s

}

如果这个for循环执行也要一秒,那么你觉得执行顺序是怎么样的呢,其实还是一样的,因为for循环还是同步函数,它执行的快慢并不是确定的,还依赖于电脑的性能,比如你拿一台00年的电脑,搞一个10000次的循环,但是拿一台几万的电脑,两者运行的时间可能会差好几秒。

那js为什么不设计成多线程语言呢,因为js当年设计的时候就是为了节省这门语言对运行内存的开销,所以毫无疑问这门语言就是会比多线程语言运行慢,它的优点也显而易见:1. 节省内存 2. 单线程没有锁的概念,节省上下文切换时间。

然后给你一段代码

console.log('start');

setTimeout(() => {
    console.log('setTiomeout');
    setTimeout(() => {
        console.log('inner');
    })
}, 1000);

new Promise((resolve,reject) => {
    console.log('Promise');
    resolve()
})

.then(() => {
    console.log('then1');
})
.then(() => {
    console.log('then2');
})

我们知道了同步和异步的概念,但是看到这段代码还是不懂它的执行顺序啊,那我们接下来看。 异步其实还分成两种:

  • 宏任务 (macrotask): script标签 setTimeout定时器 setInterval setImmediate I/O UI-rendering页面渲染

  • 微任务 (microtask): promise.then() MutationObserver Process.nextTick()

这两种都是定死的,宏任务有一个宏任务队列,微任务有一个微任务队列。v8执行代码就是先执行同步代码,同步代码执行完了就是执行异步代码。就像最开的代码,先执行同步代码,同步代码执行完了就执行异步代码,异步代码就是那个定时器代码,但是定时器里面是不是也可以写个几十行代码,然后这几十行代码里还可以放同步和异步代码,然后继续循环,是不是就可以一直循环下去,这就叫事件循环机制event-loop。

enent-loop

我们先来讲讲enent-loop的循环机制:

  1. 执行同步代码 (这属于宏任务)
  2. 当执行栈为空,查询是否有异步代码需要执行
  3. 如果有就执行微任务
  4. 如果有需要,会渲染页面
  5. 执行宏任务 (这也叫下一轮的event-loop的开启)

好了,有个这个机制,我们再看之前那段代码

console.log('start');

setTimeout(() => {
    console.log('setTiomeout');
    setTimeout(() => {
        console.log('inner');
    })
}, 1000);

new Promise((resolve,reject) => {
    console.log('Promise');
    resolve()
})

.then(() => {
    console.log('then1');
})
.then(() => {
    console.log('then2');
})

穿越JavaScript事件循环event-loop:异步的微妙舞曲

一整个代码流程就是这样了,文字描述一下

  1. 执行 console.log('start'),输出 start
  2. 调用 setTimeout,设置一个定时器,等待1秒后执行回调函数。
  3. 执行 new Promise,并立即执行其中的代码,输出 Promise
  4. 执行 resolve(),但由于setTimeout中的回调函数未指定时间,因此立即执行下一个then
  5. 执行 then(() => {console.log('then1')}),输出 then1
  6. 执行 then(() => {console.log('then2')}),输出 then2
  7. 等待1秒后,setTimeout的回调函数执行,输出 setTimeout
  8. setTimeout的回调函数中再次调用setTimeout,但未指定时间,因此立即执行下一个then
  9. 执行第二个then(() => {console.log('inner')}),输出 inner

ok,最后看一道题巩固一下

function A(){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            console.log('异步A完成');
            resolve()
        }, 1000);
    })
}
function B(){
    setTimeout(() => {
         console.log('异步B完成');
    },500)
}
function C(){
    setTimeout(() => {
        console.log('异步C完成');
    },100)
}
A()
.then(() => { 
    B()
})
.then(() => {
    C()
})

这段代码怎么一回事,它输出的是异步A完成,异步C完成,异步B完成,为什么呢?因为.thenC那部分会跟着前面那个.thenB执行,前面那个开始执行这个就立马跟着开始执行,但是前面那个里面的内容执行需要时间,C这个执行更快所以后面这个异步C完成先输出,那么我们要是想让它顺序输出ABC应该怎么办?

function A(){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            console.log('异步A完成');
            resolve()
        }, 1000);
    })
}

function B(){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            console.log('异步B完成');
            resolve()
        },500);
    })
}

function C(){
    setTimeout(() => {
        console.log('异步C完成');
    },100)
}

A()
.then(() => { 
     return B()
})
.then(() => {
    C()
})

这样改就行了,因为.then虽然默认会返回promise对象,但是当.then的回调有人为返回promise对象时,.then默认的promise会失效。

在第一个例子中,函数B并没有返回一个Promise对象,所以在调用then方法时,会直接执行函数B中的定时器,而不会等待函数B的定时器完成后再执行下一个then。因此,函数C中的定时器不会等待函数B的定时器执行完毕,而是会立即执行。

在第二个例子中,函数B返回了一个Promise对象,因此在调用then方法时,会等待函数B中的定时器完成后再执行下一个then

总结

本文主要介绍了事件循环(Event Loop)的相关知识,同时还涉及了异步和同步、微任务和宏任务、线程和进程等内容。以下是各部分的总结:

  1. 线程和进程:线程是操作系统中独立调度的基本单位,进程是资源分配的基本单位。JavaScript是单线程语言,但是可以通过Web Workers实现多线程操作。多线程操作可以提高程序的并发处理能力,但也会带来一些问题,如线程同步、死锁等。
  2. 异步和同步:在JavaScript中,异步操作不会阻塞程序的执行,而同步操作会阻塞程序的执行。异步操作通常使用回调函数、Promise和async/await等方式实现。
  3. 微任务和宏任务:微任务和宏任务都是异步操作,它们的执行顺序不同。微任务的执行顺序在宏任务之前,包括Promise的then/catch/finally、MutationObserver等;宏任务包括setTimeout、setInterval、I/O等。
  4. 事件循环(Event Loop):事件循环是JavaScript用于处理异步操作的机制。它负责监视调用栈和消息队列,确保执行顺序的稳定。事件循环的执行顺序为:首先执行同步任务,然后执行微任务,最后执行宏任务。

总之,了解事件循环、异步和同步、微任务和宏任务、线程和进程等概念,有助于我们更好地理解JavaScript的运行机制,以及如何编写高效、稳定的JavaScript程序。

觉得有帮助的大佬们点点赞吧!