Implement organic list browsing with arrow keys and smart lazy loading

Simple script

I have a list, I implemented a scan using the arrow keys (up, down) and every time I change the current list item, the database object is loaded via AJAX.

Sweet.

Problem

When the user browses the list very quickly, I do not want every request to leave. But the original request should go away instantly, of course.

My idea was to set a timeout using the variable as a delay, and after the initial loading of the element, increase this variable.

This works, but when the user stops browsing for a short time, but then continues, I still don’t want every request to leave.

So, I realized that the delay variable should be reasonably increased with each event view until the threshold is reached.

This organic approach will minimize the amount of unnecessary loading of items.

My decision

I have gone far. This piece of code (explanation below) will do the job with one main culprit :

After the first viewing and stopping session, the delay will automatically remain at the minimum value (2nd step) of 150 ms.

Of course, I tried to fix it, but, as you will see, this is an interesting, but probably quite general logical problem - and I think my general approach is wrong.

But I do not understand how to do this. The brain is not calculated. The computer says no.

the code

You can sift through my above example or go here for a fully functional simulator in jsFiddle .

If you select jsFiddle:

Click the button, the item will immediately load. Now wait a bit, press the button again, the initial load will be delayed. If you constantly press the button quickly, the load of the item will be displayed only when you finish your click.

Code example

We are inside an object literal just to let you know.

_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() } 

Explanation

On the first call: _clickTimer is false , _timerInc is 0 , so the first call will delay 0 for setTimeout() and _clickTimer . The item will be downloaded instantly.

The second call is provided that our timeout is still waiting for it to _clickTimer , _clickTimer will be cleared, the delay is set to 150 if 0 or increased by 15 if below 350 (threshold).

This works great if you keep browsing. The timer increases, the load starts only after stopping viewing for a good moment.

But after you stop, the next time you continue, _clickTimer will not be false (because setTimeout() assigns a counter to it), therefore, in turn, _timerInc will be immediately set to 150 . Thus, the first scan will result in a delay of 150 ms before anything loads.

Call me crazy or finicky, but the goal is not to delay this delay.

Of course you say: simple, set _clickTimer to false at the end of the setTimeout() close, so it gets reset after viewing and loading the element. Great, but this leads to a delay never exceeding 0 ms. Think about it, you will see.

I hope that this has been explained correctly and that someone's brain is more capable of finding a solution to this than mine.

+6
source share
3 answers

Perhaps this can be done in a very complex way using Promises . Since this is basically a sugar coating, I, however, thought that it should be possible to fix it directly, and I think I did.

Updated script . I added a delay in the text, so it was easier for me to debug the material, as well as some minor tidying up, but my actual changes are very small. This is described in detail below.

Your comment near the end was my first intuition:

Of course, you say: just set _clickTimer to false at the end of closing setTimeout() , so it gets reset after viewing and the element is loaded. Great, but this leads to a delay never exceeding 0 ms.

In fact, this would make the delay never exceed 0, because, unfortunately, we cannot quickly click this button (or quickly view it in a real application). But ... what if we only reset if the delay was not 0 ? So, if the timeout is turned off, but it went out after only 0 milliseconds, we remember that there was a timeout. If he left later, then there must be a real pause in viewing. This is easy to implement by adding a couple of lines to the timeout callback as follows.

 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() 

It seems to work exactly the way you want it, except that now the delay will be 0 ms, then 150 ms, then 0 ms, etc. if you wait between clicks long enough. This can be eliminated by adding an additional timeout in case the delay is 0 ms, which will reset the delay. Whenever a trigger fires (click in the demo mode while viewing the application), this timeout is canceled.

This together does everything the way you want, I think. For completeness, I also included the above script as a snippet here.

 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> 
+2
source

Say you want at least 100 ms between each request, but if the last request was more than 100 ms ago, the data should be received immediately.

The following should do the trick (pseudo-code):

First create the following variables (they should be available from the functions below)

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

In the first part of the page, point 1 must be indicated (with an "element" meaning one large element on the page, or a full page of lines - if the element should be one line on pages of 20 lines, additional code is required to save, keep track of which lines to retrieve, but, obviously, when the user continues to press the down button while the timer (delay) is running, at least the internal position should change to give a faster scroll feel).

 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 

If you really want a longer delay, if the user scrolls fast, you can do this:

 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 

To speed up scrolling even more - let's say there are a million elements, and the user continues to hold the down arrow, you can do something like this:

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

But you need to do something extra inside getRealData() , where changeCount is reset to 0.

This can be combined with updating the currentItem number on the page in real time.


As an alternative, you can take several items at once and save them in the local cache.

0
source

I think your problem is that you cannot detect the second key press after the first. The delay scheme 0, 150, 165, ... 350 ms is unrealistic, since real requests take a real period of time (not 0 ms).

Detecting a keystroke over a period of time is one possible solution. It goes from 0 to 150 ms delay if a second press is detected within 100 ms.

 _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); } 

Here is a version that correctly simulates a data request. It behaves in exactly the same way as the time-measuring version for the first click. However, for further clicks, it behaves differently because each delay has another 100 ΞΌs of request time. This makes the delays effectively 100, 250, 265, ... 450 ms.

 _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); } 
0
source

Source: https://habr.com/ru/post/1015429/


All Articles