本文通过四个具体代码示例,深入解析 JavaScript 的单线程执行模型与异步任务处理机制。文章详细阐述了同步代码优先执行、异步任务进入队列以及定时触发后移入同步队列的执行顺序。针对循环中 setTimeout 输出相同值的常见问题,分别展示了利用闭包缓存变量和使用 ES6 let 块级作用域两种解决方案,帮助开发者理解变量作用域对异步回调的影响。
JavaScript 运行机制概述
前言
JavaScript 采用单线程模型,其执行流程具有独特的异步处理特性。
接下来,我们将通过几个关于 setTimeout 的具体案例,来揭示 JavaScript 运行机制中的关键细节。
示例1
同步与异步任务处理流程
console.log(1);
setTimeout(function(){
console.log(2);
},0);
console.log(3);
// 打印出 1 3 2
示例2
console.log('1');
setTimeout(function(){
console.log('2');
},0);
while(1){}
// 打印出1,然后浏览器卡死,不会打印出2
JavaScript 引擎会将待执行的内容放入任务队列中进行管理。
当遇到 setTimeout 时,系统将其识别为异步任务,并优先将其放入异步队列等待处理。
浏览器引擎会优先执行当前的同步任务,只有在同步任务完全执行完毕后,才会去检查异步队列的状态。
一旦异步队列中的任务到达设定的执行时机,浏览器便会将该任务移入同步队列等待执行。
即:
异步任务的执行时机一定晚于同步任务。
示例3
循环中 setTimeout 的变量共享问题
for(var i = 0; i < 4; i++){
setTimeout(function() {
console.log(i);
}, 1000);
}
// 打印 4 4 4 4
为什么最终打印结果是 4 4 4 4 呢?
这是因为浏览器首先执行 for 循环逻辑。
在每次 for 循环迭代中,都会将一个 setTimeout 回调函数压入异步队列。
当 1000 毫秒过去,执行 setTimeout 内的方法时,变量 i 的值已经递增为 4。
如果要实现打印 0 1 2 3 的效果,该如何处理?
可以利用闭包特性,将变量 i 缓存到一个临时变量 temp 中。
利用闭包解决变量捕获
for(var i = 0; i < 4; i++){
(function(temp){
setTimeout(function() {
console.log(temp);
}, 1000);
})(i);
}
// 打印 0 1 2 3
这种做法相当于在每次 for 循环中创建一个新的匿名函数,并将当前的 i 值存储在该匿名函数的内存空间中。
理解闭包概念的读者应该能轻松掌握这一原理。
示例4
我们知道 ES6 引入了新的关键字 let。
在这里,let 具备一项新的作用域特性。
ES6 let 块级作用域的应用
for(let i = 0; i < 4; i ++){
setTimeout(function(){
console.log(i);
}, 1000);
}
// 打印 0 1 2 3
示例4与示例3仅在 var 和 let 的使用上存在差异,但打印结果却截然不同。
这是因为 let 声明了块级作用域。
使用 let 定义的 i,对于每一次 for 循环的执行而言都是全新的变量(拥有独立的内存地址)。
因此,在打印时可以得到预期的 0 1 2 3 结果。
总结
以上就是本文的全部内容,希望这些内容对您的学习或工作有所帮助,感谢支持。