JavaScript作业队列和微任务
当Promises在ES6中首次引入时,它们使编写异步代码的工作变得更加容易。回调地狱被更简单的构造所取代,该构造使开发人员可以更轻松地处理异步任务。理解诺言的关键是知道作业队列(也称为微任务队列)如何在JavaScript中工作。
我们将从看一些代码开始:
function firstFunction() {
thirdFunction()
const firstResponse = Promise.resolve('1st Promise');
const secondResponse = Promise.resolve('2nd Promise');
setTimeout(() => {
firstResponse.then(res=> {
console.log(res);
})
})
secondResponse.then(res=> {
console.log(res);
})
}
function thirdFunction() {
const thirdResponse = Promise.resolve('3rd Promise');
const fourthResponse = Promise.resolve('4th Promise');
queueMicrotask(() => {
console.log('Hello from the microtask queue')
})
thirdResponse.then(res=> {
console.log(res);
})
setTimeout(() => {
fourthResponse.then(res=> {
console.log(res);
})
})
}
function secondFunction() {
let i = 0;
let start = Date.now();
for (let j = 0; j < 5.e9; j++) {
i++;
}
console.log("Loop done in " + (Date.now() - start) + 'ms');
}
setTimeout(() => {
console.log('first timeout')
});
firstFunction()
secondFunction()
console.log('first console log')
我们期望日志以什么顺序出现?
进入事件循环
得知ECMAScript规范未提及事件循环,可能令人惊讶。相反,事件循环是指浏览器的JavaScript引擎处理代码的方式。JavaScript在单线程模型上运行,因此在任何时候都只能处理一项任务。这显然会导致并发症。如果mouseover
在计时器启动的计时器即将setTimeout
到期之前触发事件,会发生什么情况?或者,如果您触发了网络请求,并且响应出现在浏览器中间,则重新呈现了UI?
下图显示了浏览器的不同部分,它们可以协同工作来管理这种异步性。
事件循环以迭代或“滴答”的形式执行其工作。JavaScript代码以运行到完成的方式执行(当前任务总是在下一个任务执行之前完成),因此,每次任务完成时,事件循环都会检查事件是否将控制权还给其他代码。如果不是,它将运行作业队列中的所有任务,然后运行任务队列中的任务。我们可以通过将其应用于示例代码来更好地说明这一点。
// ... firstFunction, secondFunction and thirdFunction declarations have been omitted for brevity
setTimeout(() => {
console.log('first timeout')
});
firstFunction()
secondFunction()
console.log('first console log')
当firstFunction
执行时,浏览器的内部状态为:
如果setTimeout
在没有指定持续时间的情况下调用,则默认为0毫秒。setTimeout
本身是一个浏览器API,因此它不会出现在调用堆栈中,但它返回的回调将放入任务队列中,以备将来事件循环迭代时调用。
首先要做的firstFunction
是call thirdFunction
,它看起来像:
function thirdFunction() {
const thirdResponse = Promise.resolve('3rd Promise');
const fourthResponse = Promise.resolve('4th Promise');
queueMicrotask(() => {
console.log('Hello from the microtask queue')
})
thirdResponse.then(res=> {
console.log(res);
})
setTimeout(() => {
fourthResponse.then(res=> {
console.log(res);
})
})
}
这就是事情变得有趣的地方。在上面的代码中,我们解析了两个promise,并为其分配了解析值。使用then
每个promise的方法,我们指定应在结算后运行的函数。已解决的承诺是从pending
(在执行诸如获取数据之类的基础过程时)迁移到fulfilled
(成功)或rejected
(错误)的承诺。稳定后,它将微任务排队以进行回调。queueMicrotask
是不言自明的。这是排队微任务的更直接方法。
一旦thirdFunction
完成执行,控制权将交还给firstFunction
它,同时完成其代码的运行。之后,浏览器的内部状态为:
值得注意的是,尽管运行了两个函数,我们的程序在这一点上还没有做任何事情。当使用异步代码时,这些细微差别可能会使开发人员感到困惑,尤其是当您考虑到我们的代码的下一行secondFunction
通过运行持续几秒钟的循环来模仿阻塞代码的行为时。尽管队列中有六个回调,但是console.log
in语句secondFunction
是打印到控制台的第一件事,其后是脚本的最后一行,这是另一条日志语句。
在此阶段,事件循环到达其当前迭代的末尾,因此它查找作业队列并以先进先出的方式在该队列中运行回调。作业队列中的代码有可能安排更多的回调。但是,这些不会推迟到以后的迭代中,而是会在当前迭代中运行,这意味着可以通过创建作业队列回调的无穷循环来使程序饿死。在第一次迭代结束时,以下内容将被记录到控制台:
Loop done in 5672ms
first console log
Hello from the microtask queue
3rd Promise
2nd Promise
浏览器的内部状态为:
您会注意到,第一个和第四个Promise的回调从未放入作业队列。这是因为我们没有then
直接调用它们,而是将它们放在setTimeout
函数的回调中。因此,当事件循环开始其第二次迭代时,它将首先查看任务队列。setTimeout
我们的Promise的回调将每个排队另一个作业队列的回调,该回调将在当前迭代结束时运行。这意味着当我们的程序完成运行时,以下是将事物记录到控制台的顺序。如果第二次迭代中的任何代码在任务队列中排队了更多的东西,则它将在以后的迭代中运行。
Loop done in 5672ms
first console log
Hello from the microtask queue
3rd Promise
2nd Promise
first timeout
4th Promise
1st Promise
注意:本文归作者所有,未经作者允许,不得转载