本文详细解析JavaScript的单线程执行模型,阐述同步任务与异步任务的执行差异。通过深入剖析事件循环(Event Loop)机制,说明宏任务与微任务在调用栈与任务队列中的调度顺序,并结合具体代码示例展示输出结果,帮助开发者掌握JS异步编程的核心原理。
本文通过具体实例深入解析JavaScript的运行机制,旨在为读者提供清晰的参考与学习思路。
首次发布博客文章
笔者目前处于研一第二学期,自大二接触前端开发以来,长期局限于框架的简单调用,缺乏对底层原理的深入探究。为此,建立此博客以自我督促。内容源于近期开发微信小程序时遇到的页面渲染顺序与预期不符的问题,通过查阅相关文档与博客,梳理出JavaScript运行机制的基本框架,下文将展开详细论述。
JavaScript的执行顺序
JavaScript采用单线程模型,代码按行依次执行。由于不存在多线程并发执行的情况,当前代码执行期间,后续所有代码必须等待其执行完毕。这种机制若处理不当,例如发起服务器请求后阻塞后续代码,将导致用户界面长时间无响应,严重影响用户体验。
这种阻塞现象背后的原因是什么?
深入解析JavaScript的执行机制
JavaScript执行栈
JavaScript通过栈结构管理代码执行。初始状态下栈为空,引擎将代码压入栈底开始执行。若代码中包含函数调用,被调用的函数会被压入栈顶;若无非嵌套调用,则直接执行当前函数。执行完毕后函数出栈,重复此过程直至栈为空。
同步任务与异步任务的区分
尽管JavaScript内部区分了同步与异步任务,但这并未改变其单线程运行的本质特征。
- 同步任务:执行后直接返回结果,例如 console.log() 或 c = a + b。
- 异步任务:执行后不能立即返回结果,需等待特定时间或事件触发后,再执行回调函数以处理返回结果。
具体而言,JavaScript存在一个主线程。主线程优先执行所有同步任务,而异步任务则先进行注册。主线程不会等待异步任务的结果,而是继续执行后续的同步代码。在此过程中,若异步任务完成,其回调函数会被放入Event Queue(事件队列)中等待。待所有同步任务执行完毕后,主线程会从Event Queue中读取任务并执行。这一过程不断循环,即事件循环(Event Loop)。
事件循环的发生机制
有人可能会疑惑:若按上述同步与异步任务的执行方式,一轮执行即可结束,为何还需要Event Loop?
关键在于细节:当存在多个异步任务时,每个任务返回结果所需的时间各不相同。因此,Event Queue以先进先出(FIFO)的形式对返回结果进行排队。第一个异步任务完成后,其回调被置于队列首位,后续任务依次排列。当主线程空闲(即同步任务执行完毕)时,便从Event Queue中读取事件放入主线程执行。循环之所以产生,是因为当Event Queue执行完毕后,过一段时间又会有之前的异步任务返回结果并加入Event Queue。监控器检测到Event Queue非空时,主线程再次开始执行队列中的任务。
宏任务与微任务
在解释具体定义之前,我们先对常见的异步任务类型进行说明:
- 对服务器的异步请求:最常见的异步任务,涉及前后端交互,需服务器处理请求并返回结果。
- setTimeout和setInterval:延时操作,后者为循环操作(均涉及延时值设定)。
- Promise:JavaScript用于处理异步操作的对象。
- process.nextTick(callback):类似于Node.js版的"setTimeout",在事件循环的下一次循环中调用回调函数。
广义上JavaScript分为同步任务和异步任务,在此基础上,对任务进行更精细的定义如下:
- macro-task(宏任务):包括整体代码script、setTimeout、setInterval。
- micro-task(微任务):包括Promise、process.nextTick。
提出宏任务与微任务的概念,旨在更清晰地理解事件循环的工作流程。
具体执行过程如下:
- 主线程按顺序先执行第一次循环的宏任务,然后将该次循环中遇到的微任务放入微任务的Event Queue,将遇到的宏任务放入宏任务的Event Queue。需特别注意:第一次循环的宏任务即为整体script代码。
- 随后执行微任务的Event Queue中的任务。
- 在第二次循环时,从宏任务的Event Queue中取出第一个宏任务,执行当前宏任务包含的代码,同样将遇到的微任务放入微任务的Event Queue,将遇到的宏任务放入宏任务的Event Queue。
- 接着执行当前微任务的Event Queue中的任务。
- 第三次循环,从宏任务的Event Queue中取出第二个宏任务……(以此类推循环)。
简而言之,执行顺序为先宏任务后微任务,需特别注意以下两点:
- 第一次循环的宏任务是整体script代码。
- 宏任务队列是一次循环只执行一条宏任务。
以下通过一个具体示例进行验证:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
//作者:ssssyoki
//链接:https://juejin.im/post/59e85eebf265da430d571f89
//来源:掘金
代码的输出顺序为:
1,7,6,8,2,4,3,5,9,11,10,12
总结
本文可能包含部分未详细解释的名词或方法。作者认为,遇到不懂的概念时,自行查阅和理解比直接获取答案更有助于记忆和透彻理解,这类似于查单词的过程。直接给出答案反而容易让人忽视其重要性。
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码的运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript操作DOM技巧总结》、《JavaScript页面元素操作技巧总结》、《JavaScript事件相关操作与技巧大全》、《JavaScript查找算法技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript错误与调试技巧总结》。
希望本文所述内容能对大家的JavaScript程序设计有所帮助。