0
\$\begingroup\$

I have a short function written in JavaScript using jQuery which takes a message and appends it to a div. It does this one character at a time with a small delay between characters, and then returns the time that it took it will take for the function to finish running.

const log = $('#log')

function logMessage(message, type) {
    log.append('<div></div>')
    log.children().last().addClass(type);
    for (let charIndex = 0; charIndex < message.length; charIndex++)
        setTimeout(() => {
            log.children().last().append(message[charIndex])
        }, charIndex * 10)
    return message.length * 10
}

This code feels a little bit unoptimized but I'm not sure what would be a better of going about doing this.

\$\endgroup\$
2
\$\begingroup\$

You're using the right approach with the log - you're saving it in a variable so you don't have to select the element again. Do the same thing with the newly appended <div>.

const log = $('#log')

function logMessage(message, type) {
    const newDiv = $('<div></div>')
      .addClass(type)
      .appendTo(log);
    for (let charIndex = 0; charIndex < message.length; charIndex++) {
        setTimeout(() => {
            newDiv.append(message[charIndex])
        }, charIndex * 10)
    }
}

logMessage('so🌐mething with lots and lots and lots and lots of characters', 'sometype');
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="log"></div>

A tiny issue with iterating over the indicies of the string manually is that characters composed of surrogate pairs (like 🌐) will take twice as long to print as others, and if you were to slow down the animation, you'd get a broken character before the second part gets fully rendered:

const log = $('#log')

function logMessage(message, type) {
    const newDiv = $('<div></div>')
      .addClass(type)
      .appendTo(log);
    for (let charIndex = 0; charIndex < message.length; charIndex++) {
        setTimeout(() => {
            newDiv.append(message[charIndex])
        }, charIndex * 500)
    }
}

logMessage('so🌐mething with lots and lots and lots and lots of characters', 'sometype');
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="log"></div>

It also might be useful to return a Promise indicating that the message has been fully logged, instead of a number indicating how long to wait before the message is finished.

To tweak this, and to avoid having to touch the string indicies at all (since you only really care about the characters, not the indicies of characters) would be to invoke the string's iterator and await a Promise inside the loop:

const log = $('#log');
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

async function logMessage(message, type) {
    const newDiv = $('<div></div>')
      .addClass(type)
      .appendTo(log);
    for (const char of message) {
        newDiv.append(char);
        await delay(500);
    }
}

logMessage('so🌐mething', 'sometype')
  .then(() => {
      console.log('message fully printed');
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="log"></div>

I'm a bit worried about the log ID - you have both a global identifier log which refers to the HTMLElement and exists on the window. Then the code shadows the global identifier and creates a log which refers to the jQuery collection. It's an unlikely source of bugs, but I'd use a class instead of an ID.

I notice you're not using semicolons. If this is a deliberate style choice and you consider yourself an expert who can avoid the pitfalls of Automatic Semicolon Insertion, that's just fine. Otherwise, you may occasionally run into very confusing bugs due to a statement unexpectedly crossing a linebreak, and I'd recommend using them.

\$\endgroup\$
0
\$\begingroup\$

jQuery?

Is there a need for Jquery in this day and age. Interest in jQuery and demand for jQuery experience has been on the decline for years. Apart from the need to load a bulky library before you can even start access to the page via it, most of the functionality it provides is available via the much faster native API's. The main concern with jQuery is that you are not gaining experience using the native DOM API's

Avoid functions inside loops.

There is so much wrong with the following two expressions it painful to see.

for (let charIndex = 0; charIndex < message.length; charIndex++)
    setTimeout(() => {
        log.children().last().append(message[charIndex])
    }, charIndex * 10)
  • Functions in loop are needless power hungry resource drains. Each iteration is creating a new function, closure, event stack entry, memory allocations (cleanable only via deferred GC). This is a lot of RAM and resources for a single character.

    Yes its safe because you used let (part of why let is so bad). Even with let or const, functions inside loops are not good practice in terms of performance.

  • No guarantee that the timers will fire at the times given charIndex * 10

  • Would be better if you used the conventional for loop counter name i

  • You should end lines with ;. Unless you are fully clued in on how ASI works you should use semicolons. Those that learnt how API works seldom adopt a style that does not use ;

  • Did not delimit the loops code block with { }.

    Yes you can do without them as you can shoot yourself in the foot, but I strongly advice against either.

Timers

Javascript timers are throttled and thus you can not rely on them firing on time.

The reason timers are throttles is to extend battery life. Tests have shown that throttling timers can extend battery life up to 2 hours

Your function returns a time. This is only an estimation, when the actual timers have all fired will depend on many factors.

Much of the throttling is performed when a tab is not focused. However even if you force the browser to respect timers (via browser flags not something you would ask a client to do) JavaScript execution is blocking, your own code will mean that timers will not fire when you want.

How to handle timed code

The best way (and also best for battery life) is to use only one timer. That timer then uses time to workout what to do.

You can get a high resolutions time since page load via the Performance interface. Eg performance.now() will return time in milliseconds with an accuracy of up to 1 microsecond (1/1,000,000 second)

Don't Update display < 16.667ms

Browser display rates are 60FPS, displaying content at rates higher than this is pointless as anything displayed between frames will not be seen. Though it will still trigger re-flows (if you are sloppy) and force the display buffers to update (re-composite).

setTimeout and setInterval do not know anything about the display timing.

Use requestAnimationFrame when animating any type of page content, even if the update rates are low.

Why...

  • requestAnimationFrame came with power saving in mind
  • It cooperates with all the other page processes competing for time and visual content updates
  • It is smart and knows when content is visible, and will only update when it matters.
  • It is synced to the display so you know all the new content will be ready and visible for the next frame.
  • It provides a high res microsecond time to the callback function. Though I prefer to use performance.now due to some ambiguity regarding the time the argument represents.

Example timed text via requestAnimationFrame

The example shows a textLogger similar to yours that will display text at a given rate (in characters per second), updating a selected elements el display text.

Note that the center text is 100 chars per second matching your 10ms text.

Note When the text is updated as many characters as needed are display, it does not update one character at a time.

Click to start timed text.

function textLogger(text, rate, el) { // rate is chars per second
     const start = performance.now(), time = (text.length / rate) * 1000;
     (function tick() {
         const len = Math.round(text.length * (performance.now() - start) / time);
         if (len < text.length) {
             el.textContent = text.slice(0, len + 1);
             requestAnimationFrame(tick); 
         } else { 
             el.textContent = text;
         }
     })();
     return time;
}
addEventListener("click", () => {
    textLogger(
        "Testing text logger at rate 60 chars per second. abcdefghijklmnopqrstuvwxyz",
        60, logger1);
    textLogger(
        "Testing text logger at rate 100 chars per second. abcdefghijklmnopqrstuvwxyz",
        100, logger2);
    textLogger(
        "Testing text logger at rate 20 chars per second. abcdefghijklmnopqrstuvwxyz",
        20, logger3);  

});
Click to start text loggers....<br>
<div id="logger1"></div>
<div id="logger2"></div>
<div id="logger3"></div>

Personally good coding means doing all you can to benefit the client. Code that has little regard for the clients power is rude.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.