前端知识梳理(二)Event Loop

Posted by southrock on 2019-12-03

前言

之前一直都知道,JavaScript是一门单线程的语言,知道在写js时要注意不要堵塞了页面渲染或是导致了页面假死,那么JavaScript内部到底是如何实现任务分发和执行并行操作的呢,今天来好好梳理一下。

为什么JavaScript是单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质

运行时的概念

首先先了解一下JavaScript是如何协调好执行中的任务和回调任务的。

这里引用MDN里面的一张图,其大体的解释了JavaScript运行的理论模型

这个模型中有三个块,分别是调用栈、堆和执行队列。

调用栈

类似于其他语言中的调用栈,每次调用一个函数时,会创建栈的一个帧,其中包含了函数内部的所有参数和局部变量,若是函数内部调用了另一个函数,则会新建一个帧,和前一个帧一样,被压入调用栈中。当函数返回时,则他的帧在最上层会被弹出。

JavaScript里创建的对象一般会被分配在一个堆中,其是一大块非结构化的内存区域。

任务队列

在JavaScript运行时,其会保存所有待处理的消息。每个消息都关联着一个用以处理这个消息的函数,一般是已经完成的异步操作。

在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

任务队列分为微任务队列和宏任务队列。

微任务和宏任务

宏任务:当前调用栈中执行的代码。

包含:

1
2
3
4
5
6
7
8
script                //整体代码
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate //Node.js 环境

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染。

微任务:在当前 task 执行结束后立即执行的任务。

包含:

1
2
3
4
Promise.then
Object.observe
MutaionObserver
process.nextTick //Node.js 环境

在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

Event Loop

回到一开始,我们知道JavaScript是单线程语言,不能同时处理多个任务,解释器是如何协调微任务和宏任务的执行的?

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

下图很好的描述了Event Loop的运行流程

主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。

具体表现

To be continue

Credits

并发模型与事件循环

JavaScript 运行机制详解:再谈Event Loop

javascript事件系统的发展史

微任务、宏任务与Event-Loop