回调函数是否总会晚于触发后的代码执行?以loadScriptAsync场景为例
问题:func_a 是否总会在 func_b() 执行完成后才运行?
场景代码
loadScriptAsync('test.js', func_a); func_b();
loadScriptAsync 实现
function loadScriptAsync(l, cb, _importance = false, _crossOrigin = false, _async = true) { var d = document, s = 'script', w = window; function go(){ var js = d.createElement(s); js.onload = cb; if (_async) js.async = "true"; if (_importance) js.fetchpriority = _importance; if (_crossOrigin) js.crossOrigin = 'anonymous'; js.src = l; try{d.head.appendChild(js)}catch(e){ document.documentElement.appendChild(js); }; } if (w.addEventListener) { w.addEventListener("load", go, false); } else if (w.attachEvent) { w.attachEvent("onload", go); } else { go() } };
问题描述
loadScriptAsync 的作用是向 <head> 中插入 <script> 标签,并在脚本加载完成后调用传入的回调函数 func_a。请问:func_a 是否总会在 func_b() 执行完成后才运行?比如当脚本从本地主机加载极快,或者 func_b 执行耗时很长的情况,想了解背后的 JavaScript 内部运行机制。
回答
结论是:func_a 一定会在 func_b() 执行完成后才运行,不管脚本加载速度有多快,或者 func_b 执行耗时多久。核心原因要从 JavaScript 的单线程事件循环机制说起:
1. JavaScript 的核心运行规则
JavaScript 是单线程语言,所有代码都在一个执行栈里按顺序执行。只有当当前执行栈里的所有同步代码都执行完毕后,才会去处理任务队列里的异步回调(比如事件监听、网络请求完成、脚本加载完成这类回调)。
2. 结合 loadScriptAsync 的两种执行路径分析
路径一:浏览器支持 addEventListener/attachEvent
这种情况下,loadScriptAsync 会把 go 函数注册为 window.load 事件的回调。window.load 事件要等页面所有资源(包括图片、样式表、脚本等)加载完成,并且当前所有同步代码执行完毕后才会触发。
流程是:
- 先执行
loadScriptAsync函数,注册window.load回调; - 紧接着同步执行
func_b(),直到它完全执行完毕; - 等页面所有资源加载完成,才会触发
window.load,执行go函数插入脚本; - 脚本加载完成后,
func_a作为异步回调被放入任务队列,等执行栈为空时才会执行。
显然,func_a 不可能在 func_b 之前运行。
路径二:浏览器不支持事件监听,直接执行 go 函数
这种情况下,go 函数会立即执行,插入带有 async="true" 属性的脚本标签。async 脚本的特点是:
- 加载过程是异步的,不会阻塞当前同步代码;
- 加载完成后,不会立即执行回调,而是把回调放入任务队列,必须等当前执行栈里的所有同步代码(包括
func_b())执行完,才会被取出执行。
哪怕脚本从本地主机瞬间加载完成,它的 onload 回调 func_a 也会被挂起,直到 func_b() 执行完毕、当前执行栈为空时才会运行。
3. 极端场景验证
- 脚本加载极快:哪怕脚本在
func_b()执行过程中就加载完成,func_a作为异步回调也只能进入任务队列排队,等func_b()执行完、执行栈清空后才会触发。 - func_b 执行耗时很长:
func_b()会占用执行栈直到完成,期间所有异步回调(包括func_a)都无法执行,必须等func_b()执行结束才行。
内容的提问来源于stack exchange,提问作者AGamePlayer
相关产品推荐
相关产品推荐

