0

I've coded a tab function that switches between tabs after a set period of time, this seemingly worked perfectly however, it's been bought to my attention that sometimes it breaks.

After looking into setInterval and looking into the error itself it looks like setInterval after 20-30 minutes, trips up on itself and starts a 2nd timer. This causes 2 tabs to show at once.

I've read a bit online where essentially it's to do with Javascript being single threaded and it's just a fundamental flaw with setInterval that if it trips up it doesn't reset the timer it just creates a 2nd one. Is there anyway to add an error catch for this trip or a better way to run a repeating timeout function?

jQuery(document).ready(function() {
    function autoPlayTabs(tabTitle1, tabTitle2, tabTitle3, tabTitle4, tabContent1, tabContent2, tabContent3, tabContent4, tabLength, tabTitleMobile1, tabTitleMobile2, tabTitleMobile3, tabTitleMobile4) {
        var actualTabLength = tabLength * 4;
        var tabContainer = jQuery('.elementor-tabs');
        var allTabs = jQuery('.elementor-tabs .elementor-tabs-wrapper .elementor-tab-title');
        var allContent = jQuery('.elementor-tabs .elementor-tabs-content-wrapper .elementor-tab-content');
        var initialTabTimer = null;
        var tabTimer = null;

        tabTitle1.addClass('active'); tabContent1.addClass('active');

        console.log('Setting initialTabTimer');
        initialTabTimer = setTimeout(function(){
            tabTitle1.removeClass('active'); tabContent1.removeClass('active');
            tabTitle2.addClass('active'); tabContent2.addClass('active');

            console.log("Setting Timeout2");
            setTimeout(function(){
                tabTitle2.removeClass('active'); tabContent2.removeClass('active');
                tabTitle3.addClass('active'); tabContent3.addClass('active');

                console.log("Setting Timeout3");
                setTimeout(function(){
                    tabTitle3.removeClass('active'); tabContent3.removeClass('active');
                    tabTitle4.addClass('active'); tabContent4.addClass('active');
                    console.log("Executing Timeout3 Function");
                }, tabLength);
            }, tabLength);
        }, tabLength);

        console.log("Setting tabTimer");
        tabTimer = setInterval(function(){
            tabTitle4.removeClass('active'); tabContent4.removeClass('active');
            tabTitle1.addClass('active'); tabContent1.addClass('active');

            console.log("Setting TimeoutB");
            setTimeout(function(){
                tabTitle1.removeClass('active'); tabContent1.removeClass('active');
                tabTitle2.addClass('active'); tabContent2.addClass('active');

                console.log("Setting TimeoutC");
                setTimeout(function(){
                    tabTitle2.removeClass('active'); tabContent2.removeClass('active');
                    tabTitle3.addClass('active'); tabContent3.addClass('active');

                    console.log("Setting TimeoutD");
                    setTimeout(function(){
                        tabTitle3.removeClass('active'); tabContent3.removeClass('active');
                        tabTitle4.addClass('active'); tabContent4.addClass('active');
                        console.log("Executing TimeoutD Function");
                    }, tabLength);
                }, tabLength);
            }, tabLength);
        }, actualTabLength);

        allTabs.click(function() {
            if (initialTabTimer !== null) {
               clearTimeout(initialTabTimer);
               initialTabTimer = null;
               console.log("Cleared initialTabTimer");
            }

            if (tabTimer !== null) {
               clearInterval(tabTimer);
               tabTimer = null;
               console.log("Cleared tabTimer");
            } else {
                console.log("Did not need to clear tabTimer");
            }
            
            allTabs.removeClass('active');
            allContent.removeClass('active');
            tabContainer.addClass('tabsManual');
        });
    }

    if(homeTabTitle1.length > 0){
    console.log("Calling AutoPlayTabs Homepage");
    autoPlayTabs(homeTabTitle1,homeTabTitle2,homeTabTitle3,homeTabTitle4,homeTabContent1,homeTabContent2,homeTabContent3,homeTabContent4,homeTabLength);
    }
});

Below is a console.log of every setTimeout called from it working, to breaking to fixing itself:

Calling AutoPlayTabs Homepage
Setting initialTabTimer
Setting tabTimer
Setting Timeout2
Setting Timeout3
Executing Timeout3 Function

//This console.log block ran 30 times without error//
Setting TimeoutB
Setting TimeoutC
Setting TimeoutD
Executing TimeoutD Function
//This console.log block ran 30 times without error//

//It Breaks here after a total of 24 minutes//
Setting TimeoutB
Setting TimeoutC
Setting TimeoutD
Setting TimeoutB //For some reason TimeoutB has been fired again
Executing TimeoutD Function

Setting TimeoutC
Setting TimeoutD
Setting TimeoutB
Executing TimeoutD Function

Setting TimeoutC
Setting TimeoutD
Executing TimeoutD Function

//It fixed itself here after 2.4 minutes//

Setting TimeoutB
Setting TimeoutC
Setting TimeoutD
Executing TimeoutD Function
Daniel Vickers
  • 1,054
  • 1
  • 12
  • 32
  • Can you provide a working example of the issue, including all relevant HTML and CSS. I ask as JS timers are notoriously inaccurate, especially when run over long periods of time. Your problem can most likely be avoided and solved in a simpler and more performant manner using CSS animations, or at the very least without needing 7 nested timers. – Rory McCrossan Jul 21 '21 at 14:07
  • 3
    `setInterval()` doesn't "trip up". – Pointy Jul 21 '21 at 14:08
  • @RoryMcCrossan the HTML structure is just Elementor's tab widget, and the only CSS I use is display block and display none for the active/not active classes that get applied pretty basic stuff. – Daniel Vickers Jul 21 '21 at 14:09
  • @RoryMcCrossan message me and I will link you to the staging of this to see that it works as expected then randomly breaks after seemingly random and lengthy times. – Daniel Vickers Jul 21 '21 at 14:11
  • 1
    Is there any change `autoPlayTabs()` is firing more than once? If you can reproduce the error, put a console.log in that function so you can observe if the function is being called again ... – Kinglish Jul 21 '21 at 14:13
  • @Kinglish I'll stick a console log in there just incase I've reproduced it once but like I said just random lengths of time, IMO it doesn't matter no one sits on a page ready tabs for 30+ mins but oh well haha – Daniel Vickers Jul 21 '21 at 14:14
  • 3
    Timers are inaccurate in that they don't fire precisely when you ask them to (necessarily). However a `setInterval()` setup that spontaneously started another copy of itself would be a critical browser bug. – Pointy Jul 21 '21 at 14:15
  • @Pointy well you see the function, and can see if it's coded correctly. It only fires once and works perfectly as expected for at least 20 minutes then some time after that randomly trips up. – Daniel Vickers Jul 21 '21 at 14:20
  • @Kinglish will get back to you on that console log buddy as added it in :) – Daniel Vickers Jul 21 '21 at 14:20
  • 2
    Looks like it's caused by a mix of `setInterval` and `setTimeout` - if you consider a 1% variance in the actual timeouts, then you can see it won't be that long until `setInterval(.., t * 4)` will run more often the 4x `setTimeout(t)` - change your outer setInterval to a setTimeout and restart it after the 4th. Alternatively, **refactor so you only need one timeout** and the problem goes away (along with making your code much easier to maintain) – freedomn-m Jul 21 '21 at 15:03
  • Could you pop an updated answer of that @freedomn-m as I chose ```setInterval``` at the start as that loops where as ```setTimeout``` only fires the once hence the combination of the 2, if you can write an answer where ```setTimeout``` fires over and over then that's awesome! – Daniel Vickers Jul 21 '21 at 15:07

3 Answers3

2

If you just simplify your logic to only have a single interval running, all your syncronisation problems go away

 const tabs = [
  {title: tabTitle1, content: tabContent1},
  {title: tabTitle2, content: tabContent2},
  {title: tabTitle3, content: tabContent3},
  {title: tabTitle4, content: tabContent4}
 ];
 
 let curr = 0;
 let tab = tabs[curr];
 tab.title.addClass("active");
 tab.content.addClass("active");

 var timerInterval = setInterval(function(){     
        tab.title.removeClass("active");
        tab.content.removeClass("active");
        curr = ++curr % tabs.length;
        tab = tabs[curr];
        tab.title.addClass("active");
        tab.content.addClass("active");
 }, tabLength);

Live example

jQuery(document).ready(function() {
  function autoPlayTabs(tabTitle1, tabTitle2, tabTitle3, tabTitle4, tabContent1, tabContent2, tabContent3, tabContent4, tabLength, tabTitleMobile1, tabTitleMobile2, tabTitleMobile3, tabTitleMobile4) {
    var actualTabLength = tabLength * 4;
    var tabContainer = jQuery('.elementor-tabs');
    var allTabs = jQuery('.elementor-tabs .elementor-tabs-wrapper .elementor-tab-title');
    var allContent = jQuery('.elementor-tabs .elementor-tabs-content-wrapper .elementor-tab-content');
    var initialTabTimer = null;
    var tabTimer = null;
    
    const tabs = [
        {title: tabTitle1, content: tabContent1},
      {title: tabTitle2, content: tabContent2},
      {title: tabTitle3, content: tabContent3},
      {title: tabTitle4, content: tabContent4}
     ];
     
     let curr = 0;
     let tab = tabs[curr]
     tab.title.addClass("active")
     tab.content.addClass("active");
     var timerInterval = setInterval(function(){
     
            tab.title.removeClass("active")
            tab.content.removeClass("active");
        curr = ++curr % tabs.length
            tab = tabs[curr]
        tab.title.addClass("active")
            tab.content.addClass("active");
     }, tabLength)
    

    allTabs.click(function() {
      if (initialTabTimer !== null) {
        clearTimeout(initialTabTimer);
        initialTabTimer = null;
      }

      if (tabTimer !== null) {
        clearInterval(tabTimer);
        tabTimer = null;
      }

      allTabs.removeClass('active');
      allContent.removeClass('active');
      tabContainer.addClass('tabsManual');
    });
  }

  if ($("#homeTabTitle1")) {
    autoPlayTabs($("#homeTabTitle1"), $("#homeTabTitle2"), $("#homeTabTitle3"), $("#homeTabTitle4"), $("#homeTabContent1"), $("#homeTabContent2"), $("#homeTabContent3"), $("#homeTabContent4"), 1000);
  }
});
.active { background-color:red }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="homeTabTitle1">
  homeTabTitle1
</div>
<div id="homeTabTitle2">
  homeTabTitle2
</div>
<div id="homeTabTitle3">
  homeTabTitle3
</div>
<div id="homeTabTitle4">
  homeTabTitle4
</div>
<div id="homeTabContent1">
  homeTabContent1
</div>
<div id="homeTabContent2">
  homeTabContent2
</div>
<div id="homeTabContent3">
  homeTabContent3
</div>
<div id="homeTabContent4">
  homeTabContent4
</div>

Note I have not hooked up your "stop" logic, but all you need to do is clearInterval(timerInterval) in your click handler.

Daniel Vickers
  • 1,054
  • 1
  • 12
  • 32
Jamiec
  • 133,658
  • 13
  • 134
  • 193
  • now this looks like an answer I can get behind, let me implement and test. – Daniel Vickers Jul 21 '21 at 15:23
  • Will check back in 24 minutes to see if this broke or not, fingers crossed I'll research your method so I actually understand it whilst I wait. – Daniel Vickers Jul 21 '21 at 15:30
  • 1
    @DanielVickers I assure you theres nothing special about 24 minutes :D this will work for as long as the page is open. You were getting duplicate "active" tabs as your timeouts were going out of sync – Jamiec Jul 21 '21 at 15:30
  • As there's only one timer, it can't "trip over itself (or other timers)", so you could leave this forever – freedomn-m Jul 21 '21 at 15:42
  • @Jamiec I've updated my click code to be your timer however it does not stop the timer it keeps playing through do you know why? – Daniel Vickers Jul 21 '21 at 15:46
  • Never mind, would've been cache as it works fine now. – Daniel Vickers Jul 21 '21 at 15:48
1

As requested

Could you pop an updated answer of that using setInterval

You can refactor to a single setInterval(). The following is a concept - if your tabs are not next to each other in the HTML, then you can collate them up-front and loop through an array. You can also link your content to your tab using data- attributes.

This also allows your use to change the "active" tab (eg by clicking on it) and it will "auto play" to the next one each time - but you might want to reset the timer in that case.

var interval_time = 250;  // short time for demo

var tabs = $(".tab");

// single setInterval
var timer = setInterval(() => {

  // get the currently active, so no need to store what that was
  var active = tabs.filter(".active");
  active.removeClass("active");

  // get the next tab, if none, then loop back to the first
  var next = active.next(".tab");
  if (next.length == 0)
    next = tabs.first();
  next.addClass("active");

}, interval_time);

$("#stop").click(() => clearInterval(timer))
/* can show / hide
.tab { display:none; }
.tab.active { display:block; }
*/

/* or show all the tabs at once */
.tab { display:inline-block; color: #CCC; }
.tab.active { color: green }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='tab active'>tab 1</div>
<div class='tab'>tab 2</div>
<div class='tab'>tab 3</div>
<div class='tab'>tab 4</div>
<br/>
<button type='button' id='stop'>stop</button>
freedomn-m
  • 27,664
  • 8
  • 35
  • 57
-1

This is not setTimeout tripping up, it is setTimeout being called twice

To help document this, you could put console.logs in to show when each timer is being started. For simplicity, I am also getting rid of all the class adding and removals.

I am also curious where homeTabTitle1 comes from - could you clarify?

jQuery(document).ready(function() {
  function autoPlayTabs(tabTitle1, tabTitle2, tabTitle3, tabTitle4, tabContent1, tabContent2, tabContent3, tabContent4, tabLength, tabTitleMobile1, tabTitleMobile2, tabTitleMobile3, tabTitleMobile4) {
    var actualTabLength = tabLength * 4;
    var tabContainer = jQuery('.elementor-tabs');
    var allTabs = jQuery('.elementor-tabs .elementor-tabs-wrapper .elementor-tab-title');
    var allContent = jQuery('.elementor-tabs .elementor-tabs-content-wrapper .elementor-tab-content');
    var initialTabTimer = null;
    var tabTimer = null;

    console.log("Setting initialTabTimer")
    initialTabTimer = setTimeout(function() {

      console.log("Setting timer 2")
      setTimeout(function() {

        console.log("Setting timer 3")
        setTimeout(function() {

          console.log("Executing inner function 3")
        }, tabLength);
      }, tabLength);
    }, tabLength);

    console.log("Setting tabTimer")
    tabTimer = setInterval(function() {

      console.log("Setting timber B")
      setTimeout(function() {

        console.log("Setting timber C")
        setTimeout(function() {

          console.log("Setting timber D")
          setTimeout(function() {

            console.log("Executing inner function D")
          }, tabLength);
        }, tabLength);
      }, tabLength);
    }, actualTabLength);

    allTabs.click(function() {
      if (initialTabTimer !== null) {
        clearTimeout(initialTabTimer);
        initialTabTimer = null;
        console.log("Cleared initialTabTimer");
      } else {
        console.log("Did not need to clear initialTabTimer");
      }

      if (tabTimer !== null) {
        clearInterval(tabTimer);
        tabTimer = null;
        console.log("Cleared tabTimer");
      } else {
        console.log("Did not need to clear tabTimer");
      }

    });
  }

  if (homeTabTitle1) {
    console.log("calling AutoPlayTabs")
    autoPlayTabs(homeTabTitle1, homeTabTitle2, homeTabTitle3, homeTabTitle4, homeTabContent1, homeTabContent2, homeTabContent3, homeTabContent4, homeTabLength);
  }
});

Why might document ready fire more than once?

As pointed out here:

jQuery $(document).ready () fires twice

The commonest cause is something being manipulated in the DOM that causes the document to be re-rendered. The answerer suggests a solution.

enter image description here

Why do some people call setTimeout "unreliable"?

I think they mean you cannot rely on when the callback function will be called. I think they unreasonably expect setTimeout to be able to magically cause Javascript to run the code exactly at the desired time. In reality, setTimer simply puts the task on a list of tasks to be run when the main program has run out of things to do.

Typically, on a web page, there is almost nothing for the Javascript interpreter to do after the page is initially rendered. In that case, the setTimeout callbacks will be called at a reasonably accurate time.

However, if the web page has plenty of things for the Javascript interpreter to do, taking many seconds or even minutes of "thinking time" such as massive calculations, then setTimeout will appear to be unreliable at a superficial assessment. However it is not really unreliable, it is doing what it should do, which is delaying the callback until there is both free time, and sufficient time has passed.

If the Javascript code calls asynchronous functions, then that potentially frees up the interpreter to look at setTimeout events. So if the web page becomes ready (from a jQuery point of view) but then has several seconds of communication with other sites to get database entries etc then, as long as those database calls are asynchronous, setTimeout is able to fire its callback if needed.

setInterval and setTimeout are different

"For some reason timeoutB has been fired again"

enter image description here

The reason is that it is a setInterval, not a setTimeout. Was that intentional? setInterval will keep firing over and over.

ProfDFrancis
  • 8,816
  • 1
  • 17
  • 26
  • But why would it be called twice when it is only called once on page load? And that is the only time? I've added console logs in and will monitor until it breaks again but it's only being called once on document ready. ```homeTabTitle1``` is a check to make sure the tabs are on the page, this then runs the function. – Daniel Vickers Jul 21 '21 at 14:30
  • do you think going to another tab, then going back to that tab after half an hour or so would refresh the document ready state also? – Daniel Vickers Jul 21 '21 at 14:48
  • No I don't think so. You can test it, but my experience is that just changing tabs and coming back is not a new "ready" state. Nor is the passage of time relevant I think. it must be something that is actively happening to the DOM, caused by the code. – ProfDFrancis Jul 21 '21 at 15:00
  • updated my post with the console log, it ran fine for 24 minutes then broke for 2.4 mins then fixed itself lol – Daniel Vickers Jul 21 '21 at 15:05
  • Yes it's intentional, ```setInterval``` is setup to fire again every 24 seconds in order to loop the timeouts and works perfectly fine until it gets to 24 minutes where it then breaks and fixes itself after 2.4 minutes I think someones comment about 0.1s inaccuracy is the answer here because it's exactly 24 mins and 2.4 mins – Daniel Vickers Jul 21 '21 at 15:21