一文搞懂EventLoop与堆栈,宿主环境之间的事件环机制

参考文章Tasks, microtasks, queues and schedulesjavascript

事件循环机制 Event Loop

首先让咱们来思考这样的一段代码html

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');
复制代码

相信不少程序员都能准确的答出执行顺序,若是不知道,test it!java

那么为何会这样呢?

要了解这一点,咱们必须理解事件循环如何处理任务和微任务程序员

ES6 规范中,microtask 称为 jobsmacrotask 称为 task, 本文中 task 指宏任务, microtask 指微任务。promise

js 引擎是单线程运行的,每一个“线程”都有本身的事件循环,所以每一个Web工做者都有本身的事件循环,所以能够独立执行,而同一源上的全部窗口均可以共享事件循环,由于它们能够同步通讯。 事件循环持续运行,执行全部排队的任务。 事件循环具备多个任务源,这些任务源保证了该源中的执行顺序,但浏览器能够在循环的每一个循环中选择从哪一个源执行任务。 这个选择的实现就是咱们的 事件循环机制核心。浏览器

计划了任务,以便浏览器能够从其内部切换 JavaScript / DOM领域,并确保这些操做顺序发生。 在任务之间,浏览器能够呈现更新。 从鼠标单击进入事件回调须要安排任务,解析HTML也是这样,在上面的示例中是setTimeout。markdown

setTimeout等待给定的延迟,而后为其回调安排新任务。 这就是为何在脚本结束以后打印 setTimeout 的缘由,由于打印 script end 是第一个 task 的一部分,而setTimeout被记录在单独的 task 中。浏览器线程在两个 task 之间执行 DOM 的更新渲染等宿主环境须要执行的任务。app

到这一步总结就是宿主环境在运行时的顺序是:dom

  • task ==> 宿主环境任务 ==> task ==> 宿主环境任务 ==> done

一般,微任务是当前正在执行的 task 发生的事情安排的,例如对一批动做作出反应,或使某些事情异步而不做为一个新 task ,仅仅做为一个小的反作用被立马实现。 只要当前 task 没有其余 JavaScript 在执行,微任务队列就会在回调以后进行处理,也就是在每一个任务结束时进行处理。 在微任务期间排队的任何其余微任务都将添加到队列的末尾并进行处理。 微任务包括变异观察者回调 MutaionObserverpromise 回调, 以及 process.nextTick(Node.js)异步

一旦 promise 得以解决,或者若是 promise 已经解决,它就会将微任务排队以进行反动回调。 这样能够确保即便promise已经解决,promise回调也是异步的。 所以,针对已解决的Promise调用.then(yey,nay)会当即将微任务排队。 这就是为何在脚本结束后记录 promise1promise2 的缘由,由于当前运行的脚本必须在处理微任务以前完成。 由于微任务老是在下一个任务以前发生,因此 promise1promise2setTimeout 以前记录

到这一步总结就是宿主环境在运行时的顺序是:

  • task ==> microtask ==> 宿主环境任务 ==> task ==> microtask ==> 宿主环境任务 ==> done

老是在下一个 task 以前把 上一个任务的反作用 microtask 执行完毕,而后执行宿主环境任务后开始下一个 task

一些浏览器的不同的处理

部分浏览器会打印 script start, script end, setTimeout, promise1, promise2.

他们在 setTimeout 以后运行 promise 回调。 他们可能将 promise 回调称为 task 的一部分,而不是微任务 microtask

promise 做为 task 的一部分会致使性能问题,由于回调可能会因与任务相关的宿主任务(例如渲染)而没必要要地延迟。 它还会因为与其余任务源的交互而致使不肯定性,并可能中断与其余API的交互。

ECMAScript具备相似于微任务的“做业”概念,广泛的共识是,应将 promise 做为微任务队列的一部分,这是有充分理由的。WebKit 内核始终都能正确的处理任务之间的关系。

一个更复杂的例子

让咱们来建立一个 HTML 文档,并思考一下的代码。

<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Untitled Document</title>
</head>

<body>
  <div class="outer" style="width: 200px;height: 200px;background: #888888;">
    <div class="inner" style="width: 160px;height: 160px;background: #444444;"></div>
  </div>
</body>

<script> // 获取两个元素 var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // 监听 outer 的元素属性变化 new MutationObserver(function () { console.log('mutate'); }).observe(outer, { attributes: true, }); // 点击回调 function onClick() { console.log('click'); setTimeout(function () { console.log('timeout'); }, 0); Promise.resolve().then(function () { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); </script>

</html>
复制代码

结合上面对 taskmicrotask 的分析,咱们很容易做出如下分析:

  • 点击回调是一项 task
  • promiseMutationObserver 回调做为 microtask 排队
  • setTimeout 做为一个新的 task 放到队列中。

因此是这么回事

// click
// promise
// mutate
// click
// promise
// mutate
// timeout
// timeout
复制代码

这里须要注意的,实际在于微任务在点击回调以后被处理,

这来自于 HTML 的规范

If the stack of script settings objects is now empty, perform a microtask checkpoint

— HTML: Cleaning up after a callback step 3

若是脚本执行堆栈被设置为空,请执行微任务检查点

一样,ECMAScript对做业 jobs (ECMAScript 将微任务称之为 jobs )进行说明:

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…

ECMAScript: Jobs and Job Queues

仅当没有正在运行的执行上下文而且执行上下文堆栈为空时才能够启动做业的执行。

这里的点击回调是浏览器处理的,实际被宿主环境在某个循环结束的时候,做为下一个 task 被推入堆栈执行。因此执行回调完毕,即出栈。进行对应微任务检查点。

有兴趣可使用浏览器控制台 debugger 回调执行时,堆栈只有 回调执行的堆栈。不存在其余执行堆栈。因此点击回调属于直接被宿主浏览器推入堆栈执行。

这与 ECMAScript 的规范上下文堆栈为空时才能被执行相呼应。

那么让咱们作点别的事情

在上面测试代码的 script 最后加入代码触发点击事件。并思考会有什么样的变化。

inner.click()
复制代码

那么能够看到的是执行输出:

// click
// click
// promise
// mutate
// promise
// timeout
// timeout
复制代码

根据上面提到的 HTML 和 ECMAScript 规范对 执行堆栈与微任务执行的描述

之前,微任务在侦听器回调之间运行,可是 .click() 致使事件同步分派,调用 .click() 的脚本仍在回调之间的堆栈中。 上面的规则确保微任务不会中断执行中的 JavaScript 。 这意味着咱们不在侦听器回调之间处理微任务队列,而是在两个侦听器以后对它们进行处理。咱们能够理解为,由于 .click() 的堆栈保留,致使宿主环境把两次的回调,做为一次 task 执行。

总结

  • 任务按顺序执行,浏览器能够在它们之间进行渲染
  • 微任务按顺序执行,并:
    • 在每次回调以后,只要没有其余JavaScript在执行中间
    • 在每一个任务结束时

流程分析

task ==> clear call stack ==> microtask ==> 宿主环境任务 ==> task

相关文章
相关标签/搜索