使用箭头键和智能延迟加载实现有机列表浏览

时间:2017-03-03 07:39:16

标签: javascript jquery timeout settimeout

简单场景

我有一个列表,我使用箭头键(向上,向下)实现浏览,并且在每次更改当前列表项时,数据库对象通过AJAX加载。

甜。

问题

当用户非常快速地浏览列表时,我不希望每个请求都被取消。但当然,原始请求应该立即生效。

我的想法是使用变量作为延迟设置超时,并在项目的初始加载后,增加该变量。

这样可行,但是当用户停止浏览一小段时间但又继续浏览时,我仍然不希望每个请求都被取消。

所以我想,每次浏览事件都必须合理地增加延迟变量,直到达到阈值。

这种有机方法可以成功地减少不必要的物品装载量。

我的解决方案

我走了很远。这段代码(下面的解释)将完成这项工作,只有一个主要罪魁祸首

第一次浏览完成然后停止后,延迟将自动保持在(第二步)最小值150ms。

当然我试图修复此问题,但正如您将看到的,这是一个有趣但可能相当常见的逻辑问题 - 我认为我的整体方法是错误的。

但我不知道怎么做。大脑无法计算。电脑说没有。

代码

您可以筛选我的示例或go here for a fully functional simulator in jsFiddle

如果您选择jsFiddle:

单击按钮,立即显示项目加载。现在稍等一下,再次单击该按钮,初始加载将被延迟。如果您持续快速按下按钮,只有在点击完成后才会显示项目加载。

代码示例

我们在对象文字中,只是你知道。

_clickTimer: false,                     // holds what setTimeout() returns
_timerInc: 0,                           // the your timer delay are belong to us

/**
 * Function is triggered whenever the user hits an arrow key
 * itemRef is the passed list item object (table row, in this case)
 */

triggerItemClick: function(itemRef){

    var that=this;
    var itemId=$(itemRef).data('id');   // Get the item id

    if(this._clickTimer){               // If a timeout is waiting

        clearTimeout(this._clickTimer); // we clear it
        this._itemClickTimer=false;     // and reset the variable to false

        /**
         * Note that we only arrive here after the first call
         * because this._clickTimer will be false on first run
         */

        if(this._timerInc == 0){        // If our timer is zero
            this._timerInc = 150;       // we set it to 150
        } else {                        // otherwise
            if(this._timerInc <= 350)   // we check if it is lower than 350 (this is our threshold)
                this._timerInc += 15;   // and if so, we increase in steps of 15
        }

    } 

    /**
     * Regardless of any timing issues, we always want the list
     * to respond to browsing (even if we're not loading an item.
     */

    this.toggleListItem(itemId);

    /**
     * Here we now set the timeout and assign it to this._clickTimer
     */

    this._clickTimer=setTimeout(function(){

        // we now perform the actual loading of the item
        that.selectItem(itemId);

        // and we reset our delay to zero
        that._timerInc=0;

    }, this._timerInc); // we use the delay for setTimeout()

}

解释

首次通话时:_clickTimerfalse_timerInc0,因此第一次通话会导致0延迟setTimeout() }}和_clickTimer将被设置。该项目将立即加载。

第二次通话 - 鉴于我们的超时仍在等待触发,_clickTimer被清除,延迟设置为150如果0或增加15低于350(阈值)。

如果你继续浏览,这很有用。计时器增加,只有在您停止浏览一段时间后才会触发加载。

停止后

,下次再次继续时,_clickTimer不为假(因为setTimeout()会为其指定一个计数器) ,反过来_timerInc将立即设置为150。因此,第一次浏览将导致在加载任何内容之前延迟150ms。

叫我疯狂或挑剔,但目标是不要有这种延迟。

当然你会说:简单,在_clickTimer闭包结束时将setTimeout()设置为false,这样一旦浏览完成并且项目被加载就会重置。太好了,但这会导致延迟永远不会超过0ms。仔细想想,你会看到。

我希望这是正确解释的,并且某人的大脑比我的更有能力找到解决方案。

3 个答案:

答案 0 :(得分:2)

可能使用Promises以非常复杂的方式执行此操作。由于这主要是糖衣,但我认为必须有可能直接解决这个问题,我想我做了。

Updated fiddle。我在文本中添加了延迟,因此我更容易调试内容,并且还进行了一些小的整理,但我的实际更改非常小。详情如下。

你的评论接近尾声是我的第一个直觉:

  

当然你会说:简单,在结束时将_clickTimer设置为false   setTimeout()关闭,因此一旦完成浏览就会重置   项目被加载。很好,但是这会导致延迟永远不会超过0毫秒。

实际上,这会使延迟永远不会超过0,因为我们无法快速点击(或在实际应用中快速浏览)。但是......如果我们只在延迟不是0时重置怎么办?因此,如果超时消失,但仅在0毫秒后消失,我们就会记得超时。如果它晚于此时间,则必须在浏览中实际停顿。这可以通过在超时回调中添加几行来轻松实现,如下所示。

this._clickTimer = setTimeout(function() {

  // we now perform the actual loading of the item
  that.selectItem();

  // and we reset our delay to zero
  if (that._timerInc > 0) {
    that._clickTimer = false;
  }
  that._timerInc = 0;

}, this._timerInc); // we use the delay for setTimeout()

它似乎完全按照您的要求工作,但现在,延迟将是0ms,然后是150ms,然后是0ms,等等,如果您在点击之间等待足够长的时间。这可以通过添加额外的超时来解决,以防延迟 0ms仍将重置延迟。每当触发器发生时(单击演示,在应用程序中浏览),此超时将被取消。

我相信这一起使一切按照你想要的方式运作。为了完整起见,我还在上面提到了上面提到的小提琴。

var _simulator = {

  _clickTimer: false, // holds what setTimeout() returns
  _cancelClickTimer: false,
  _timerInc: 0, // the your timer delay are belong to us

  /**
   * Function is triggered whenever the user hits an arrow key
   * itemRef is the passed list item object (table row, in this case)
   */
  triggerItemClick: function() {

    var that = this;
    
    // always cancel resetting the timing, it can never hurt
    clearTimeout(that._cancelClickTimer);
    that._cancelClickTimer = false;

    if (this._clickTimer) { // If a timeout is waiting
      clearTimeout(this._clickTimer); // we clear it
      this._clickTimer = false; // and reset the variable to false

      /**
       * Note that we only arrive here after the first call
       * because this._clickTimer will be false on first run
       */
      if (this._timerInc == 0) { // If our timer is zero
        this._timerInc = 150; // we set it to 150
      } else { // otherwise
        if (this._timerInc <= 350) // we check if it is lower than 350 (this is our threshold)
          this._timerInc += 15; // and if so, we increase in steps of 15
      }
    }

    /**
     * Regardless of any timing issues, we always want the list
     * to respond to browsing (even if we're not loading an item.
     */
    this.toggleListItem();

    /**
     * Here we now set the timeout and assign it to this._clickTimer
     */
    this._clickTimer = setTimeout(function() {

      // we now perform the actual loading of the item
      that.selectItem();

      // and we reset our delay to zero
      if (that._timerInc > 0) {
      	that._clickTimer = false;
      } else {
      	that._cancelClickTimer = setTimeout(function() {
        	that._clickTimer = false;
        }, 150);
      }
      that._timerInc = 0;

    }, this._timerInc); // we use the delay for setTimeout()

  },

  /** the following functions are irrelevant for the problemsolving above **/

  toggleListItem: function() {
    $('#status').prepend($('<div />').text('You toggled a list item ... in ' + this._timerInc + ' ms'));
  },

  selectItem: function(id) {
    $('#loader').show();
    setTimeout(function() {
      $('#loader').hide();
    }, 800);
  }

};

$('#clickZone').on('click', function() {
  _simulator.triggerItemClick();
});
#clickZone {
  background: #369;
  color: #fff;
  width: 420px;
  height: 80px;
  text-align: center;
  line-height: 80px;
  cursor: pointer;
  -ms-user-select: none;
  -moz-user-select: -moz-none;
  -webkit-user-select: none;
  user-select: none;
  font-family: Arial;
}

#status {
  line-height: 20px;
  margin-top: 10px;
  font-family: Arial;
  font-size: 12px;
  background: #936;
  color: #fff;
  padding: 7px 10px;
}

#status > div {
  padding: 2px 0 4px;
  border-bottom: 1px dashed #ddd;
}

#status > div:last-child {
  border-bottom: 0;
}

#loader,
#notice {
  display: none;
  margin-top: 10px;
  width: 320px;
  padding: 10px 15px;
  background: #ddd;
  font-family: Arial;
  font-size: 11px;
  text-align: center;
}

#notice {
  background: lightblue;
  font-size: 14px;
  color: #333;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="clickZone">
  CLICK ME TO SIMULATE LIST BROWSING
</div>
<div id="loader">
  &#10003; &nbsp;Browsing ended, loading item!
</div>
<div id="status">
  <div>
    Waiting for something to happen ...
  </div>
</div>

答案 1 :(得分:0)

我们假设您希望每个请求之间至少有100毫秒,但如果最后一个请求超过100毫秒,则应立即获取数据。

以下应该做的技巧(伪代码):

首先,创建以下变量(需要从以下函数访问的变量)

delayLoad       // (boolean) - initial false
currentItem     // initial 1
lastItemFetched // initial 1

在第一次页面加载时,应显示第1项(&#39;项目&#39;表示每页一个大项目,或整页行 - 如果项目应该是20行页面上的一行一些额外代码将需要跟踪需要获取哪些行 - 但显然,当用户在计时器(延迟)运行时一直按下向下按钮时,至少内部位置应该改变,以给予更快的感觉滚动)。

OnArrowDown:
    currentItem++ // and check boundaries
    getData()
OnArrowUp:
    currentItem-- // and check boundaries
    getData()

getData()
    if not delayLoad
        getRealData()
    // else do nothing

getRealData()
    delayLoad = true
    lastItemFetched = currentItem // currentItem could change while
                                  // data is being fetched
    get itemdata for lastItemFetched from server
    on receive
        update data on page for lastItemFetched
        set 100ms timer 

onTimer
    if currentItem != lastItemFetched
        getRealData()
    else
        delayLoad = false

如果您确实希望延迟滚动,如果用户继续快速滚动,则可以执行以下操作:

delayLoad       // (boolean) - initial false
currentItem     // initial 1
lastItemFetched // initial 1
changeCount     // initial 0

OnArrowDown:
    currentItem++ // and check boundaries
    getData()
OnArrowUp:
    currentItem-- // and check boundaries
    getData()

getData()
    changeCount++
    if not delayLoad
        getRealData()
    // else do nothing

getRealData()
    delayLoad = true
    lastItemFetched = currentItem // currentItem could change while
                                  // data is being fetched
    get itemdata for lastItemFetched from server
    on receive
        update data on page for lastItemFetched
        set timer to 100 + changeCount * 5 // or some other number
                                 // and maybe set a max value for the total
        changeCount = 0

onTimer
    if currentItem != lastItemFetched
        getRealData()
    else
        delayLoad = false

为了加快滚动速度 - 假设有一百万个项目且用户一直按住向下箭头,你可以这样做:

OnArrowDown:
    currentItem++ // and check boundaries
    if changeCount > some_value // user keeps holding down the button
        currentItem += changeCount * some_factor // check boundaries
    getData()

但是你必须在getRealData()里面做一些额外的事情,changeCount重置为0。

这可以与实时更新页面上的currentItem号码结合使用。

或者,您可以一次获取多个项目并将它们存储在本地缓存中。

答案 2 :(得分:0)

我认为你的问题是,在第一次按键后你无法检测到第二次按键。延迟模式0,150,165,... 350ms是不现实的,因为实际请求需要实际的时间(而不是0ms)。

在一定时间范围内检测按键是一种可能的解决方案。如果在100ms内检测到第二次按键,则从0到150ms延迟。

_clickTimer: false,
_timerInc: 0,

// When was the last trigger action (ms)
_lastTriggerTime: 0,

// Time span after first trigger event, during which the _timerInc
// is advanced.
_initRestartInterval: 100,

// Time span after last trigger event, during which the _timerInc
// is advanced.
_currentRestartInterval: 100,

triggerItemClick: function(itemRef){

    var that=this;
    var itemId=$(itemRef).data('id');

    if(this._clickTimer){
        clearTimeout(this._clickTimer);
        this._clickTimer = false;
    }

    var _triggerTime = new Date().getTime();
    var _elapsed = _triggerTime - this._lastTriggerTime;
    this._lastTriggerTime = _triggerTime;

    if (_elapsed > this._currentRestartInterval) {
        this._timerInc = 0;
        this._currentRestartInterval = this._initRestartInterval;
    } else {
        if(this._timerInc == 0){
            this._timerInc = 150;
        } else {
            if(this._timerInc <= 350)
                this._timerInc += 15;
        }
        this._currentRestartInterval = this._timerInc;
    }

    this.toggleListItem(itemId);

    this._clickTimer=setTimeout(function(){

        // we now perform the actual loading of the item

        // :ws: And here is the problem with a simulation that
        // does not take any relevant time at all.

        // If _clickTimer is set to false, before the "loading"
        // has taken a noticable amount of time, the early reset
        // problem arises and _timerInc is always 0.
        that.selectItem(itemId);

        // In an asynchronous environment, the following should
        // take place in the result handler.
        if (that._timerInc > 0) {
            // next trigger event will reset _timerInc to 0
            that._lastTriggerTime = 0;
        }

    }, this._timerInc);
}

这是一个版本,可以正确模拟数据请求。它的行为与第一次点击的时间测量版本完全相同:但是,对于进一步的点击,它的行为会有所不同,因为每个延迟都会增加100毫秒的请求时间。这使得延迟有效地为100,250,265,...... 450ms。

_clickTimer: false,
_timerInc: 0,

// Is there still a request for data?
_pendingRequest: false,

triggerItemClick: function(itemRef){

    var that=this;
    var itemId=$(itemRef).data('id');

    var tooFast = this._clickTimer || this._pendingRequest;

    if(this._clickTimer){
        clearTimeout(this._clickTimer);
        this._clickTimer = false;
    }

    // asynchronous AJAX request simulated with a timer
    if(this._pendingRequest){
        clearTimeout(this._pendingRequest);
        this._pendingRequest = false;
    }

    if (! tooFast) {
        this._timerInc = 0;
    } else {
        if(this._timerInc == 0){
            this._timerInc = 150;
        } else {
            if(this._timerInc <= 350)
                this._timerInc += 15;
        }
    }

    this.toggleListItem(itemId);

    this._clickTimer=setTimeout(function(){

        // we now perform the actual loading of the item
        // :ws: assuming it will take 100ms
        that._pendingRequest = setTimeout(function(){
            that.selectItem(itemId);
            that._pendingRequest = false;
        }, 100);

        that._clickTimer = false;

    }, this._timerInc);
}