概括
我们常说 Javascript
(下面简称JS
)是一门单线程编程语言,而单线程就意味着同一时刻只允许一个代码段在主线程上执行,那么对于执行一些需要长时间等待的任务来说,它们会占据线程不放,这会造成后续代码无法执行,程序无法正常使用。这是单线程的弊端,而 JS
是通过事件循环机制(Event Loop
)来解决这一弊端。
要理解清楚事件循环这个机制会涉及很多让人头疼的概念,如 JS
的执行机制、调用栈(执行栈)、任务队列(消息队列)、宏任务与微任务。当然,认识完这些东西,你对 JS
将会有更深层的认识,话不多说,我们开始本篇的愉快旅程吧。
栈、堆、队列
在讲正题之前,我们先来了解一下三个数据结构类型,相信很多人也不是很陌生了,之所以讲这个,是因为下面可能会涉及这其中的概念,希望你能有个更好的认识。
- 栈(Stack):栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶。栈被称为是一种后入先出(LIFO,last-in-first-out)的数据结构。由于栈具有后入先出的特点,所以任何不在栈顶的元素都无法访问。为了得到栈底的元素,必须先拿掉上面的元素。
- 队列(Queue):栈数据结构的访问规则是LIFO(后进先出),而队列数据结构的访问规则是FIFO(Fist-In-First-Out,先进先出)。队列在列表的末端添加项,从列表的前端移除项。
- 堆(Heap):堆是一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,我们只需要关心书的名字。
JS执行机制
我们知道 JS
的执行顺序是从上到下一行一行顺序执行的,但是如果在执行过程中碰到耗时比较长的任务要怎么办呢? JS
就只能傻等了吗? 当然没那么傻了,聪明的程序猿把任务分成了同步任务和异步任务,避免了单线程的 JS
在执行过程被阻塞的问题,下面我画了个草图来描述这一执行过程:
图解:
-
JS
在开始执行的时候,会把任务分为同步任务和异步任务。 -
同步任务会直接进入主线程依次执行。
-
异步任务会进入到 “任务队列” 中,等待异步任务有了结果后,会将注册的回调函数放入任务队列中等待,当主线程空闲的时候(执行栈被清空),会被读取到执行栈等待主线程的执行。
-
异步任务可以再分为宏任务和微任务。
宏任务与微任务
JS
把异步任务再分为宏任务和微任务。那么,它们俩又是什么呢?
- 宏任务(Macrotask):可以理解为每次执行栈执行的代码就是一个宏任务。(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
- 微任务(Microtask):可以理解为当前宏任务执行结束后立即执行的任务。也就是说,在当前宏任务后,下一个宏任务之前。微任务是在运行宏任务/同步任务的时候产生的,是属于当前任务的。
为什么会产生宏任务和微任务呢?
前面我们说过,异步任务会进入所谓的 “任务队列” 中了,而任务队列具有 队列 的性质,先进先出,也就是后加入的任务必须等待前面的任务执行完才能执行。如果在执行的过程中突然有重要的数据需要获取,或是说有事件突然需要处理一下,按照队列的先进先出顺序这些是无法得到及时处理的。这个时候就催生了宏任务和微任务,微任务使得一些异步任务得到及时的处理。
举个例子形容宏任务和微任务?
曾经看到的一个例子很好,宏任务和微任务形象的来说就是:你去营业厅办一个业务会有一个排队号码,当叫到你的号码的时候你去窗口办充值业务(宏任务执行),在你办理充值的时候你又想改个套餐(微任务),这个时候工作人员会直接帮你办,不可能让你重新排队。例子来源
产生宏任务和微任务分别有哪些?
-
宏任务:
script
(整体的代码)setTimeout
setInterval
I/O 操作
UI渲染
setImmediate
(Node.js 环境)
-
微任务:
Promise.then
Mutation Observer API
(具体使用)Process.nextTick
(Node独有)Object.observe
(废弃)
微任务与任务的区别?
-
首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。
-
其次,如果一个微任务通过调用
queueMicrotask()
, 向队列中加入了更多的微任务,则那些新加入的微任务 会早于下一个任务运行 。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。
事件循环
了解了 JS
的整个执行机制过程后,事件循环(Event Loop
)就比较简单好理解了,开头我们提过它的出现是为了解决 JS
单线程带来的弊端,它也是整个 JS
单线程执行过程中最核心的一部分,也是最重要的一部分。
讲事件循环前,还要涉及一个 执行栈(也称调用栈) 的概念。它又是什么呢?网上的说法是,所有同步任务都在主线程上执行,形成一个执行栈。详细解释
还是一样,我们先上图再分析:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个 “任务队列”,它是存放异步任务运行后的回调函数的,也就是异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦 “执行栈” 中的所有同步任务执行完毕,主线程就会读取 “任务队列”,看看里面有哪些事件。然后把那些对应的异步任务,压入执行栈中,开始执行。
而主线程不断重复上面的第三步,就形成了我们常说的事件循环了。
举个栗子
讲了那么多,举个栗子最实在,下面我们就来细细分析一下。
var p = new Promise((resolve, reject) => {
console.log('Promise - 初始化');
resolve('Promise - 结果')
})
function fn1() {
console.log('fn1 - 执行');
}
function fn2() {
console.log('fn2 - 开始执行');
setTimeout(() => {
console.log('setTimeout - 执行');
})
fn1();
console.log('fn2 - 再次执行');
p.then(res => {
console.log('Promise - 第一个then :' + res);
}).then(() => {
console.log('Promise - 第二个then');
})
}
fn2();
- 首先,从上到下依次执行,先是会把
Promise()
对象压入执行栈
中执行,输出 “Promise - 初始化” 并给p
赋值了一个Promise
对象,之后执行栈
把Promise()
对象弹出,也就是执行栈
清空了。 - 继续往下,不管两个函数的声明,来到
fn2()
的调用,把fn2()
压入栈中执行,输出 “fn2 - 开始执行”,继续把setTimeout()
压入栈中执行,会把它里面的console.log('setTimeout - 执行');
语句放入任务队列
中,弹出setTimeout()
,执行栈
中fn2()
继续调用。 - 往下,来到
fn1()
的调用,把fn1()
压入栈中执行,输出 “fn1 - 执行”,弹出fn1()
,往下,再次打印输出 “fn2 - 再次执行”。 - 往下,来到第一个
.then()
,把它压入栈中执行,会把它里面的console.log('Promise - 第一个then :' + res);
语句放入微任务队列
中,弹出它,再压入第二个.then()
,继续把console.log('Promise - 第二个then');
语句放入微任务队列
中, 弹出它。 - 到这里,
fn2()
就执行完了,会被执行栈
弹出,栈内又清空了。 - 同步任务都执行完了,主线程空闲了,开始读取
微任务队列
,按照队列先进先出的性质,会先把console.log('Promise - 第一个then :' + res);
语句压入执行栈
中执行,输出 “Promise - 第一个then :Promise - 结果” ,然后弹出,再压入另一语句,输出 “Promise - 第二个then” 弹出。 执行栈
又清空了,开始读取任务队列
,把console.log('setTimeout - 执行');
语句压入栈中执行,输出 “setTimeout - 执行”,然后弹出。
这就是整个执行过程了,文字有点多和乱,但仔细看应该能瞧明白的(-^〇^-) ,步骤中加黑文字对应下图的输出。
这上面的例子应该还比较好理解,但它还不是很能体现 Event Loop
的精髓,我们再来改造改造。
...
function fn2() {
console.log('fn2 - 开始执行');
setTimeout(() => {
console.log('setTimeout - 执行');
// start
setTimeout(() => {
console.log('又一个宏任务')
})
p.then(() => {
console.log('Promise - 第三个then')
})
// end
})
fn1();
console.log('fn2 - 再次执行');
p.then(res => {
console.log('Promise - 第一个then :' + res);
}).then(() => {
console.log('Promise - 第二个then')
})
}
fn2();
上面代码我们在一个宏任务中再增加了一个宏任务和一个微任务,然后我们直接来看输出结果:
是否符合你的预期呢?这里其实有个容易踩坑的点,既然 .then()
是一个微任务,我们新加的微任务,为什么它没有在上面提到的第 6 步骤中读取 微任务队列
的时候一起执行呢?原因很简单,就是下面红框的宏任务还没有执行。我们应该把它们看成一个整体,它们还没有细化。
之所以来细说这个点,主要是想表明,执行一个宏任务可能会继续产生宏任务和微任务,然后主线程来继续读取 微任务队列
和 任务队列
,以此来构成 Event Loop
的过程。
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!