如何解释JavaScript的事件循环面试官才满意

9次阅读

共计 4530 个字符,预计需要花费 12 分钟才能阅读完成。

观感度:????????????????????

口味:甘梅地瓜

烹饪时间:30min

每天都在写 JavaScript 的你,是否清楚 JavaScript 引擎的原理呢?

想要了解 JavaScript 引擎,首先我们从它的运行机制 Event Loop 来说起。

首先科普一些基础知识。

进程和线程

进程

应用程序的执行实例,每一个进程都是由私有的虚拟地址空间、代码、数据和其他系统资源所组成。

线程

线程是进程内的一个独立执行单元,在不同的线程之间是可以共享进程资源的。

有句老话是这样说的,穷养儿子富养女。

进程就是一个富二代爸爸,它选择了穷养线程儿子。

进程拥有独立的堆栈空间和数据段,每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段。

线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高。

一句话解释进程和线程

进程:资源分配的最小单位

线程:程序执行的最小单位

关于进程和线程方面的知识我们先了解到这,感兴趣的同学们可以移步
进程和线程的区别

Q&A 再来回答一个问题:

在多线程操作下可以实现应用的并行处理,从而以更高的 CPU 利用率提高整个应用程序的性能和吞吐量。特别是现在很多语言都支持多核并行处理技术,然而 JavaScript 却以单线程执行,为什么呢?

答:JavaScript 作为脚本语言,最初被设计用于浏览器。为了避免复杂的同步问题 (做人嘛,还是简单点好,语言也一样),如果 JavaScript 同时有两个线程,一个线程中执行在某个 DOM 节点上添加内容,另一个线程执行删除这个节点,这时浏览器会……

所以 JavaScript 的单线程是这门语言的核心,未来也不会改变。

有人说,那 HTML5 的新特性Web Worker,可以创建多线程呀~

是的,为了解决不可避免的耗时操作 (多重循环、复杂的运算),HTML5 提出了Web Worker,它会在当前的 js 执行主线程中开辟出一个额外的线程来运行 js 文件,这个新的线程和 js 主线程之间不会互相影响,同时提供了数据交换的接口:postMessageonMessage

但是因为它创建的子线程完全受控于主线程,且位于外部文件中,无法访问 DOM。所以它并没有改变 js 单线程的本质。

单线程就意味着,所有的任务都需要排队。

就像还不能自助点餐的时候你去肯德基需要排队,有的人没想好点什么或者点的东西很多,耗时就会长,那么后面的人也只好排队等待。有了自助点餐服务后,一切问题迎刃而解。

语言的设计和生活中的现实情况很像,IO 设备 (输入输出) 很慢(比如 Ajax),那么语言的设计者意识到这一点,就在主线程中挂起处于等待中的任务,先运行后面的任务,等 IO 设备有了结果,再把挂起的任务执行下去。

Event Loop

从上图中我们可以看到,在主线程运行时,会产生堆 (heap) 和栈(stack)。

堆中存的是我们声明的 object 类型的数据,栈中存的是基本数据类型以及函数执行时的运行空间。

栈中的代码会调用各种外部 API,它们在任务队列中加入各种事件 (onClick,onLoad,onDone),只要栈中的代码执行完毕(js 引擎存在monitoring process 进程,会持续不断的检查主线程执行栈是否为空),主线程就回去读取任务队列,在按顺序执行这些事件对应的回调函数。

也就是说主线程从任务队列中读取事件,这个过程是循环不断的,所以这种运行机制又成为Event Loop(事件循环)。

同步任务和异步任务

我们可以将任务分为同步任务和异步任务。

同步任务就是在主线程上排队执行的任务,只能执行完一个再执行下一个。

异步任务则不进入主线程,而是先在 event table 中注册函数,当满足触发条件后,才可以进入任务队列来执行。只有任务队列通知主线程说,我这边异步任务可以执行了,这个时候此任务才会进入主线程执行。

举个????

console.log(a);

setTimeout(function () {console.log(b);
  },1000)
  
console.log(c)  

// a
// c
// b

1.console.log(a)是同步任务,进入主线程执行,打印 a。

2.setTimeout 是异步任务,先被放入 event table 中注册,1000ms 之后进入任务队列。

3.console.log(c)是同步任务,进入主线程执行,打印 c。

当 a,c 被打印后,主线程去事件队列中找到 setTimeout 里的函数,并执行,打印 b。

综上所述,b 最持久~(扯个????)

宏任务和微任务

本文的 MacrotaskWHATWG 中叫 taskMacrotask 为了便于理解,并没有实际的出处。

同步任务和异步任务的划分其实并不准确,准确的分类方式是宏任务 (Macrotask) 和微任务(Microtask)。

宏任务包括:script(整体代码), setTimeout, setInterval, requestAnimationFrame, I/O,setImmediate

其中 setImmediate 只存在于 Node 中,requestAnimationFrame只存在于浏览器中。

微任务包括:Promise, Object.observe(已废弃), MutationObserver(html5 新特性),process.nextTick

其中 process.nextTick 只存在于 Node 中,MutationObserver只存在于浏览器中。

注意:

UI Rendering不属于宏任务,也不属于微任务,它是一个与微任务平行的一个操作步骤。

HTML 规范文档

这种分类的执行方式就是,执行一个宏任务,过程中遇到微任务时,将其放到微任务的事件队列里,当前宏任务执行完成后,会查看微任务的事件队列,依次执行里面的微任务。如果还有宏任务的话,再重新开启宏任务……

再举个????

setTimeout(function() {console.log('a')
});

new Promise(function(resolve) {console.log('b');

    for(var i =0; i <10000; i++) {i ==99 && resolve();
    }
}).then(function() {console.log('c')
});

console.log('d');

// b
// d
// c
// a

1. 首先执行 script 下的宏任务,遇到setTimeout,将其放入宏任务的队列里。

2. 遇到 Promisenew Promise 直接执行,打印 b。

3. 遇到 then 方法,是微任务,将其放到微任务的队列里。

4. 遇到console.log('d'),直接打印。

5. 本轮宏任务执行完毕,查看微任务,发现 then 方法里的函数,打印 c。

6. 本轮 event loop 全部完成。

7. 下一轮循环,先执行宏任务,发现宏任务队列中有一个setTimeout,打印 a。

综上所述,不要说 a 是最持久的,如果你认为你彻底明白了,给你出道题,看看下面的代码中,谁最持久?

console.log('a');

setTimeout(function() {console.log('b');
    process.nextTick(function() {console.log('c');
    })
    new Promise(function(resolve) {console.log('d');
        resolve();}).then(function() {console.log('e')
    })
})
process.nextTick(function() {console.log('f');
})
new Promise(function(resolve) {console.log('g');
    resolve();}).then(function() {console.log('h')
})

setTimeout(function() {console.log('i');
    process.nextTick(function() {console.log('j');
    })
    new Promise(function(resolve) {console.log('k');
        resolve();}).then(function() {console.log('l')
    })
})


好,不要怂,我们来逐步分析。

第一轮事件循环:

1. 第一个宏任务 (整体 script) 进入主线程,console.log('a'),打印 a。

2. 遇到setTimeout,其回调函数进入宏任务队列,暂定义为setTimeout1

3. 遇到process.nextTick(),其回调函数被分发到微任务队列,暂定义为process1

4. 遇到 Promisenew Promise 直接执行,打印 g。then进入微任务队列,暂定义为then1

5. 遇到setTimeout,其回调函数进入宏任务队列,暂定义为setTimeout2

此时我们看一下两个任务队列中的情况

宏任务队列 微任务队列
setTimeout1、setTimeout2 process1、then1

第一轮宏任务执行完毕,打印出 a 和 g。

查找微任务队列中有 process1then1。全部执行,打印 f 和 h。

第一轮事件循环完毕,打印出 a、g、f 和 h。

第二轮事件循环:

1. 从 setTimeout1 宏任务开始,首先是console.lob('b'),打印 b。

2. 遇到process.nextTick(),进入微任务队列,暂定义为process2

3.new Promise直接执行,输出 d,then进入微任务队列,暂定义为then2

此时两个任务队列中

宏任务队列 微任务队列
setTimeout2 process2、then2

第二轮宏任务执行完毕,打印出 b 和 d。

查找微任务队列中有 process2then2。全部执行,打印 c 和 e。

第二轮事件循环完毕,打印出 b、d、c 和 e。

第三轮事件循环

1. 执行setTimeout2,遇到console.log('i'),打印 i。

2. 遇到process.nextTick(),进入微任务队列,暂定义为process3

3.new Promise直接执行,打印 k。

4.then进入微任务队列,暂定义为then3

此时两个任务队列中

宏任务队列:空

微任务队列:process3then3

第三轮宏任务执行完毕,打印出 i 和 k。

查找微任务队列中有 process3then3。全部执行,打印 j 和 l。

第三轮事件循环完毕,打印出 i、k、j 和 l。

到此为止,三轮事件循环完毕,最终输出结果为:

a、g、f、h、b、d、c、e、i、k、j、l

l 最持久,你答对了吗?

以上代码仅在浏览器环境中执行顺序如下,node环境下可能存在不同。

看完本文希望你能够理解 JavaScript 引擎的 Event Loop 执行机制,不仅可以让我们更加深刻的认识 JavaScript 这门语言,而且面试被问起的时候可以和面试官侃侃而谈。

交流

欢迎来我的个人公众号交流,优质原创文章将同步推送。后台回复 福利,即可领取福利,你懂得~

你的前端食堂,记得按时吃饭。

正文完
 0