博客园js依赖问题,defer和async

背景

我在博客园后台写的自定义script依赖某个js库,他始终提示xxx not defined.

原因

image

image

通过对比,不难发现,博客园偷偷改了你的代码,用defer优化了一下,防止js代码的加载阻塞dom渲染。

defer和async

script标签可以使用defer或者async属性。
defer: 到dom渲染完毕后执行。
async: 下载完js后执行。

说得再多不如图示来得简洁明了,请看图
image
绿色是dom加载,即html标签解析,解析的同时,defer的script会新建一个线程下载,不阻塞其解析。在html解析完毕后,defer的script执行。
图片来自如下链接
https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html

分析

在html解析的过程中,我的自定义代码率先执行了。而其依赖的js库,被博客园擅自改成了defer,自然是html解析之后再执行,这时为时已晚。

解决?

错误方法

!!!错的方法
js库引入写在前面,自定义代码写在后面,并且为其加上defer。
image
上面这种写法是不行的,博客园会把红框里的defer属性去除掉。
试了一下,这样写在一起依然不行。把js库放页首,自定义代码写在侧边栏试试。
加载顺序: 页首 -> 页脚 -> 侧边栏。
建议之后在博客园中加入的任何script标签都加上defer属性,因为你不加博客园会给你加上,容易懵逼。

真是挺坑爹的,我调了半天,一直以为是代码写错了,因为这个better-scroll我也没怎么用过。

博客园的顺序

页首/页脚

普通script标签会照常顺序执行,defer最后执行。

侧边栏

defer和普通的script标签被视为同级,顺序执行。

值得一提的是,侧边栏的defer会比页首的先执行。后面发现,侧边栏的引入js库的标签被删掉了,可能是改成普通的阻塞引入了。

博客园的defer

由于defer的后置执行特性,我们手写的script标签不能依赖于defer的脚本。
我们可以将依赖js库的脚本也写成一个js文件,然后放到博客园自带的文件管理中,使用defer按顺序导入,但是需要注意的是,此时普通script标签不能依赖于这些defer的脚本。
image
如图,假设main2.js依赖于test.js,他们会正常运行。

但是,正如我前面所说,普通的script标签不能依赖于defer,那么你后面需要用到testjs的代码都得写在js文件中并导入,每次改代码都需要改js文件。这样用还是不太方便的。

我举个例子,我在js文件中创建一个对象,设置初始值(默认值),并且挂载到全局对象上,比如jquery或者window。为了便于使用,应该定义一个方法,用于修改默认的参数。
那么最便于修改的地方就是博客园的后台,而在后台写js就是普通的script标签,是不能依赖于defer脚本的。至于js文件是很麻烦的,博客园的js文件有缓存。

结论

正确做法

博客园页首和页脚的defer引入是真正的defer,而侧边栏引入的js会特殊处理,导致比页首的更快。
如果想要引入被其他js代码所依赖的js库,应当写在侧边栏的部分,并且不加async和defer属性(因为加不加都一样)。

错误方法2

添加async属性

<script src="https://blog-static.cnblogs.com/files/blogs/806667/test.js" async></script>

可以看到,虽然还是被博客园添加了defer属性,但是async属性被保留下来,生效的是async而不是defer。
image

引入博客园的js时,页首里面的async有可能会比侧边栏或者页脚的script代码更先执行,因为处于页首会先解析到然后立即开始下载,如果在没有解析到依赖于这个js库的script标签前,下载已经完成,那么看上去一切都是那么合理。但是如果是外站或者文件比较大,先后顺序就不一定了,这是一种不严谨的做法,只能说是有可能成功,但是不应该这样写。
因此async作为依赖是一种不正确的写法。

使用场景

普通script,如果脚本很小并且由异步脚本(async和defer)依赖,则使用内联脚本,在异步脚本上方。

普通的script一般写在body的尾部,以确保我们的页面能尽可能快地展示给用户。
如果一个网络脚本被内联脚本依赖,那么他不能是defer和async的,只能是普通的script。

async,没有依赖关系。

defer,被其他脚本所依赖,而且不被普通的script标签依赖。

无论是哪个脚本,都是被解析到了才开始下载,因此即使是async也可以对其前面的dom进行操作。只不过async和defer的下载不中断dom解析。
由于defer的特性,生产者和消费者(依赖和被依赖)应当都是defer的脚本。

其他

执行流程。

  1. 从网页下载html文件
  2. 逐行解析html
  3. 执行下载好了的defer和async
  4. DOMContentLoaded事件触发
  5. 图片之类的继续加载
  6. 图片以及其他资源加载完毕后,触发load事件

image

load之后一般就不管了,load之前发起了异步请求,当这个请求完成后,这个完成用时就会变,因此完成用时没什么意义。

async

image
和网络上流传的这张图不一样的是,async并不会像他们说的那样阻塞html解析,而是和defer类似,在解析完毕后才执行。
和我预想中不一样的是,async并不会插队执行。
一个脚本下载完毕后,进入一个任务队列,谁在前面谁先执行。
假如是defer,当前有多个defer在队列中,那么按照其出现的顺序执行。
因此,async之所以被推荐,就是因为他不需要去和其他标签比较谁先谁后,理论上性能更高一些。
当然,我只是根据行为来猜测。

测试代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>渲染测试</h1>
    <script src="./async.js" async></script>
    <script src="./defer.js" defer></script>

    <script>
      document.addEventListener("DOMContentLoaded", function () {
        console.log("DOM 已完全加载和解析");
        // 在这里可以安全地操作 DOM
      });
      function fibonacci(n) {
        if (n <= 1) return n;
        return fibonacci(n - 1) + fibonacci(n - 2);
      }

      function simulateHeavyTask() {
        console.log("开始耗时操作...");
        const startTime = Date.now();

        // 计算斐波那契数列的第40项(非常耗时)
        const result = fibonacci(40);

        const endTime = Date.now();
        console.log(
          `耗时操作结束,结果: ${result}, 耗时: ${endTime - startTime}ms`
        );
      }

      // 模拟耗时操作,由此可以确保脚本下载完毕。
      simulateHeavyTask();
      console.log("主线程继续执行...");
      // 此处debugger;打开浏览器,F12,然后刷新,你就会发现,后面的两个元素不会被渲染出来,因为阻塞了。
      // 而async内部的debugger没有阻塞后面的元素渲染,因此async的脚本并不是下载完就立即执行的(而是等待dom解析完毕)
    </script>
    <h2>后面的元素</h2>
    <h2>后面的元素2</h2>
  </body>
</html>
// async.js
console.log('async');
debugger;
console.log('defer');
posted @ 2024-12-27 02:40  魂祈梦  阅读(75)  评论(0)    收藏  举报