加载中...

Javascript运行机制(Event loop)原理知道吗?不懂就来看看吧,一篇文章让你搞定

吴佳
2020-04-29 18:31:21
分类:JavaScript
538
1
0
关注微信公众号前端技术专栏 ,回复“资源”免费领取全套视频教程

前言

在写这篇文章之前,我看了很多写的不错的文章,但是每篇文章都有那么几个关键的点,很多篇文章凑在一起综合来看,才可以对这些概念有较为深入的理解。所以,我就想要写这么一篇文章,结合自己的理解以及示例代码,用最通俗的文字表达出来。
希望大家可以通过这篇文章,学习到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单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

吴佳前端博客 event loop

任务队列Task Queue,即队列,是一种先进先出的一种数据结构。

吴佳前端博客 event loop1

事件循环的进程模型

  • 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即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有来初步认识,现在下面开始解析代码

  1. 首先,打印script start,调用async1()时,返回一个Promise,所以打印出来async2 end。
  2. 每个 await,会新产生一个promise,但这个过程本身是异步的,所以该await后面不会立即调用。
  3. 继续执行同步代码,打印Promise和script end,将then函数放入微任务队列中等待执行。
  4. 同步执行完成之后,检查微任务队列是否为null,然后按照先入先出规则,依次执行。
  5. 然后先执行打印promise1,此时then的回调函数返回undefinde,此时又有then的链式调用,又放入微任务队列中,再次打印promise2。
  6. 再回到await的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,打印async1 end。
  7. 当微任务队列为空时,执行宏任务,打印setTimeout。

扫码关注后,回复“资源”免费领取全套视频教程

前端技术专栏

1

发表评论(共0条评论)

请输入评论内容
啊哦,暂无评论数据~