Skip to content Skip to sidebar Skip to footer

Executing JavaScript In Order

I'm trying to print to a div one character at a time. It works, however, it runs both lines at the same time so that all I get is a jumbled mess. How can I make the commands run on

Solution 1:

You have two asynchronous functions that you start one right after the other so they run in parallel. If you want them to run serially, then you have to create some sort of notification when the first one is done so you then can trigger the start of the next one and so on. This can be done a number of ways (I show three ways below). You can use a callback, you can use promises and you can avoid having to sequence the async operations at all.

Method #1 - Completion Callback

Here's adding a callback to your print function and then use that to trigger the next string to go and then changing your iteration of strings to use the callback:

Working demo: http://jsfiddle.net/jfriend00/Lyu5V/

$(function() {
    function print(str, fn) {
        var i = 0;
        var items = $(".content");

        function write() {
            setTimeout(function() {
                if (i < str.length) {
                    items.html(items.html().replace("_", "") + str.charAt(i) + "_");
                    i++;
                    write();
                } else {
                    fn();
                }
            }, 100);
        }
        write();
    }

    var data = [
            "I am the egg man...",
            "I am the walrus"
        ];

    var i = 0;
    function next() {
        if (i < data.length) {
            print(data[i++], next);
        }
    }
    next();
});

FYI, there's really no reason to split your string into an array. You can access the individual characters of the string with the .charAt(index) method.


Method #2 - Promises - use .then() to sequence operations

And, here's a version of your code using promises instead of passing the callback:

Working demo: http://jsfiddle.net/jfriend00/97UtX/

$(function() {
    function print(str) {
        var i = 0, items = $(".content"), def = $.Deferred();

        function write() {
            setTimeout(function() {
                if (i < str.length) {
                    items.html(items.html().replace("_", "") + str.charAt(i) + "_");
                    i++;
                    write();
                } else {
                    def.resolve();
                }
            }, 100);
        }
        write();
        return def.promise();
    }

    var data = [
            "I am the egg man..",
            "I am the walrus"
    ];

    data.reduce(function(p, item) {
        return p.then(function() {
            return print(item);
        });
    }, $.Deferred().resolve());

});

Method #3 - Avoid sequencing by combining data into one single operation

And, if you want to simplify/DRY it up a bit, you can do this which avoids having to sequence the successive operations by just turning it into one longer operation and I made a few simplifications to your code:

Working demo: http://jsfiddle.net/jfriend00/TL8pP/

$(function() {
    function print(str) {
        var i = 0, items = $(".content");

        function write() {
            setTimeout(function() {
                if (i < str.length) {
                    items.html(items.html().replace("_", "") + str.charAt(i) + "_");
                    i++;
                    write();
                }
            }, 100);
        }
        write();
    }

    var data = [
            "I am the egg man..",
            "I am the walrus"
    ];
    print(data.join(""));

});

Solution 2:

This is based on jfriend's answer, but it uses primitives with promises rather than promises at a high level. I believe this makes for cleaner code.

First, let's write a function that represents a delay with promises:

function delay(ms){ // generic delay function
     var d = $.Deferred();
     setTimeout(d.resolve, ms);
     return d;
}

Next, let's use promises to their fullest

var delay100 = delay.bind(null, 100); // a 100 ms delay

function write(el, str, initial) { // write a single word
    return [].reduce.call(str, function (prev, cur) { // reduce is generic
        return prev.then(delay100).then(function (letter) {
            initial += cur;
            el.text(initial + "_");
        });
    }, $.when());
}
data.reduce(function (p, item) {
    return p.then(function () { // when the last action is done, write the next
        return write($(".content"), item, ""); // might want to cache this
    });
}, $.ready.promise()); // we don't need `$(function(){})` 

Here is a fiddle illustrating this solution: http://jsfiddle.net/feq89/

Just for fun, here is an ES6 solution without jQuery:

var delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

var write = (el, str, initial) => 
    [].reduce.call(str, (prev, cur) =>
        prev.then(() => delay(100)).then(() => {
          initial += cur;
          el.textContent = initial + "_";
        });
    }, Promise.resolve());

var content = document.querySelector(".content");
data.reduce((p, item) => p.then(() => write(content, item, "")));

Solution 3:

bobef is right.

Add another argument to print, which is a callback. And you should call the print method inside another recursive method instead a loop.

function print(str, _cb){
    var arr = str.split("");
    var i = 0;
    function write(){
        setTimeout(function(){
            if(i < arr.length){
                var cont = $(".content").html();
                cont = cont.replace("_","");
                $(".content").html(cont + arr[i] + "_");
                i++;
                write();
            } else {
                _cb();
            }
        },30);
    }
    write();
}

var str = [
    "I am the egg man",
    "I am the walrus"
];

var j = 0,
    callback = function () {
        if(j < str.length){
            print (str[j++], callback);
        }
    };

callback();

Post a Comment for "Executing JavaScript In Order"