关于 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??

阅读 7.6k
评论
    6 个回答
    • 8k

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

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

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

      • 18.4k

      现代浏览器支持了对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>
        • 1.2k

        因为你创建的新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

            因为异步啊

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

              • 3
              • 新人请关照

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

                撰写回答

                登录后参与交流、获取后续更新提醒