【问题标题】:How to load JS scripts dynamically, concurrency-safe?如何动态加载 JS 脚本,并发安全?
【发布时间】:2017-02-18 11:10:10
【问题描述】:

在我的项目中,我有非常标准的 loadScript 方法来动态加载 JS 脚本,完成后会触发回调:

app.utilities.loadScript = function loadScript(url, callback)
{

  if (app.scripts[url]===true) {
    callback();
  } else {

    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;

    script.onload = function() {
      app.scripts[url] = true;
      callback();
    }
     // Fire the loading
    head.appendChild(script);
  }
};

在我的项目情况下,我正在使用基于组件的系统。为了说明这个问题,想象在同一个页面上有两个 Google Maps 组件。两者都是组件的实例。在他们的构造函数中,我正在调用该方法,就像这样......

  constructor(selector) {

    super(selector);

    // API load status
    this._api = false;
    // load the maps API
    app.utilities.loadScript('https://maps.googleapis.com/maps/api/js?v=3.exp&key=' + app.config.mapsKey, this.initMap.bind(this));

  }

我预料到如果一个组件的两个实例加载相同的脚本,它不应该被加载两次。出于这个原因,您可以看到在 loadScript 帮助程序中,我如何维护一个已加载脚本的全局数组:app.scripts[url] = true;

但是,此策略不起作用,因为多个 loadScript 调用彼此非常接近地启动。由于第一个调用尚未完成(正在进行中),第二个调用开始另一个调用。

我正在考虑立即在全局数组中的脚本上设置“加载”状态,但这仍然让我想知道第二个调用如何监听第一个调用准备就绪,因为它确实需要这样的触发器回调触发得太快了。

我觉得我可能想多了?

编辑:包括解决此问题的代码,基于@siam 的回答,并添加了一些调整:

app.utilities.loadScriptEvent = function loadScript(url, eventName)
{

    if (app.scripts[url] == 'loading') {
      return true;
    }

    else {
    app.scripts[url] = 'loading';

    var event = new CustomEvent(eventName);

    // Adding the script tag to the head as suggested before
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;

    // Then bind the event to the callback function.
    // There are several events for cross browser compatibility.
    //script.onreadystatechange = callback;

    script.onload = function() {
      app.scripts[url] = true;
      app.window.dispatchEvent(event);
    }
     // Fire the loading
    head.appendChild(script);
  }
};

请注意,该方法现在命名为 loadScriptEvent,并且第二个参数是事件名称,而不是回调。这个想法是,即使两个组件几乎可以同时调用它,它们都侦听相同的单个事件。但这还不够,如果第一个请求已经忙于加载脚本,则必须停止第二个请求。我已经意识到加载状态。你可以这样调用这个方法:

app.utilities.loadScriptEvent('https://maps.googleapis.com/maps/api/js?v=3.exp&key=' + app.config.mapsKey, "gmloaded");
app.window.addEventListener('gmloaded',this.initMap.bind(this));

这很好用。组件实例彼此不知道,但脚本仍然只加载一次,异步的,我们可以在它完成后立即运行代码。

【问题讨论】:

  • 使用setInterval检查第一个请求是否完成,如果完成则启动第二个请求并清除该间隔
  • @siam。我害怕这样的答案,但这可能是唯一的方法。请注意,虽然第二个请求不知道第一个请求(因为这是两个彼此不知道的组件实例)。
  • 嗯...也许还有另一种方法。您可以创建一个在第一个请求完成时触发的自定义事件。您还可以通过该事件传递第一个请求的data
  • @siam 将进一步探索该选项,谢谢!

标签: javascript concurrency callback


【解决方案1】:

以下代码可能会让您大致了解如何完成它:

// using jQuery

app.utilities.loadScript = function loadScript(url, callback) { 
  ...
  script.onload = function(data) {
        app.scripts[url] = true;
        callback();
        ...
        $(document).trigger('firstRqstCompleted', data);
    }
  ...
}

$(document).on('firstRqstCompleted', function(e, data) {
    // do stuff with first request's data
    // initiate second request 
});

【讨论】:

  • 感谢您的代码,但不幸的是,此解决方案无法解决问题。问题的本质是两个对 loadscript 的调用都会非常快。因此,您在 onload 中输入的任何代码都不会阻止第二个请求执行相同的操作。刚刚使用自定义事件处理程序尝试了此操作,它将被调用两次。
  • 您的代码需要稍作调整(请参阅我编辑的答案)以避免再次触发 onload,但使用事件的总体思路是正确的,因此我将其标记为正确答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-21
  • 1970-01-01
  • 1970-01-01
  • 2019-01-28
  • 2011-07-23
相关资源
最近更新 更多