关于 JavaScript 的加载执行顺序

在页上有两个script标签,第一个script标签内动态创建了一个script,去加载一个config.js文件,第二个script标签内,需要用到动态加载的config。如下:

config.js

  window.config={
    appName:'name',
    appId:'appId'
  }

index.html

//第一个script
<script type="text/javascript">
  (function () {
    var script=document.createElement('script');

    //设置为同步加载
    script.async=false;
    script.src='config.js';

    document.head.appendChild(script);
    //此时head内有了这个script标签,
    //它会去加载config.js
    //因为设置的是同步;async=false;
    //此时程序应该停在此处,等待config.js的加载执行
  })();
</script>

//第二个script
<script>
  (function () {
    var config=window.config;

    //js的加载执行是同步的,
    //按理说,程序执到这里时,动态加载的config.js文件已经加载执行完毕
    //window.config应该是有数据的
    //但实际上此时config为undefined
    //也就是说动态加载的js文件,并没有阻塞其后的代码执行
    //这是为什么??
    console.log(config);//undefined
  })();
</script>

为什么在第二个script标签内读取window.config时,却是undefined??

阅读 13.3k
6 个回答

啊,那纯粹是因为下载的异步性

如果用 script.text 就没这问题了

至于为什么 document.write 可以,那是因为浏览器会选择下载完再继续解析 HTML

现代浏览器支持了对script的async``和defer`属性的支持,提高浏览器对HTML页面的解析速度和渲染呈现的速度。这2个属性只对设置了scr属性的外部脚本有效,对内联脚本(没有设置src属性的script)无效。
没有设置asyncdefer属性的脚本按其在html中出现的顺序阻塞式的执行-也就是浏览器会暂停对html页面元素的解析,等script脚本执行完毕后才能继续解析,在解析完成后触发DOMContentLoaded事件。
设置有async/defer属性的script不会阻塞对HTML标签的解析,HTML标签的解析继续进行;设置了async的script将会并发异步的去下载对应的外部脚本文件(可能从本地缓存中获取),下载完成后,通过事件通知机制通知浏览器可以执行进程,事件调度器会调度执行脚本,按下载完成的时间的先后顺序,依次执行。
defer的script将会并发异步的去下载对应的外部脚本文件(可能从本地缓存中获取),下载完成后不会马上执行,要等到HTML解析完成后,按脚本出现的顺序依次执行,这个过程中多个脚本之间是顺序执行。
asyncdefer同时指定的情况下,以defer的方式处理script的执行

<script>脚本执行是在html解析过程中发生的,只有在在脚本执行了document.write操作时,页面解析器将会解析其中的html内容如果有脚本并马上执行脚本~~

//writDoc.js
document.open();
document.write("<script src='config.js'></script><h1>Out with the old - in with the new!</h1>");
document.close();


//html
<!DOCTYPE html>
<html>
<head>
    <script src="writeDoc.js"></script>
    <script>
        console.log(window.config);
    </script>
</head>

<body>
<p>Some original document content.</p>
</body>
</html>

而script脚本有2种类型:
parser-inserted scripts 我们经常遇到的出现在HTML中以<script>方式出现,我们姑且称之为解析型脚本

<script src="config.js"></script>
<script>
 console.log(window.config);
</script>

script-inserted scripts 通过JS代码动态添加的script脚本,动态型脚本,不涉及页面标签的解析

var script=document.createElement('script');
//设置为同步加载
script.async=false;
script.src='config.js';
document.head.appendChild(script);

这2种类型的script脚本,在async方式的处理方式有些差异
动态型脚本在插入到DOM中后,即使马上从DOM中删除,也不影响脚本的存在:
A: 如果没有设置async属性并设置src属性,那么JS解析器就其当做async=true处理,此脚本将异步加载处理;

//config.js
window.config={
    appName:'name',
    appId:'appId'
}

//第一个script
<script type="text/javascript">
  (function () {
    var script=document.createElement('script');
    script.src='config.js';
    document.head.appendChild(script);
    document.head.removeChild(script);//可以马上从DOM中删除刚插入的script元素,但是浏览器依然认为其存在,依然会去加载执行
  })();
</script>

//第二个script
<script>
  (function () {
    var config=window.config;
    console.log(config);//因为config.js下载执行事件不可预知,所以结果不可预知
  })();

//获取设置一定的超时或许可以:)
setTimeout(function(){
    var config=window.config;
    console.log(config);
},100);

</script>

B: 如果没有设置src属性,那么无论是否设置async属性,通过为其text属性设置脚本代码的方法,那么动态添加的脚本被马上执行-可以认为是当前脚本一部分(实际不是,作用域不同);这个也是jquer的ajax加载执行外部JS脚本的方式。

//第一个script
<script type="text/javascript">
  (function () {
    var script=document.createElement('script');
    //内联脚本
    script.text="window.config={"+
            "appName:'name',"+
            "appId:'appId'"+
            "};";
    document.head.appendChild(script);
    document.head.removeChild(script);//可以马上从DOM中删除刚插入的script元素,但是浏览器依然认为其存在,依然会去加载执行
  })();
</script>

//第二个script
<script>
  (function () {
    var config=window.config;
    console.log(config);//正确输出你期望的值
  })();

</script>

c: 如果设置src属性并设置async=false,那么次动态脚本将被同步化处理,但是其执行时机不是暂停当前脚本的执行,而是等当前页面的解析工作完成后。多个async=false脚本按其插入的次序顺序执行。

//config2.js
var config=window.config;
console.log(config);//

//第一个script
<script type="text/javascript">
  (function () {
    var script=document.createElement('script');
    script.async=false;
    script.src='config.js';
    document.head.appendChild(script);
    document.head.removeChild(script);
    
    var script2=document.createElement('script');
    script2.async=false;
    script2.src='config2.js';
    document.head.appendChild(script2);
    document.head.removeChild(script2);
    
    //config1.js先于config2.js执行,并在end of boby后面,也就是在当前页面的所有的script的后面执行
  })();
</script>
<body>
    <script>
        console.log('end of boby');
    </script>
</body>

因为你创建的新script加载是异步的,这个新下载也并不会阻塞浏览器往下执行。

可以往全局挂一个config.js加载成功的标志,然后在第二个script中检测这个标志。



<script type="text/javascript">
(function(){
    var script = document.createElement('script');
    script.async = false;
    script.src = "config.js";
    document.head.insertBefore(script,document.head.firstChild);
    script.onload = function(){
        window.configReady = true;
        console.log('config.js done');
    }
})();
</script>



<script type="text/javascript">
    (function(){
        function checkConfig(callback){
            var timer = setInterval(function(){
                if(window.configReady){
                    clearInterval(timer);
                    var config = window.config;
                    if(callback && typeof callback === "function"){
                        callback(config);
                    }
                }
            },30);
        }
      checkConfig(function(config){
         console.log(config);
      })
    })();
</script>

猜一下应该是这样的。

浏览器开始解释代码:

  1. 发现script1,执行script1(丢了个config.js到head里)

  2. 发现script2,执行script2

  3. 解释完毕
    ...

  4. config.js加载成功,执行config.js

因为异步啊

执行第一段时主线程并不等待加载完成,而是执行完后立刻执行第二段

新手上路,请多包涵

因为前面加载到head时文件未下载执行,而进行了下一个代码片段,异步问题。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏