【问题标题】:DOMContentLoaded never fires inside a moduleDOMContentLoaded 永远不会在模块内触发
【发布时间】:2020-05-05 11:38:02
【问题描述】:

我对在特定 ES6 模块加载场景中未触发的事件感到困惑。这是一个支持我的示例的 Codepen 项目: https://codepen.io/fchristant/project/editor/AYQkGJ

考虑这个非常简单的模块app.js

console.log('in module');
document.addEventListener('DOMContentLoaded', (event) => {
  console.log('DCL:2');
});

我正在使用来自文档头部脚本标签的动态 ES6 导入:

<script>
  (async () => {
    const module = await import("./js/app.js");
    console.log('async done');
   })();
</script>

所以,这个调用代码是一个非模块(同步脚本)动态加载一个模块。因为它在文档的头部而不是异步或延迟,所以我希望它立即运行,并以阻塞的方式运行。

这是我的期望,但不是正在发生的事情。特别是,问题是模块中的事件永远不会被触发。看起来是因为来不及听了,事件已经发生了。鉴于文档头部的阻止脚本,这怎么可能?它看起来是非阻塞的,但我不明白为什么。

要清楚,讨论不是这是否是一个好主意,我最感兴趣的是为什么这不起作用的概念性答案。或者换个说法:非模块脚本如何以同步方式动态加载模块?

【问题讨论】:

  • 如果您想以 同步 方式加载脚本,那么您只需将其添加到文档中的 &lt;script&gt; 标记中即可。模块的优点是您可以在单个脚本中使用与其他脚本不同的功能,方法是导入它们并在那里调用它们。在您的情况下,您的文件已下载,但您的 app.js 脚本中没有导出函数,因此没有什么可调用或使用的。
  • 那我应该导出什么?模块内的代码显然正在运行,因为打印了“模块内”。然而,当它运行时,domcontentloaded 已经发生了。我仍然不知道为什么会这样。
  • 异步导入你的模块会告诉浏览器它不应该在发出DOMContentLoaded事件之前等待模块。这与将async 属性添加到SCRIPT 标记几乎相同。如果您希望您的模块处理 DOMContentLoaded 事件 - 您不应该使用异步导入。
  • async 函数是非阻塞的 - 所以没有“头部阻塞脚本”
  • 您可以将domcontentloaded 事件监听器放在您的主脚本中,并在事件发生时导入您的模块。

标签: javascript ecmascript-6 es6-modules


【解决方案1】:

等待domcontentloaded 事件在您的index.html 文件中发生。在事件的回调中导入您想要在事件触发时运行的模块。

// app.js
export function start() {
  console.log('in module');
  console.log('DCL:2');
}
<!-- index.html -->
<script>
  (async () => {
    document.addEventListener('DOMContentLoaded', (event) => {
      const module = await import("./js/app.js");
      module.start();
      console.log('async done');
    });
  })();
</script>

或者,如果您不需要任何模块,并且只想在渲染 DOM 之前依次加载所有内容,则只需使用传统的阻止渲染的 &lt;script&gt; 标签。

<script src="./js/app.js"></script>
<script>
  console.log('module loaded');
</script>

【讨论】:

  • 这不是问题,这是一种解决方法和完全不同的解决方案。这会起作用,但不是我所追求的。问题是如何在 inside 模块代码中监听这个 DOM 事件。答案很可能是“你不能”,但这似乎很奇怪。模块等待特定 dom 事件的用例并不奇怪。
  • 考虑到这一点,您的模块是通过动态导入异步加载到文档中的,这是没有办法的。因此,如果您的 DOM 很大并且连接速度很快,那么您可能会及时捕捉到domcontentloaded 事件。但如果 DOM 较小且连接速度较慢,您的模块可能总是为时已晚。所以你有一个竞争条件。所以,是的,这不可能是你想要的方式。
  • 不,模块处理domcontentloaded 之类的事件并不奇怪,至少当您使用将所有内容放在单个文件中的模块捆绑器时不会。
  • 在我的例子中,app.js 本身就是一个模块,原因是它静态地导入了自己的模块。仅当 app.js 也是一个模块时,才可能进行静态导入。然而似乎没有办法同步加载任何模块,这看起来根本不可能。看起来你是对的。
【解决方案2】:

您可以使用辅助函数检查document.readyState,然后检查addEventListener 或立即运行代码。

  • onDCL1() 可以与回调 fn (A.1) 一起使用
  • onDCL2() 基于 Promise
    • …传统的.then()链(B.1)
    • ... 或 await (C.1)

我们可以“伪造”并发送我们自己的DOMContentLoaded 事件并验证else 块是否有效(A.2.、B.2.、C.2.)

class App {
  static onDCL1(callback) {
    if (document.readyState !== 'loading')
      callback(`onDCL1 ➜ readyState: '${document.readyState}'`);
    else
      document.addEventListener('DOMContentLoaded',
        e => callback(`onDCL1 ➜ ... waited readyState: '${document.readyState}'`));
  }

  static onDCL2() {
    return new Promise(resolve => {
      if (document.readyState !== 'loading')
        resolve(`onDCL2 ➜ readyState: '${document.readyState}'`);
      else
        document.addEventListener('DOMContentLoaded',
          e => resolve(`onDCL2 ➜ ... waited readyState: '${document.readyState}'`));
    });
  }
}

(async() => {
  console.log(`readyState: '${document.readyState}'`);

  // A.1
  App.onDCL1(payload => console.log(`A.1.| ${payload}`)); // run callback
  // B.1
  App.onDCL2().then(payload => console.log(`B.1.| ${payload}`)); // Promise chain
  // C.1
  let payload = await App.onDCL2(); // await  for Promise. Hint: no 'async' required, since we already return a Promise
  console.log(`C.1.| ${payload}`);


  // have to wait for C.1. (`await`) ....
  // ... comment 2 lines of "C.1" will show 'END' bevor Promise B.1. gets resolved async (~1ms)
  console.log('END 1');

  setTimeout(async() => {
    // dispatch "fake" event in 100ms, (we need to addEventListeners first
    // ... since C.2 has `await` we do this here
    setTimeout(() => {
      window.document.dispatchEvent(new Event("DOMContentLoaded", {
        bubbles: true,
        cancelable: true
      }));
    }, 100);

    // A.2
    App.onDCL1(payload => console.log(`A.2.| ${payload}`)); // run callback
    // B.2
    App.onDCL2().then(payload => console.log(`B.2.| ${payload}`)); // Promise chain
    // C.2
    let payload = await App.onDCL2(); // await  for Promise
    console.log(`C.2.| ${payload}`);

    console.log('END 2');
  }, 300);
})();

【讨论】:

    猜你喜欢
    • 2018-09-13
    • 1970-01-01
    • 2014-05-15
    • 2021-05-22
    • 2015-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多