[翻译JavaScript中的事件循环、任务、和微任务
[翻译JavaScript中的事件循环、任务、和微任务
原文地址:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
当我告诉我的同事 Matt Gaunt 我正在考虑在浏览器的事件循环中写一篇关于微任务队列和执行的文章时,他说“我跟你说实话,杰克,我不会读那个”。好吧,反正我已经写好了,所以我们都会坐在这里享受它,好吗?
实际上,如果您更喜欢视频,Philip Roberts 在 SConf 上就事件循环发表了精彩的演讲 - 微任务没有被涵盖,但它是对其余部分的一个很好的介绍。不管怎样,我们继续这个话题…
拿一点点 JavaScript 代码来说:
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');
控制台的日志应该以什么顺序出现?
小试牛刀
正确答案:script start、script end、promise1、promise2、setTimeout,但不同的浏览器会展示出不同的效果。
Microsoft Edge、Firefox 40、iOS Safari 和桌面 Safari 8.0.8 在 promise1 和 promise2 之前记录 setTimeout - 尽管它似乎是一个竞争条件。这真的很奇怪,因为 Firefox 39 和 Safari 8.0.7 一直都是正确的。
为什么这种情况发生了 Why this happens
要理解这一点,您需要知道事件循环如何处理任务(task)和微任务(microtask)。第一次遇到它时,这可能会让您大吃一惊。深呼吸…
每个“线程”都有自己的事件循环,因此每个网页行为都有自己的事件循环,因此它可以独立执行,而同一源上的所有窗口都共享一个事件循环,因为它们可以同步通信。事件循环持续运行,执行排在队列中的任务。每个事件循环都有属于他们自己的事件源(如单击按钮执行函数,则这个函数内的事件循环是独立出来的),但是浏览器可以在循环的每一轮中选择从哪个源中获取任务。这允许浏览器优先处理性能敏感的任务,例如用户输入。好吧好吧,我们继续……
如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
192.168.0.201:8088/index.html
和192.168.0.201:8088/login.html
就是同源
任务是经过调度的,因此浏览器可以从其内部进入 JavaScript/DOM 领域,并确保这些操作按顺序发生。在任务之间,浏览器可能会呈现更新。从鼠标点击到事件回调需要安排一个任务,解析 HTML 也是如此,在上面的例子中就是setTimeout。
setTimeout 等待给定的延迟,然后为其回调安排一个新任务。这就是为什么在脚本结束后记录 setTimeout 的原因,因为记录脚本结束是第一个任务的一部分,而 setTimeout 记录在一个单独的任务中。是的,我们快结束了,但我需要你在接下来的一段时间里保持坚强……
注意,setTimeout的任务和设置触发setTimeout的函数的任务本质上没任何区别 因此他们必须分道扬镳
微任务通常被安排在当前执行的脚本之后应该立即发生的事情上,例如对一批操作做出反应,或者在不承担全新任务损失的情况下使某些事情异步。只要没有其他 JavaScript 在执行中,并且在每个任务结束时,微任务队列就会在回调之后处理。在微任务期间排队的任何其他微任务都会添加到队列的末尾并进行处理。微任务包括变化观察者(Observer Watcher--Vue中大量使用的)的回调,还有如上例所示的Promise回调。
一旦 promise 成立,或者如果它已经成立,它就会将一个微任务排队等待它的宿主任务回调。这确保了Promise回调是异步的,即使Promise已处于解决状态(fullfilled)。因此,针对已解决的承诺调用 .then(yet, nay)
会立即将微任务排入队列。这就是为什么在脚本结束后记录 promise1 和 promise2 的原因,因为当前运行的脚本必须在处理微任务之前完成。 promise1 和 promise2 在 setTimeout 之前打印,因为微任务总是在下一个任务之前发生。
现在我们来重新审视一下先前的代码...
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');
是的,没错,我创建了一个动画分步图。你星期六是怎么度过的?和朋友出去晒太阳?嗯,我没有。嗯,如果从我惊人的 UI 设计中看不清楚,请单击上面的箭头前进。
这里建议回原文使用动画来跑一遍代码
为什么一些浏览器不是这样的(了解即可)
一些浏览器会在控制台打印Script Start、Script End、setTimeout、promise1、promise2。他们在 setTimeout 之后运行Promise回调。他们很可能将 Promise 回调作为新任务的一部分而不是微任务。
这有点情有可原,因为Promise来自 ECMAScript 而不是 HTML。 ECMAScript 具有类似于微任务的“作业”概念,但除了模糊的邮件列表讨论之外,这种关系并不明确。然而,普遍的共识是Promise应该是微任务队列的一部分,这是有充分理由的。
将 Promise 视为任务会导致性能问题,因为回调可能会被与任务相关的事情(例如渲染)不必要地延迟)。由于与其他任务源的交互,它还会导致不确定性,并且可能会中断与其他 API 的交互,我们稍后会详细讨论这个。
这是使用微任务进行Promise的 Edge 证据。 WebKit nightly 正在做正确的事情,所以我认为 Safari 最终会修复它,它似乎在 Firefox 43 中得到修复。
非常有趣的是,Safari 和 Firefox 在这里都出现了倒退问题,此后已修复。我想知道这是否只是巧合。
怎么分辨一些东西用了任务还是微任务
测试是一种方式。查看与 promises 和 setTimeout 相关的日志何时出现,尽管您很依赖正确的结果。
更好的方法是直接查找细节。例如,setTimeout 的第 14 步将任务排队,而将突变记录排队的第 5 步将微任务排队。
如前所述,在 ECMAScript 领域,他们将微任务称为“作业”。在 PerformPromiseThen 的步骤 8.a 中,调用 EnqueueJob 将微任务排队。
现在,我们来看看一个更复杂的例子。这时你心里有个声音会说: "不,我还没有准备好!"。别理他,你已经准备好了。我们来做这个...
最终决战
在写这篇文章之前,我已经弄错了。这是一些html:
<div class="outer">
<div class="inner"></div>
</div>
给定下面的 JS,如果我点击 div.inner 会打印什么?
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function () {
console.log('mutate');
}).observe(outer, {
attributes: true,
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function () {
console.log('timeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
继续,在查看答案之前先试一试。提示:日志可以发生不止一次。
答案
单击inner的时候
click
promise
mutate
click
promise
mutate
timeout
timeout
单击outer的时候
click
promise
mutate
timeout
你的猜测不一样吗?如果是这样,您可能仍然是对的。有的浏览器中可能效果不一致,以Chrome浏览器为标准答案
谁是对的? Who's Right
调度 'click' 事件是一项任务。变异观察者(Mutation Observer)和Promise回调被视为微任务并排队。 setTimeout 的回调被视为任务并排队。
这里建议回原文使用动画来跑一遍代码
在每一次侦听器回调结束后...
如果脚本设置对象堆栈现在为空,则执行微任务检查点
— HTML:回调步骤 3 后的清理
以前,这意味着微任务在侦听器回调之间运行,但 .click() 导致事件同步调度,因此调用 .click() 的脚本仍在回调之间的堆栈中。上述规则确保微任务不会中断正在执行的 JavaScript。这意味着我们不会在侦听器回调之间处理微任务队列,而是在两个侦听器之后处理它们。
这些重要吗
作者在这里聊了聊之前遇到的bug,建议大家用最新版本的浏览器调试,屁事没有
你做到了!
总结:
- 任务按顺序执行,但是浏览器可能会在两个任务之间重渲染网页
- 微任务按顺序执行,且在以下情况下执行:
- 每次回调函数结束后,只要没有JS代码在执行中
- 每次任务结束后
希望你现在按照自己的方式理解了事件循环(event loop),至少知道如何去做或者如何躺平。
补充说明和await的情况
一张图可以快速帮助你理解这些不同任务的执行情况:
- await/async执行的代码会怎么做?
遇到await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,继续执行await后面的代码。(好好理解一下上面的说明。也就是说,await只会阻塞当前async方法内的代码,不会影响外部代码的执行) - 相关案例
async function Say() {
console.log('Say Hello')
await Promise.resolve()
console.log('Say GoodBye')
}
setTimeout(() => {
console.log('timeout 1')
Promise.resolve().then(() => {
console.log('timeout 1 >> Promise 1')
})
}, 0)
setTimeout(() => {
console.log('timeout 2')
Promise.resolve().then(() => {
console.log('timeout 2 >> Promise 1')
})
}, 0)
Promise.resolve().then(() => {
console.log('Main >> Promise 1')
})
Promise.resolve().then(() => {
console.log('Main >> Promise 2')
})
Promise.resolve().then(() => {
console.log('Main >> Promise 3')
})
Say()
console.log('Main >> Bonjour!')
控制台输出结果:
Say Hello
index.vue:91 Main >> Bonjour!
index.vue:82 Main >> Promise 1
index.vue:85 Main >> Promise 2
index.vue:88 Main >> Promise 3
index.vue:67 Say GoodBye
index.vue:70 timeout 1
index.vue:72 timeout 1 >> Promise 1
index.vue:76 timeout 2
index.vue:78 timeout 2 >> Promise 1
对于上述过程来说,值得注意的是await阻塞了Say()方法的执行,导致先输出Main >> Bonjour!,而后继续返回Say()的任务队列继续执行后面的Say GoodBye
本文来自博客园,作者:Maji-May,转载请注明原文链接:https://www.cnblogs.com/caozhenfei/p/15261701.html
English Blog: http://flynncao.github.io/