浏览器的事件循环
JavaScript的任务分为两种同步和异步:
- 同步任务: 在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,
- 异步任务: 不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。
上面提到了任务队列和执行栈,下面就先来看看这两个概念。
(1)执行栈与任务队列
1)执行栈:从名字可以看出,执行栈使用到的是数据结构中的栈结构, 它是一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。 每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。以下图为例:
当执行这段代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。
JavaScript在按顺序执行执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出此方法,然后继续执行下一个方法。
2)任务队列: 从名字中可以看出,任务队列使用到的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。
JavaScript在执行代码时,会将同步的代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。
JavaScript任务的执行顺序如下:
在事件驱动的模式下,至少包含了一个执行循环来检测任务队列中是否有新任务。通过不断循环,去取出异步任务的回调来执行,这个过程就是事件循环,每一次循环就是一个事件周期。
(2)宏任务和微任务
任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列和宏任务(macro task)队列。常见的任务如下:
- 宏任务: script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
- 微任务: Promise、MutaionObserver、process.nextTick(Node.js 环境);
任务队列执行顺序如下:
可以看到,Eventloop 在处理宏任务和微任务的逻辑时的执行情况如下:
- JavaScript 引擎首先从宏任务队列中取出第一个任务;
- 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行,也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
- 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。
也是就是说,一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。
以上内容引用自 juejin.cn/post/699216… 彻底搞懂JavaScript事件循环
个人语言通俗理解
JavaScript的的异步任务分为宏任务和微任务两种。
时间执行的顺序:
整个Script入执行栈(或者说整个都是一个宏任务),执行栈执行完之后,开始执行微任务队列,在此期间,即使微任务又产生了微任务,也会在当前循环中继续执行下去,直到微任务队列执行结束,把下一个宏任务推入执行栈中。
一道有意思的题目
相同的代码,通过手动鼠标点击按钮触发click事件,输出顺序如图。
通过btn.click()直接在代码中调用click事件,输出顺序为
1 2 microtask1 microtask2
原因是通过btn.click()调用的时候会把整个代码块打包执行,因此先执行console.log同步任务,然后再执行两个微任务。