JS 事件循环 Event Loop

163 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

在 JS 中,事件循环是相当重要的一个知识点,它涉及到很多方面的内容,也是很多知识点的前置知识。所以在这里做一个总结,随着知识面的扩充,还会添加更多相关的知识内容,以期形成知识网络。

JS 的「单线程」

进程和线程

想要了解JS的单线程,首先我们先来复习下进程和线程的概念:

  • 进程:操作系统分配的占有 CPU 资源的最小单位。拥有独立的地址空间。
  • 线程:安排 CPU 执行的最小单位。同一个进程下的所有线程,共享进程的地址空间。

单线程和多线程

  • 单线程:就是一个进程中只有一个线程。程序顺序执行,前面的执行完,才会执行后面的程序。
  • 多线程:就是一个进程中只有多个线程。在进程内部进行线程间的切换,由于每个线程执行的时间片很短,所以在感觉上是并行的。

浏览器的渲染进程

浏览器也是很复杂的一套知识体系,现在我们主要关注浏览器进程的相关知识。

浏览器主要有四种进程:

  • 浏览器进程:浏览器的主进程(负责协调、主控),只有一个
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程:最多一个,负责3D绘制和硬件加速
  • 浏览器渲染进程:浏览器内核,主要负责 HTML,CSS,JS 等文件的解析和执行

浏览器是多进程的,浏览器的渲染进程是多线程的。浏览器渲染进程即浏览器内核,是浏览器的进程之一:主要作用:进行页面的渲染、脚本执行、事件处理等。

渲染进程主要包括如下几个线程:

  • JS 引擎线程
  • GUI 渲染线程
  • 事件触发线程
  • 定时器触发线程
  • 异步 HTTP 请求线程

JS引擎线程

  • 也称为JS内核,负责 处理 JavaScript 脚本程序。例如谷歌浏览器的V8引擎
  • JS引擎线程负责 解析 JavaScript 脚本,运行代码。
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页( render 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。这也就是常说的**「 JS 是单线程的」**

可是在上文提到,单线程的定义是一个进程中只有一个线程,但是现在面对的情况是,浏览器渲染进程下有五种不同的线程,包括 JS 引擎线程,那这不是和定义冲突了吗?这是我在学习过程中的疑惑。

个人认为,「JS 是单线程的」 的说法并不确切,这个词是想表达的意思是:

在浏览器渲染进程中,有且仅有一个 JS 引擎线程。并且在这个线程中,所有待执行的程序任务按顺序排好队,依次执行,当上一个任务完成后,才会执行下一个任务。

鉴于 「 JS 是单线程的」 的说法流传已广,这里建议大家再见到有关“ JS " "单线程"的说法时,在脑海中还是要联想到它实际要表达的含义,而不是局限于单线程的定义。

同步任务和异步任务

从上文的叙述中我们了解了 「JS单线程」 的特性,这个特性与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作 DOM,这决定了它只能是单线程。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

但是这种执行任务的方式有个显著的问题,如果前一个任务耗时很长,后一个任务就不得不一直等着,因此会造成线程堵塞,用户界面卡顿,带来不好的体验。

JavaScript 语言的设计者意识到,这时主线程完全可以不管 IO 设备,挂起处于等待中的任务先运行排在后面的任务。等到 IO 设备返回了结果,再回过头,把挂起的任务继续执行下去。 所谓挂起一个任务,就是暂停这个任务运行,它仍然占用一定的内存空间,有可能对 CPU 也在占用着。

基于此,可以将任务划分为同步任务异步任务

  • 同步任务:在线程中排队依次执行的任务。注意!并不是指同时进行的任务!
  • 异步任务:一般是执行花费时间长,会阻塞线程的任务。并且这样的任务通常会有回调函数。当遇到这样的任务时,JS 引擎线程将该任务交由别的线程执行(事件触发线程、定时器触发线程、异步 HTTP 请求线程),当任务在这些线程上执行完毕后,会将回调函数放入一个任务队列中,等待被 JS 引擎线程执行。

完整的异步过程:异步请求 -> 执行异步操作 ->(异步操作完成)-> 回调函数到消息队列中排队 -> 主线程空闲时到消息队列中读取回调函数到执行栈 -> 执行回调函数

接下来,再对如下两个概念进行详细解读。

回调函数

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.

说白了,回调函数,就是 「回头再调用」 的意思,那要怎么理解呢?有两个注意点:

  1. 在另一个函数内部调用
  2. 回调函数必须是作为参数传入另一个函数

第二点尤其重要,下面代码中,只有函数b中的callback才叫回调函数,在这里callback函数a

function a() {
    console.log("a");
}
​
function b(callback) {
    console.log("callback");
    callback();
}
​
function c() {
    console.log("not callback");
    a();
}
​
function d() {
    function a() {
        console.log("a in d");
    }
    console.log("not callback");
    a();
}
​
b(a);
c();
d();

还有一点需要明确,回调函数和同步、异步任务并没有什么直接联系,并不是说只有用在异步任务里才有回调函数。

任务队列

任务队列,或者叫事件队列,总之是和异步任务相关的队列。这种先入先出的数据结构,和排队是类似的,哪个异步操作完成的早,就排在前面。不论异步操作何时开始执行,只要异步操作执行完成,就可以到任务队列中排队。这样,主线程在空闲的时候,就可以从任务队列中获取任务并执行。

对于任务队列,其实是有更细的分类。其被分为 微任务(microtask)队列 & 宏任务(macrotask)队列,在一个浏览器渲染进程中,可以同时存在多个宏任务队列,但是同时只能有一个微任务队列。

宏任务:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

微任务:

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

Event Loop

在理解事件循环之前,这里再补充两个知识点:

:栈会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。

:动态分配的内存,大小不定也不会自动释放,存放引用类型,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。

好了,有了上面的知识铺垫,理解事件循环机制就比较简单了。下面先说说我的理解:

事件循环机制,就是处理同步任务和异步任务执行时机的一种机制。

具体执行流程:

  1. 首先要执行完栈的内容,确保栈已清空;
  2. 如果微任务队列不为空,则依次取出微任务队列中的回调函数,放入执行栈执行,执行后确保栈已空;否则直接进入下一步;
  3. 取出宏任务队列中等待执行的回调函数,放入执行栈并执行,最后执行栈弹出该回调函数,确保执行栈清空;
  4. 重复 2/3 步,形成循环。

下面是一个便于理解的示意图:

小测试

下面这段代码输出是啥样的嘞?

setTimeout(() => {
    console.log("我是第一个宏任务");
    Promise.resolve().then(() => {
        console.log("我是第一个宏任务里的第一个微任务");
    });
    Promise.resolve().then(() => {
        console.log("我是第一个宏任务里的第二个微任务");
    });
}, 0);
​
setTimeout(() => {
    console.log("我是第二个宏任务");
}, 0);
​
Promise.resolve().then(() => {
    console.log("我是第一个微任务");
    setTimeout(() => {
        console.log("我是第三个宏任务");
        Promise.resolve().then(() => {
            console.log("我是第3个宏任务里的第一个微任务");
        });
        Promise.resolve().then(() => {
            console.log("我是第3个宏任务里的第二个微任务");
        });
    }, 0);
});
​
console.log("执行同步任务");

参考文献

深入理解javascript中的事件循环event-loop

html.spec.whatwg.org/multipage/w…

micro task这个任务队列是由事件触发线程维护还是有JS引擎线程维护的

blog.csdn.net/qq_43952245…

github.com/aooy/blog/i…

segmentfault.com/a/119000001…

jakearchibald.com/2015/tasks-…