JQuery .when .done似乎导致代码在脚本完成加载

时间:2016-10-18 06:32:38

标签: javascript jquery promise

用JQuery承诺替换硬编码的<script>之后,我经常遇到这些错误:

enter image description here

重现问题是不一致的。有时,页面将加载而没有错误,如果我一直按下刷新按钮,而不是从新选项卡重新加载页面,这似乎会发生。

以下是演示此问题的代码的最小版本:

<!DOCTYPE html>
<html>
<head>
  <style>
    #map {
      width: 100%;
      height: 100vh;
    }
  </style>
</head>

<body>
<div id="map"></div>
<script src=//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js></script>
<script>
  $(makemap());

  var pins = [
    {
      "lng": -79.9133742,
      "lat": 43.2594723,
      "id": 544
    },
    {
      "lng": -79.9239563,
      "lat": 43.2585329,
      "id": 545
    },
    {
      "lng": -79.92670809999998,
      "lat": 43.2580113,
      "id": 546
    },
  ];

  function makemap() {
    $.when(
      $('<link/>', {
        rel: 'stylesheet',
        href: '//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css'
      }).appendTo('head'),
      $('<link/>', {
        rel: 'stylesheet',
        href: '//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css'
      }).appendTo('head'),
      $.getScript("//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"),
      $.getScript("//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster-src.js"),
      $.Deferred(function (deferred) {
        $(deferred.resolve);
      })
    ).done(function () {
      var tiles = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
          maxZoom: 18,
          attribution: '&copy; <a href=http://osm.org/copyright>OpenStreetMap</a> contributors'
        }),
        latlng = L.latLng(43.26, -79.92);

      var map = L.map('map', {center: latlng, zoom: 14, layers: [tiles]});

      var markers = L.markerClusterGroup();
      var markerList = [];

      for (var i of pins) {
        var marker = L.marker(L.latLng(i.lat, i.lng), {title: i.id});
        marker.placeid = i.id;
        markers.addLayer(marker);
        markerList.push(marker);
      }
      map.addLayer(markers);
    })
  }
</script>
</body>
</html>

我做错了什么?如果这是JQuery的限制,是否有任何替代方法可以完成我想要做的事情(最好使用原生ES6或更低版本)?

使用Mike的代码尝试#2:

<!DOCTYPE html>
<html>
<head>
  <link rel=stylesheet href=//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css>
  <link rel=stylesheet href=//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css>
  <style>
    #map {
      width: 100%;
      height: 100vh;
    }
  </style>
</head>

<body>
<div id="map"></div>
<script src=//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js></script>
<script>
  var pins = [
    {
      "lng": -79.9133742,
      "lat": 43.2594723,
      "id": 544
    },
    {
      "lng": -79.9239563,
      "lat": 43.2585329,
      "id": 545
    },
    {
      "lng": -79.92670809999998,
      "lat": 43.2580113,
      "id": 546
    },
  ];

  var scripts = [
    "//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js",
    "//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster-src.js"
  ];

  var loaded = scripts.length;

  function checkDone() {
    loaded = loaded - 1;
    if (loaded === 0) {
      var tiles = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
          maxZoom: 18,
          attribution: '&copy; <a href=http://osm.org/copyright>OpenStreetMap</a> contributors'
        }),
        latlng = L.latLng(43.26, -79.92);

      var map = L.map('map', {center: latlng, zoom: 14, layers: [tiles]});

      var markers = L.markerClusterGroup();
      var markerList = [];

      for (var i of pins) {
        var marker = L.marker(L.latLng(i.lat, i.lng), {title: i.id});
        marker.placeid = i.id;
        markers.addLayer(marker);
        markerList.push(marker);
      }
      map.addLayer(markers);
    }
  }

  while (scripts.length) {
    var head = scripts.splice(0, 1)[0];
    $.getScript(head, checkDone);
  }
</script>
</body>
</html>

3 个答案:

答案 0 :(得分:1)

正如您所发现的那样,问题的核心是jQuery.getScript()

  • 它可靠地触发其回调(或链式.then())以指示脚本已加载,但
  • 无法保证已加载的脚本已执行

根据this answer,问题应该在jQuery 2.1.0+中修复,但是,根据你的说法,似乎并非如此。

为了让脚本有更大的机会执行,地图/标记代码的执行需要稍后推送一些未知的少量。

以下是尝试的一些事情 - 并不比我害怕的更好。

首先通过将checkDone()拆分为两个函数来为seconmd脚本提供更多的加载时间,如下所示:

function makeMap() {
    var map = L.map('map', {
        center: L.latLng(43.26, -79.92), 
        zoom: 14, 
        layers: [L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 18,
            attribution: '&copy; <a href=http://osm.org/copyright>OpenStreetMap</a> contributors'
        })]
    });
    setTimeout(makeMarkers.bind(map), 0);
}
function makeMarkers() {
    var markers = L.markerClusterGroup();
    pins.forEach(function(pin) {
        var marker = L.marker(L.latLng(pin.lat, pin.lng), { title: pin.id });
        marker.placeid = pin.id;
        markers.addLayer(marker);
    });
    this.addLayer(markers);
}

请注意,makeMarkers()是通过超时从makeMap()调用的。即使超时0,调用也会在稍后的事件线程中发生。

现在,将这两个函数放在palce中,逐步将while (scripts.length) {...}循环替换为:

$.when.apply(null, scripts.map(function(url) {
    return $.getScript(url);
})).then(makeMap);

然后:

scripts.reduce(function(promise, url) {
    return promise.then(function() {
        return $.getScript(url);
    });
}, $.when()).then(makeMap);

然后:

scripts.reduce(function(promise, url) {
    return promise.then(function() {
        return $.getScript(url);
    });
}, $.when()).then(function() {
    setTimeout(makeMap, 0);
});

然后:

scripts.reduce(function(promise, url) {
    return promise.then(function() {
        return $.getScript(url);
    });
}, $.when()).then(function() {
    setTimeout(makeMap, 1000);
});

在某个阶段,您应该发现其中一个进程(或者可能是延迟时间更长的最后一个版本)在当前条件下在您的浏览器/计算机上触发makeMap到可靠性最晚。

您仍然对其他浏览器和其他计算机存在不确定性....

最终,您可能需要恢复到硬编码脚本标记并接受更长的页面加载时间。最好的方法是在所有浏览器中可靠地支持<SCRIPT>标记的defer属性。

答案 1 :(得分:0)

两件事:

  1. 您错过了代码周围的主$( function doThisOnDocumentReady() { ... })包装器。现在它将立即启动 而不是等待首先完成DOM。这在这里并不是非常关键,但这是一个很好的练习和良好的习惯(如果你只是希望解决这个问题,你几乎可以忽略这个建议)

  2. getScript本身不会等待这些节点中链接的资源在返回之前“完全”完成。您可以在完成加载脚本后给它一个回调来触发,但这仍然不能保证脚本的执行完毕。因此,在技术上保证您需要的东西实际可用之前,您的代码现在运行。

  3. 所以,我们不是希望他们阅读,而是在我们运行依赖于正确设置传单等的代码之前明确确保它们已经准备就绪:

    function doStuffKnowingItllWork() {
      // ... your actual code ...
    }
    
    $.when() {
      // ... load scripts etc here ...
    ).done(function () {
    
      // ensure we have everything available
      (function ensureDependencies() {
        if (window.L) {
          return setTimeout(doStuffKnowingItllWork,0);
        }
        setTimeout(ensureDependencies, 200);
      }());
    });
    

    这设置了一个函数,检查我们是否有我们需要做的工作(在传单,全局L变量的情况下),如果它不可用,我们安排一个调用至少相同的功能(至少)200毫秒。

    如果我们确实拥有了我们需要的所有依赖项,我们会调度“实际运行您需要的代码”,使用新的执行上下文和callstack,通过在下一个tick上安排它,使用setTimeout 0 - 你可以只是return doStuffKnowingItllWork();,但如果出现错误,您的堆栈跟踪将包括依赖项检查和jquery包装。

    另请注意,这是一个在声明后立即调用的函数,并且有一些语法自由:(function x(){})()(function x(){}())都做同样的事情;放置执行操作符(运行函数的()部分)主要取决于您以及您想要遵循的JS样式。另请注意,它在功能上等同于function x(){}; x();,因此如果您想要明确使用它:也取决于您。

答案 2 :(得分:-2)

在leaflet.js之前调用Jquery 和$ .when(在document.ready