33

Is it possible to chain setTimout functions to ensure they run after one another?

5 Answers 5

61

Three separate approaches listed here:

  1. Manually nest setTimeout() callbacks.
  2. Use a chainable timer object.
  3. Wrap setTimeout() in a promise and chain promises.

Manually Nest setTimeout callbacks

Of course. When the first one fires, just set the next one.

setTimeout(function() {
    // do something
    setTimeout(function() {
        // do second thing
    }, 1000);
}, 1000);

Chainable Timer Object

You can also make yourself a little utility object that will let you literally chain things which would let you chain calls like this:

delay(fn1, 400).delay(fn2, 500).delay(fn3, 800);

function delay(fn, t) {
    // private instance variables
    var queue = [], self, timer;
    
    function schedule(fn, t) {
        timer = setTimeout(function() {
            timer = null;
            fn();
            if (queue.length) {
                var item = queue.shift();
                schedule(item.fn, item.t);
            }
        }, t);            
    }
    self = {
        delay: function(fn, t) {
            // if already queuing things or running a timer, 
            //   then just add to the queue
        	  if (queue.length || timer) {
                queue.push({fn: fn, t: t});
            } else {
                // no queue or timer yet, so schedule the timer
                schedule(fn, t);
            }
            return self;
        },
        cancel: function() {
            clearTimeout(timer);
            queue = [];
            return self;
        }
    };
    return self.delay(fn, t);
}

function log(args) {
    var str = "";
    for (var i = 0; i < arguments.length; i++) {
        if (typeof arguments[i] === "object") {
            str += JSON.stringify(arguments[i]);
        } else {
            str += arguments[i];
        }
    }
    var div = document.createElement("div");
    div.innerHTML = str;
    var target = log.id ? document.getElementById(log.id) : document.body;
    target.appendChild(div);
}


function log1() {
	  log("Message 1");
}
function log2() {
	  log("Message 2");
}
function log3() {
	  log("Message 3");
}

var d = delay(log1, 500)
    .delay(log2, 700)
    .delay(log3, 600)

Wrap setTimeout in a Promise and Chain Promises

Or, since it's now the age of promises in ES6+, here's similar code using promises where we let the promise infrastructure do the queuing and sequencing for us. You can end up with a usage like this:

Promise.delay(fn1, 500).delay(fn2, 700).delay(fn3, 600);

Here's the code behind that:

// utility function for returning a promise that resolves after a delay
function delay(t) {
    return new Promise(function (resolve) {
        setTimeout(resolve, t);
    });
}

Promise.delay = function (fn, t) {
    // fn is an optional argument
    if (!t) {
        t = fn;
        fn = function () {};
    }
    return delay(t).then(fn);
}

Promise.prototype.delay = function (fn, t) {
    // return chained promise
    return this.then(function () {
        return Promise.delay(fn, t);
    });

}

function log(args) {
    var str = "";
    for (var i = 0; i < arguments.length; i++) {
        if (typeof arguments[i] === "object") {
            str += JSON.stringify(arguments[i]);
        } else {
            str += arguments[i];
        }
    }
    var div = document.createElement("div");
    div.innerHTML = str;
    var target = log.id ? document.getElementById(log.id) : document.body;
    target.appendChild(div);
}

function log1() {
    log("Message 1");
}

function log2() {
    log("Message 2");
}

function log3() {
    log("Message 3");
}

Promise.delay(log1, 500).delay(log2, 700).delay(log3, 600);

The functions you supply to this version can either by synchonrous or asynchronous (returning a promise).

Sign up to request clarification or add additional context in comments.

10 Comments

I apologise for my dumb questions, but are there any internet browsers which process javascript functions concurrently, (including mobile), and if so does this ensure that they don't run concurrently?
All browser javascript is single threaded. One thread of execution runs at a time so sequential statements are always run one after the other. Two pieces of javascript NEVER run concurrently.
Added a chainable timer object with queuing and a cancel method.
Can the fn argument of delay be a function that takes an argument like function logMessage(message) { log(message); }? And if so, how could I pass in the message argument?
Thanks, bind is sweet and works. For anyone whose interested here jsfiddle.net/efbbrown/ypa25gsc/1 is how you can bind inputs to the fn argument.
|
12

Inspired by the Promise-based solution in jfriend00's answer, I demonstrated a shorter version:

Promise.resolve()
  .then(() => delay(400))
  .then(() => log1())
  .then(() => delay(500))
  .then(() => log2())
  .then(() => delay(800))
  .then(() => log3());

function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

function log1() {
  console.log("Message 1");
}

function log2() {
  console.log("Message 2");
}

function log3() {
  console.log("Message 3");
}

1 Comment

This works better for me, the code is as concise as it gets.
6

With ES6, this is pretty simple using async/await. This is also very easy to read and a little upgrade to the promises answer.

// Expect to be async
    async function timePush(...arr){
        function delay(t){
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    resolve();
                },t)
            })
        }
        // for the length of this array run a delay
        // then log, you could always use a callback here
        for(let i of arr){
            //pass the items delay to delay function
            await delay(i.time);
            console.log(i.text)
        }
    }
    
    
    timePush(
        {time:1000,text:'hey'},
        {time:5000,text:'you'},
        {time:1000,text:'guys'}
    );

Comments

1

I have encountered the same issue. My solution was to call self by setTimeout, it works.

let a = [[20,1000],[25,5000],[30,2000],[35,4000]];

function test(){
  let b = a.shift();
  console.log(b[0]);
  if(a.length == 0) return;
  setTimeout(test,b[1]);
}

the second element in array a is time to be delayed

Comments

1

Using async / await with @Penny Liu example:

(async() => {
  await delay(400)
  log1()
  await delay(500)
  log2()
  await delay(800)
  log3()
})()

async function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

function log1() {
  console.log("Message 1");
}

function log2() {
  console.log("Message 2");
}

function log3() {
  console.log("Message 3");
}

1 Comment

I like this style of delay call, but async like a virus: one async, all (call stack functions) need be async

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.