Javascript: исправить множественное падение функции при достижении нижней части страницы?


Я думаю, что все вы знакомы с перезагрузкой элементов на веб-сайте через ajax, как только достигается нижняя часть страницы.

Я пытался сделать это сам, но, похоже, мой сценарий срабатывает несколько раз ...

<script type="text/javascript">
        $(document).ready(function() {

            $(window).scroll(function() {
                if ( $(window).scrollTop() + $(window).height() == $(document).height() ) {
                      myfunctionCall();

Есть идеи, как я могу остановить это от многократного вызова? Я думаю, что это происходит только на mac с "плавной прокруткой" или как вы это можете назвать.

С уважением, Matt

8 7

8 ответов:

Один действительно простой вариант-использовать переменную для хранения того, следует ли вызывать функцию.

<script type="text/javascript">
$(document).ready(function() {
  var callFunction = true;
  $(window).scroll(function() {
    if ( callFunction && $(window).scrollTop() + $(window).height() == $(document).height() ) {
      myfunctionCall();
      callFunction = false;
    }
  }
}
</script>

Попробуйте это. Когда пользователь прокручивает этот div в нижней части страницы, функция запускается.

<div id="loadMore"></div>

При работе с функцией прокрутки необходимо соблюдать осторожность, так как событие запускается один раз для каждого прокрученного пикселя. Поэтому вам нужно использовать тайм-аут, чтобы вызвать функцию только после остановки прокрутки.

function isScrolledIntoView(elem) {
    var docViewTop = $(window).scrollTop();
    var docViewBottom = docViewTop + $(window).height();
    var elemTop = $(elem).offset().top;
    var elemBottom = elemTop + $(elem).height();

    console.log(docViewTop, docViewBottom, elemTop, elemBottom);
    return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) && (elemBottom <= docViewBottom) && (elemTop >= docViewTop));

}

var timer;
$(window).scroll(function () {
    clearTimeout(timer);
    timer = setTimeout(function() {
        if (isScrolledIntoView($('#loadMore'))) {
            alert('reached');
            myfunctionCall();
            return false;
        }
    }, 50);
});

Вот скрипка, которая демонстрирует вызов. http://jsfiddle.net/2KhjJ/

Вы можете установить переменную, чтобы предотвратить многократное срабатывание обратного вызова scroll, но убедитесь, что она будет работать в сочетании с вероятным асинхронным способом myfunctionCall, используемым для извлечения и добавления данных.

Пример,

Http://jsbin.com/OfaKarek/1/edit

Html

<div id="container">
  </div>

  <div id="popup">loading....</div>

Css

#popup{
  display:none;
  position:fixed;
  left:25%;
  top:25%;
  background-color:grey;
}

Js

var counter=1;
function generateData(){
  var text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu lectus viverra, convallis enim interdum, viverra nisl.";
  var limit = counter+50;
  for(counter;counter<limit;counter++){
    var data=("<p>"+counter+text+"</p>");
    $('#container').append(data);
  }
}

function myfunctionCall(callback){
  $("#popup").show();
  /*retrieve data, mimic an async ajax call*/
  setTimeout(function(){
/*this is the part where the data returned from the call are appended*/
    generateData();
    $("#popup").hide();
    callback();
  },4000);
}

$(document).ready(function() {

  generateData();

  var retrievingData = false;
  $(window).scroll(function() {

    if ( !retrievingData && $(window).scrollTop() + $(window).height() == $(document).height() ) {
      retrievingData = true;
      myfunctionCall(function(){retrievingData = false;});
    }
  });

});

Таким образом, предполагается, что myfunctionCall или другая функция, вероятно, будет связана с получением данных асинхронно, выполнив вызов ajax. Чтобы сделать функцию не относящейся к событию прокрутки, был выполнен обратный вызов. Обратный вызов с myFunctionCall используется для установки значения переменной в соответствующем контексте, как это происходит в функции scroll event.

Кроме того, существует множество решений для достижения эффекта бесконечной или ленивой прокрутки нагрузки. Одним из которых является http://www.infinite-scroll.com/ со многими вариантами, созданными полом Айришем, я использовал в прошлом и это сработало великолепно.

Если вы знаете, какие элементы добавляются, как только вы достигнете дна, вы можете использовать это в своих интересах. Не запускайте обратный вызов только при достижении нижней части страницы, но в сочетании с последним отображаемым элементом. То, что я использовал для реализации непрерывной прокрутки, выглядит примерно так:

$(window).on("scroll", function ()
{
    // Get last shown element
    var trigger = $(".elements:last-child()"); 

    // Trigger callback when the top of the last element comes in view
    if ($(window).scrollTop() + $(window).height() > trigger.offset().top)
        myfunctionCall();
});
jQuery Waypoints - v2.0.3
Copyright (c) 2011-2013 Caleb Troughton
Dual licensed under the MIT license and GPL license.
https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt
*/
(function () { var t = [].indexOf || function (t) { for (var e = 0, n = this.length; e < n; e++) { if (e in this && this[e] === t) return e } return -1 }, e = [].slice; (function (t, e) { if (typeof define === "function" && define.amd) { return define("waypoints", ["jquery"], function (n) { return e(n, t) }) } else { return e(t.jQuery, t) } })(this, function (n, r) { var i, o, l, s, f, u, a, c, h, d, p, y, v, w, g, m; i = n(r); c = t.call(r, "ontouchstart") >= 0; s = { horizontal: {}, vertical: {} }; f = 1; a = {}; u = "waypoints-context-id"; p = "resize.waypoints"; y = "scroll.waypoints"; v = 1; w = "waypoints-waypoint-ids"; g = "waypoint"; m = "waypoints"; o = function () { function t(t) { var e = this; this.$element = t; this.element = t[0]; this.didResize = false; this.didScroll = false; this.id = "context" + f++; this.oldScroll = { x: t.scrollLeft(), y: t.scrollTop() }; this.waypoints = { horizontal: {}, vertical: {} }; t.data(u, this.id); a[this.id] = this; t.bind(y, function () { var t; if (!(e.didScroll || c)) { e.didScroll = true; t = function () { e.doScroll(); return e.didScroll = false }; return r.setTimeout(t, n[m].settings.scrollThrottle) } }); t.bind(p, function () { var t; if (!e.didResize) { e.didResize = true; t = function () { n[m]("refresh"); return e.didResize = false }; return r.setTimeout(t, n[m].settings.resizeThrottle) } }) } t.prototype.doScroll = function () { var t, e = this; t = { horizontal: { newScroll: this.$element.scrollLeft(), oldScroll: this.oldScroll.x, forward: "right", backward: "left" }, vertical: { newScroll: this.$element.scrollTop(), oldScroll: this.oldScroll.y, forward: "down", backward: "up" } }; if (c && (!t.vertical.oldScroll || !t.vertical.newScroll)) { n[m]("refresh") } n.each(t, function (t, r) { var i, o, l; l = []; o = r.newScroll > r.oldScroll; i = o ? r.forward : r.backward; n.each(e.waypoints[t], function (t, e) { var n, i; if (r.oldScroll < (n = e.offset) && n <= r.newScroll) { return l.push(e) } else if (r.newScroll < (i = e.offset) && i <= r.oldScroll) { return l.push(e) } }); l.sort(function (t, e) { return t.offset - e.offset }); if (!o) { l.reverse() } return n.each(l, function (t, e) { if (e.options.continuous || t === l.length - 1) { return e.trigger([i]) } }) }); return this.oldScroll = { x: t.horizontal.newScroll, y: t.vertical.newScroll } }; t.prototype.refresh = function () { var t, e, r, i = this; r = n.isWindow(this.element); e = this.$element.offset(); this.doScroll(); t = { horizontal: { contextOffset: r ? 0 : e.left, contextScroll: r ? 0 : this.oldScroll.x, contextDimension: this.$element.width(), oldScroll: this.oldScroll.x, forward: "right", backward: "left", offsetProp: "left" }, vertical: { contextOffset: r ? 0 : e.top, contextScroll: r ? 0 : this.oldScroll.y, contextDimension: r ? n[m]("viewportHeight") : this.$element.height(), oldScroll: this.oldScroll.y, forward: "down", backward: "up", offsetProp: "top" } }; return n.each(t, function (t, e) { return n.each(i.waypoints[t], function (t, r) { var i, o, l, s, f; i = r.options.offset; l = r.offset; o = n.isWindow(r.element) ? 0 : r.$element.offset()[e.offsetProp]; if (n.isFunction(i)) { i = i.apply(r.element) } else if (typeof i === "string") { i = parseFloat(i); if (r.options.offset.indexOf("%") > -1) { i = Math.ceil(e.contextDimension * i / 100) } } r.offset = o - e.contextOffset + e.contextScroll - i; if (r.options.onlyOnScroll && l != null || !r.enabled) { return } if (l !== null && l < (s = e.oldScroll) && s <= r.offset) { return r.trigger([e.backward]) } else if (l !== null && l > (f = e.oldScroll) && f >= r.offset) { return r.trigger([e.forward]) } else if (l === null && e.oldScroll >= r.offset) { return r.trigger([e.forward]) } }) }) }; t.prototype.checkEmpty = function () { if (n.isEmptyObject(this.waypoints.horizontal) && n.isEmptyObject(this.waypoints.vertical)) { this.$element.unbind([p, y].join(" ")); return delete a[this.id] } }; return t }(); l = function () { function t(t, e, r) { var i, o; r = n.extend({}, n.fn[g].defaults, r); if (r.offset === "bottom-in-view") { r.offset = function () { var t; t = n[m]("viewportHeight"); if (!n.isWindow(e.element)) { t = e.$element.height() } return t - n(this).outerHeight() } } this.$element = t; this.element = t[0]; this.axis = r.horizontal ? "horizontal" : "vertical"; this.callback = r.handler; this.context = e; this.enabled = r.enabled; this.id = "waypoints" + v++; this.offset = null; this.options = r; e.waypoints[this.axis][this.id] = this; s[this.axis][this.id] = this; i = (o = t.data(w)) != null ? o : []; i.push(this.id); t.data(w, i) } t.prototype.trigger = function (t) { if (!this.enabled) { return } if (this.callback != null) { this.callback.apply(this.element, t) } if (this.options.triggerOnce) { return this.destroy() } }; t.prototype.disable = function () { return this.enabled = false }; t.prototype.enable = function () { this.context.refresh(); return this.enabled = true }; t.prototype.destroy = function () { delete s[this.axis][this.id]; delete this.context.waypoints[this.axis][this.id]; return this.context.checkEmpty() }; t.getWaypointsByElement = function (t) { var e, r; r = n(t).data(w); if (!r) { return [] } e = n.extend({}, s.horizontal, s.vertical); return n.map(r, function (t) { return e[t] }) }; return t }(); d = { init: function (t, e) { var r; if (e == null) { e = {} } if ((r = e.handler) == null) { e.handler = t } this.each(function () { var t, r, i, s; t = n(this); i = (s = e.context) != null ? s : n.fn[g].defaults.context; if (!n.isWindow(i)) { i = t.closest(i) } i = n(i); r = a[i.data(u)]; if (!r) { r = new o(i) } return new l(t, r, e) }); n[m]("refresh"); return this }, disable: function () { return d._invoke(this, "disable") }, enable: function () { return d._invoke(this, "enable") }, destroy: function () { return d._invoke(this, "destroy") }, prev: function (t, e) { return d._traverse.call(this, t, e, function (t, e, n) { if (e > 0) { return t.push(n[e - 1]) } }) }, next: function (t, e) { return d._traverse.call(this, t, e, function (t, e, n) { if (e < n.length - 1) { return t.push(n[e + 1]) } }) }, _traverse: function (t, e, i) { var o, l; if (t == null) { t = "vertical" } if (e == null) { e = r } l = h.aggregate(e); o = []; this.each(function () { var e; e = n.inArray(this, l[t]); return i(o, e, l[t]) }); return this.pushStack(o) }, _invoke: function (t, e) { t.each(function () { var t; t = l.getWaypointsByElement(this); return n.each(t, function (t, n) { n[e](); return true }) }); return this } }; n.fn[g] = function () { var t, r; r = arguments[0], t = 2 <= arguments.length ? e.call(arguments, 1) : []; if (d[r]) { return d[r].apply(this, t) } else if (n.isFunction(r)) { return d.init.apply(this, arguments) } else if (n.isPlainObject(r)) { return d.init.apply(this, [null, r]) } else if (!r) { return n.error("jQuery Waypoints needs a callback function or handler option.") } else { return n.error("The " + r + " method does not exist in jQuery Waypoints.") } }; n.fn[g].defaults = { context: r, continuous: true, enabled: true, horizontal: false, offset: 0, triggerOnce: false }; h = { refresh: function () { return n.each(a, function (t, e) { return e.refresh() }) }, viewportHeight: function () { var t; return (t = r.innerHeight) != null ? t : i.height() }, aggregate: function (t) { var e, r, i; e = s; if (t) { e = (i = a[n(t).data(u)]) != null ? i.waypoints : void 0 } if (!e) { return [] } r = { horizontal: [], vertical: [] }; n.each(r, function (t, i) { n.each(e[t], function (t, e) { return i.push(e) }); i.sort(function (t, e) { return t.offset - e.offset }); r[t] = n.map(i, function (t) { return t.element }); return r[t] = n.unique(r[t]) }); return r }, above: function (t) { if (t == null) { t = r } return h._filter(t, "vertical", function (t, e) { return e.offset <= t.oldScroll.y }) }, below: function (t) { if (t == null) { t = r } return h._filter(t, "vertical", function (t, e) { return e.offset > t.oldScroll.y }) }, left: function (t) { if (t == null) { t = r } return h._filter(t, "horizontal", function (t, e) { return e.offset <= t.oldScroll.x }) }, right: function (t) { if (t == null) { t = r } return h._filter(t, "horizontal", function (t, e) { return e.offset > t.oldScroll.x }) }, enable: function () { return h._invoke("enable") }, disable: function () { return h._invoke("disable") }, destroy: function () { return h._invoke("destroy") }, extendFn: function (t, e) { return d[t] = e }, _invoke: function (t) { var e; e = n.extend({}, s.vertical, s.horizontal); return n.each(e, function (e, n) { n[t](); return true }) }, _filter: function (t, e, r) { var i, o; i = a[n(t).data(u)]; if (!i) { return [] } o = []; n.each(i.waypoints[e], function (t, e) { if (r(i, e)) { return o.push(e) } }); o.sort(function (t, e) { return t.offset - e.offset }); return n.map(o, function (t) { return t.element }) } }; n[m] = function () { var t, n; n = arguments[0], t = 2 <= arguments.length ? e.call(arguments, 1) : []; if (h[n]) { return h[n].apply(null, t) } else { return h.aggregate.call(null, n) } }; n[m].settings = { resizeThrottle: 100, scrollThrottle: 30 }; return i.load(function () { return n[m]("refresh") }) }) }).call(this);

Включите приведенный выше код. Это плагин waypoint.

Используйте его так:

$(window).waypoint(function() {
  myFunctionCall() //This function will be called after the page is scrolled to the bottom 
}, { offset: '100%' });

Для получения дополнительной информации: Путевая точка

Отметьте это как ответ, если это поможет :)

В вашей логике нет ничего, что могло бы предотвратить многократный вызов функции, поэтому код делает именно то, что вы просите. Браузеры действуют по-разному при запуске события "scroll". Некоторые делают это один раз за действие прокрутки, в то время как другие запускают событие непрерывно, пока пользователь прокручивает.

Один из способов решить вашу проблему - "обнулить" событие так, чтобы оно срабатывало только один раз за столько-то миллисекунд. Точный способ обработки дебоунсинга будет зависеть от вашего специфические потребности. Если вы используете (или можете использовать) подчеркивание .JS служебная библиотека, есть встроенная функция debounce(). Если вы не хотите использовать всю библиотеку целиком, вы можете скопировать ее код. (Это всего лишь несколько строк.) Если вы ориентируетесь на современные браузеры, вы можете использовать события requestAnimationFrame для дебаунса.

Доктор Google имеет много информации.

Вы можете использовать решение @rtcherry, но я просто хочу добавить некоторые мысли:

  1. есть много плагинов, которые помогут с этим. Просто погуглите "бесконечный свиток".
  2. Используя такую конструкцию:

    if ( $(window).scrollTop() + $(window).height() == $(document).height() ) {
        // code
    }
    

    Означает, что" код " будет выполняться в самом низу страницы... Я рекомендую сделать такое условие:

    if ( $(window).scrollTop() + $(window).height() + 500 == $(document).height() ) {
    

    В этом случае код будет запущен немного раньше и пользователь не будет ждать результата в самом низу. Вы можете использовать любое другое число вместо "500", это было просто для демонстраций.

  3. Насколько мне известно," scroll " запускается несколько раз во всех браузерах и ОС, а не только на mac.

Вы можете отменить привязку события scroll в своем методе myfunctionCall и повторно привязать его позже, когда он вам снова понадобится. Используйте методы jqueries on() и off(). Я думаю, что этот подход более чистый, чем добавление глобальной булевой переменной.

   var myfunctionCall= function(){
       win=$(this);//we use the function on the window, so $(this) is the window
       if ( win.scrollTop() + win.height() == $(document).height() ){
          //first unbind the scroll event
           win.off('scroll',myfunctionCall);
          //then do your (assync) stuff
           alert('this can be anything');
      } 
    }

    $(document).ready(function() {
       //bind the scroll handler on document ready. You can do this whenever you want 
       //to rebind the handler.
       $(window).on('scroll', myfunctionCall);
    });