Javascript运行机制(Event loop)原理知道吗?不懂就来看看吧,一篇文章让你搞定
前言
在写这篇文章之前,我看了很多写的不错的文章,但是每篇文章都有那么几个关键的点,很多篇文章凑在一起综合来看,才可以对这些概念有较为深入的理解。所以,我就想要写这么一篇文章,结合自己的理解以及示例代码,用最通俗的文字表达出来。
希望大家可以通过这篇文章,学习到Event loop的运行原理。如果在文中出现有错误的地方,欢迎大家留言一起探讨。
正文
在开始之前,先理解一下三个概念:堆、栈、队列
堆(Heap)
堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆是线性数据结构,相当于一维数组,有唯一后继。
栈(Stack)
栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
栈是只能在某一端插入和删除的特殊线性表。
队列(Queue)
队列特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)
什么是Event loop?
event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
- 浏览器的Event Loop是在html5的规范中明确定义。
- NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
- libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商
在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
MacroTask(宏任务)
一些异步任务的回调会依次进入macro task queue(宏任务队列),等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
MicroTask(微任务)
另一些异步任务的回调会依次进入micro task queue(微任务队列),等待后续被调用,这些异步任务包括:
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
浏览器的Event loop
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
JS调用栈
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
同步任务和异步任务
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
任务队列Task Queue,即队列,是一种先进先出的一种数据结构。
事件循环的进程模型
- 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。
- 将事件循环中的任务设置为已选择任务。
- 执行任务。
- 将事件循环中当前运行任务设置为null。
- 将已经运行完成的任务从任务队列中删除。
- microtasks步骤:进入microtask检查点。
- 更新界面渲染。
- 返回第一步。
执行进入microtask检查点时,会执行以下步骤:
- 设置microtask检查点标志为true。
- 当事件循环microtask执行不为空时:选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtask为null,移出microtask中的microtask。
- 清理IndexDB事务
- 设置进入microtask检查点的标志为false。
可能上面说的比较抽象,那下面我们用一个例子理解一下
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
第一次执行
执行同步代码将宏任务(Tasks)和微任务(Microtasks)划分到各自队列中
打印log:script start
将setTimeout放入宏任务(Tasks)队列
将Promise.then放入到微任务(Microtask)队列
打印log:script end
打印结果:script start、script end
第二次执行
执行宏任务时检测到微任务(Microtasks)队列中不为空,去执行微任务
执行Promise1,执行Promise1完成后
调用Promise2.then
将Promise2.then放入微任务(Microtasks)队列中
再执行Promise2.then
打印结果:promise1、promise2
第三次执行
当微任务(Microtasks)队列中为空时,执行宏任务(Tasks)
执行 setTimeout callback
打印结果:setTimeout
最后一次执行
清空Tasks队列和JS stack
下面再来一个比较复杂的例子
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
解析之前,这里需要先理解async/await。
async/await 在底层转换成了 promise 和 then 回调函数。
也就是说,这是 promise 的语法糖。
每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。
async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。
现在对async/await有来初步认识,现在下面开始解析代码
- 首先,打印script start,调用async1()时,返回一个Promise,所以打印出来async2 end。
- 每个 await,会新产生一个promise,但这个过程本身是异步的,所以该await后面不会立即调用。
- 继续执行同步代码,打印Promise和script end,将then函数放入微任务队列中等待执行。
- 同步执行完成之后,检查微任务队列是否为null,然后按照先入先出规则,依次执行。
- 然后先执行打印promise1,此时then的回调函数返回undefinde,此时又有then的链式调用,又放入微任务队列中,再次打印promise2。
- 再回到await的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,打印async1 end。
- 当微任务队列为空时,执行宏任务,打印setTimeout。
扫码关注后,回复“资源”免费领取全套视频教程
3
发表评论(共0条评论)