原文链接 medium.com/@Rahulx1/un…
在本文中,我们将深入探讨 javascript 的工作原理,它如何执行我们的异步 javascript 代码,Promise 与 setTimeout 的执行顺序,它如何生成堆栈跟踪等等。
大多数开发人员都知道,Javascript 是单线程的,这意味着 javascript 中的两条语句不能并行执行。只能逐行执行,这意味着每个 javascript 语句都是同步和阻塞的。 但是有一种方法可以异步运行您的代码,如果您使用 setTimeout() 函数,浏览器提供的 Web API,它可以确保您的代码在指定时间(以毫秒为单位)后执行。 示例代码:
console.log('Message 1');
setTimeout(function() {
console.log('Message 2');
}, 100);
console.log('Message 3');
setTimeout 第一个参数是回调函数,第二个参数是以毫秒为单位的时间。还有两个可选的参数,具体可参阅 MDN。
执行完上述语句后,浏览器会先打印“Message 1”和“Message 3”,然后再打印“Message 2”。 这就是事件循环的用武之地,它确保您的异步代码在所有同步代码执行完毕后运行。
Event Loop 可视化
Stack: 这是当解析器读取您的程序时,您的所有 javascript 代码都被一一推送和执行的地方,并在执行完成后弹出。 如果您的语句是异步的:包含 setTimeout、ajax()、promise 或 click 事件,那么该代码将被转发到 Event 表,该表负责在指定时间后将您的异步代码移动到回调/事件队列。
Heap: 这是您在程序中定义的变量所有内存分配发生的地方。
Callback Queue: 这是您的异步代码被推送到并等待执行的地方。
Event Loop: 事件循环,如果有任何帧要执行,它持续运行并检查主堆栈;如果没有,那么它会去检查回调队列,如果回调队列中有要执行的代码,那么它会将消息从它里面弹出到主堆栈去执行。
Job Queue: 除了回调队列之外,浏览器还引入了另一个队列,即 “Job Queue”,它仅为新的 Promise() 功能保留。当你的代码中使用了 promise 并添加了 .then() 方法,它是一个回调方法。 一旦promise returned/resolve,这些thenable方法就会被添加到作业队列中,然后被执行。
看下面的代码示例,你能预测输出的顺序吗?
console.log('Message no. 1: Sync');
setTimeout(function() {
console.log('Message no. 2: setTimeout');
}, 0);
var promise = new Promise(function(resolve, reject) {
resolve();
});
promise.then(function(resolve) {
console.log('Message no. 3: 1st Promise');
})
.then(function(resolve) {
console.log('Message no. 4: 2nd Promise');
});
console.log('Message no. 5: Sync');
提示:
promise的所有thenable回调先被调用,然后 setTimeout 回调被调用。
正确的输入结果如下:
// Message no. 1: Sync
// Message no. 5: Sync
// Message no. 3: 1st Promise
// Message no. 4: 2nd Promise
// Message no. 2: setTimeout
Why? : Job Queue 优先执行回调,如果事件循环 tick 进入 Job Queue,它会先执行 Job Queue 中的所有job,直到它为空,然后才会移动到回调队列。
如果您想深入了解为什么在 setTimeout 之前调用 promises,您可以查看 Jake Archibald 撰写的这篇文章任务、微任务、队列和调度, 有更详细和容易理解的说明。
代码执行注意事项
- 您的异步代码将在“主堆栈”执行完所有任务后运行。
- 堆栈中的当前语句/函数将运行完成之前,异步代码不能中断它们。一旦您的异步代码准备好待执行,它将等待主堆栈为空再执行。
- 这也意味着不能保证您的 setTimeout() 或任何其他异步代码将在您指定的时间之后运行。 该时间是您的代码将执行的最短时间,如果主堆栈忙于执行现有代码,则可能会延迟
在下面的示例中,第一个输出将是“Message 2”,然后是“Message 1”,即使 setTimeout 设置为在 0 毫秒后运行,但因主线程繁忙,并不能立即执行。浏览器遇到 setTimeout,会将它从主堆栈弹出到回调队列,在那里等待主堆栈完成第二个 console.log,然后 setTimeout 返回到主堆栈,并运行第一个 console.log。
setTimeout(function() {
console.log('Message 1')
}, 0);
console.log('Message 2');
如果你做了太多繁重的计算,那么它会使浏览器无响应,因为你的主线程被阻塞,无法处理任何其他任务。 因此用户将无法在您的网页上进行任何点击。 那时浏览器会抛出“脚本执行时间过长”错误,并为您提供“终止脚本”或“等待”选项。
[可选] 错误堆栈跟踪
我们已经知道,如果解释器遇到一个函数,那么这个函数就会被压入堆栈,现在如果这个函数调用另一个函数,那么这个函数调用也会被压入堆栈顶部,这个链一直持续到一个函数 执行完成或返回某些内容,然后仅将其从堆栈中删除并且上下文返回到调用最后一个函数的函数,然后执行继续。
这个函数调用堆栈有助于浏览器为您提供特定函数中发生的错误的堆栈跟踪。 例如:
function func1 () {
// 访问未定义的变量会抛出错误
console.log(err);
}
function func2 () {
func1();
}
function func3 () {
func2()
}
// 调用func3,会导致func1出错
func3();
正如您在错误堆栈跟踪中看到的,错误发生在 func1 函数中,该函数在第 1 行调用。第三行代码是console的位置,第7行代码在 func2 中,然后第11行代码 func2 在 func3 中被调用。
什么时候使用事件循环?
- 当您需要进行繁重的计算时,不需要按顺序运行,这意味着可以在没有它的情况下执行下一条语句。 在这种情况下,不会因为该计算而阻塞主线程。
- 当你想在所有其他语句/函数都执行完之后,最后执行你的代码时。