资讯专栏INFORMATION COLUMN

泡杯茶,我们坐下聊聊Javascript的事件环

douzifly / 3276人阅读

摘要:是怎么执行的一开始先简单聊了聊基本的数据结构,它和我们现在说的事件环有什么关系么当然有,首先要明确的一点是,代码的执行全都在栈里,不论是同步代码还是异步代码,这个一定要清楚。

栈和队列

在计算机内存中存取数据,基本的数据结构分为栈和队列。

栈(Stack)是一种后进先出的数据结构,注意,有时候也管栈叫做“堆栈”,但是“堆”又是另一种复杂的数据结构,它和栈完全是两码事。栈的特点是操作只在一端进行,一般来说,栈的操作只有两种:进栈和出栈。第一个进栈的数据总是最后一个才出来。

队列(Queue)和栈类似,但它是先进先出的数据结构,插入数据的操作从队列的一端进行,而删除的操作在另一端。

通俗的比喻栈就像是一个立好的桶,先放入栈的数据会放在桶底,出栈时会在桶口一一将数据取出,所以最先放入栈的数据总是最后一个才能取出。而队列就像是一个水管,最先放入队列的数据会第一个从队列的另一端流出,这是它们最大的区别。

在javascript中,函数的执行就一个典型的入栈与出栈的过程:

function fun1() {
    function fun2() {
        function fun3() {
            console.log("do it");
        }
        fun3();
    }
    fun2();
}
fun1();

在程序执行时,首先将fun1,fun2,fun3依次入栈,而在调用函数时,是先将fun3调用(出栈),再是fun2和fun1,试想一下,如果fun1先出栈,那么函数fun2和fun3必将丢失。

单线程和异步

在javascript这门语言中程序是单线程的,只有一个主线程,这是为什么?因为不难想像,最初javascript的设计是跑在浏览器中的脚本语言,如果设计成多线程,两个线程同时修改DOM那以谁的为准呢?所以javascript为单线程,在一个线程中代码会一句一句向下走,直到程序跑完,若中间有较为费时的操作,那也只能等着。

单线程的设计使得语言的执行效率很差,为了利用多核心CPU的性能,javascript语言支持异步代码,当有较为费时的操作时,可将任务写为异步执行,当一个异步任务还没有执行完时,主线程会将异步任务挂起,继续执行后面的同步代码,之后再回过头来看,如果有异步任务运行完了再执行它。

这种执行代码的方式其实很符合我们生活中的很多场景,比如小明同学下班回家了,他很渴,想烧水泡茶,如果是同步的执行方式那就是烧水,在水没开时小明像个傻子似的等着,等水开了再泡茶;若是异步执行,小明先开始烧水,然后就去干点别的事,比如看会电视、听听音乐,等水烧开了再去泡茶。明显第二种异步方式效率更高。

常见的异步操作都有哪些?有很多,我们可以罗列几个常见的:

Ajax

DOM的事件操作

setTimeout

Promise的then方法

Node的读取文件

我们先来看一段代码:

//示例1
console.log(1);
setTimeout(function () {
    console.log(2);
}, 1000);
console.log(3);

这段代码非常简单,把它们放在浏览器中执行结果如下:

1
3
2

因为setTimeout函数延时了1000毫秒执行,因此先输出1和3,而2是过了1000毫秒之后再输出,这很合逻辑。

我们稍稍改动一下代码,将setTimeout的延时时间改为0:

//示例2
console.log(1);
setTimeout(function () {
    console.log(2);
}, 0); //0毫秒,不延时
console.log(3);

运行结果:

1
3
2

为什么延时了0毫秒还是最后输出的2?先别急,我们再来看一段代码:

//示例3
console.log(1);
setTimeout(function () {
    console.log(2);
}, 0);
Promise.resolve().then(function(){
    console.log(3);
});
console.log(4);

运行结果:

1
4
3
2

以上三段代码,如果你能正确的写出结果,并且能说明白为什么这样输出,说明你对javascript的事件环理解的很清楚,如果讲不出来,我们就一起聊聊这里面发生了什么,其实很有意思。

javascript是怎么执行的?

一开始先简单聊了聊基本的数据结构,它和我们现在说的事件环有什么关系么?当然有,首先要明确的一点是,javascript代码的执行全都在栈里,不论是同步代码还是异步代码,这个一定要清楚。

而代码我们大体上分为了同步代码和异步代码,其实异步代码还可以再分为两类:宏任务微任务

先别管什么是宏任务和微任务,往往这种高大上的术语不利于我们理解,我们先这么认为:宏,即是宏观的、大的;微即微观的、小的。

javascript是解释型语言,它的执行过程是这样的:

从上到下依次解释每一条js语句

若是同步任务,则压入一个栈(主线程);如果是异步任务,就放到一个任务队列里

开始执行栈里的同步任务,直到将栈里的所有任务都走完,此时栈清空了

回过头看异步队列里如果有异步任务完成了,就生成一个事件并注册回调,压入栈中

再返回第3步,直到异步队列都清空,程序运行结束

语言描述的费劲,不如看图:

通过以上的步骤可以看到,不论是同步还是异步,只要是执行的时候都是要在栈里执行的,而一遍又一遍的回头检查异步队列,这种执行方式 就是所谓的“事件环”。

明白了javascript的执行原理,我们就不难理解之前的第二段代码,为什么setTimeout为0时会最后执行,因为setTimeout是异步代码,必须要等所有的同步代码都执行完,才会执行异步队列。即使setTimeout执行得再快,它也不可能在同步代码之前执行。

浏览器中的事件环

聊了这么多,我们好像还没有说宏任务和微任务的话题呢,上面说了,异步任务又分为微任务和宏任务,那它们又是一个怎样的执行机制呢?

注意!微任务和宏任务的执行方式在浏览器和Node中有差异,有差异!重要的事我们多说几遍,以下我们讨论的是在浏览器的环境里。

在浏览器的执行环境中,总是先执行小的、微任务,再执行大的、宏任务,回过头再看看第三段代码,为什么Promise的then方法在setTimeout之前执行?其根本原理就是因为Promise的then方法是一个微任务,而setTimeout是一个宏任务。

接下来我们借用阮一峰老师的一张图来说明:

其实,以上这张图示我们可以再将它细化一点,这个图上的异步队列只画了一个,也就是说没有区分微任务队列和宏任务队列。我们可以脑补一下,在此图上多加一个微任务队列,当javascript执行时再多加一个判断,如果是微任务就加到微任务队列里,宏任务就加到宏任务队列里,在清空队列时,浏览器总会优先清空“微任务”。这样就把浏览器的事件环撤底说全了。

最后来一个大考,以下代码的运行结果是什么:

将此代码拷到chrome中跑一下,结果是:

5
4
1
2
3

不妨我们试着分析一下为什么是这个结果,首先输出5,因为console.log(5)是同步代码,这没什么可说的。

之后将前两个setTimeout和最后一个Promise放入异步队列,注意它们的区分,此时执行完了同步代码之后发现微任务和宏任务队列中都有代码,按浏览器的事件环机制,优先执行微任务,此时输出4。

然后执行宏任务队列里的第一个setTimeout,输出1。

此时,setTimeout中又有一个Promise,放入微任务队列。

再次清空微任务队列,输出2。

最后宏任务队列里还有最后一个setTimeout,输出3。

Node中的事件环

而Node中的事件环又和浏览器有些许的不同,在node.js的官方文档中有专门的描述,其中文档中有一张图,详细的说明了它的事件环机制,我们把它拿出来:

可以看到,node.js中的事件环机制分为了6个阶段,其中最重要的3个阶段我在上面做了注明:

timer阶段,指的就是setTimeout等宏任务

poll轮询阶段,如读取文件等宏任务

check阶段,setImmediate宏任务

图中每一个阶段都代表了一个宏任务队列,在Node事件环中,微任务的运行时机是在每一个“宏任务队列”清空之后,在进入下一个宏任务队列之间执行。这是和浏览器的最大区别。

还是用代码说话吧,有一道经典的Node.js事件环面试题:

const fs = require("fs");

fs.readFile("./1.txt", (err, data) => {
    setTimeout(() => {
        console.log("timeout");
    });
    setImmediate(() => {
        console.log("immediate");
    });
    Promise.resolve().then(() => {
        console.log("Promise");
    });
});

运行结果:

Promise
immediate
timeout

代码并不复杂,首先使用fs模块读取了一个文件,在回调的内部有两个宏任务和一个微任务,微任务总是优于宏任务执行的,因此先输出Promise。

但是之后的区别为什么先输出immdiate?原因就在于fs读取文件的宏任务在上图中的第4个轮询阶段,当第4个阶段清空队列之后,就该进入第5个check阶段,也就是setImmediate这个宏任务所在的阶段,而不会跳回第1个阶段,因此先输出immedate。

尾巴

最后总结一下,分析完浏览器和Node的事件环发现它们并不简单,但只要记住了它们之间的区别就可以分析出结果。

浏览器事件环是运行完一个宏任务马上清空微任务队列
Node事件环是清空完一个阶段的宏任务队列之后再清空微任务队列

最后,总结一下常见的宏任务和微任务:

宏任务 微任务
setTimeout Promise的then方法
setInterval process.nextTick
setImmediate MutationObserver
MessageChannel

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97544.html

相关文章

  • 设计模式系列·Facade模式之MVC烦恼

    摘要:没有任何意外,王小二的公司用来开发公司的主打产品。臃肿的着手开干吧小二打开熟悉的,找到提交订单模块的。要不再去请教下哥的烦恼小二找到哥,详细的描述了他的问题。 流行的MVC架构模式 如今的Web开发,各种框架风起云涌,势如破竹。 从国民第一的ThinkPhp到称霸全球的Laravel,这些框架有一个共同特征,都采用了MVC的架构模式。 showImg(https://segmentfa...

    zhichangterry 评论0 收藏0
  • 编程范式 —— 函数式编程入门

    摘要:在函数式编程中数据在由纯函数组成的管道中传递。函数式编程中函子是实现了函数的容器下文中将函子视为范畴,模型可表示如下但是在函数式编程中要避免使用这种面向对象的编程方式取而代之对外暴露了一个的接口也称为。 showImg(https://segmentfault.com/img/remote/1460000018101204); 该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数...

    flyer_dev 评论0 收藏0
  • 使用fjpublish发布前端项目(基础篇)

    摘要:任务描述使用的来发布多个目录使用的忽略所有结尾的文件。任务描述使用的配置项在项目发布后重启进程忽略当次构建过程并提交一次使用的配置项设置每个远程命令超时时间为秒。下一期地址使用发布前端项目安全篇官方交流群 本系列文章共分为基础篇,安全篇,拓展篇。 前言 曾几何时,我相信部分Web Developer(包括我)使用的项目发布方式比较传统(使用xftp或者sublime text的插件sf...

    anonymoussf 评论0 收藏0
  • 聊聊幽灵Class

    摘要:有锁的地方就会有锁竞争,并且也是一个耗时的过程,所以同一个如果并发出现在日志堆栈中势必会导致一部分线程会,这对于线上系统中简直就是灾难改进在最近的版本中并没有发现对的类做特殊处理,并且正如官方说的所以只要的版本大于,这个配置默认都是关闭的 起因 偶然一次路过同事电脑,看着黑底蓝色满屏的堆栈信息,过去笑着拍了拍他的肩膀说道「小哥,又在写BUG呢」凑过去仔细看了一眼异常堆栈详情,「虎躯一震...

    Ilikewhite 评论0 收藏0
  • 牛仔很忙之大话HTTPS

    摘要:移动组的壕星在灌水群里发了条消息,这家伙又准备显摆他的肾了。什么是小夏弱弱的问。配备身份证书,防止身份被冒充。那这么说,岂不是将来所有网站都会用小夏接着道,又加了个羞涩的表情。 中秋前,本牛仔正在苦思新任务的对策,突然桌面右下角的小企鹅一阵跳动:刚收到消息, 第三方下载的XCode被植入后门,N多app信息疑似泄密。还是你们用安卓的好啊,唉。 移动组的壕星在灌水群里发了条消息,这家伙又...

    MonoLog 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<