Search code examples
javascriptpromiserequestanimationframe

JS — FLIP technique and requestAnimationFrame, refactor to not be nested


I'm having some trouble understanding the requestAnimationFrame API. I think it makes the browser wait for the callback to complete until the browser triggers a repaint (or reflow?). Is that correct? If you call requestAnimationFrame within the callback, the browser will repaint and execute the new RAF's callback and then repaint again? If this is the case, how can I make two sequential calls to this API be more async/promisified rather than nesting them like my example below?

Here I have used the FLIP technique to insert a document fragment into the DOM and animate the height of a DOM element, it seems to work fine. I do not like the callback nature/nesting of the code. I struggle to understand how I can make this more async. Is it possible use a promise and make it thenable?

window.requestAnimationFrame(() => {

    this.appendChild(frag);
    this.style.height = `${rows * rowHeight}px`;

    const { top: after } = this.getBoundingClientRect();

    this.style.setProps({
        "transform"  : `translateY(${before - after}px)`,
        "transition" : "transform 0s"
    });

    window.requestAnimationFrame(() => {
        this.style.setProps({
            "transform"  : "",
            "transition" : "transform .5s ease-out"
        });

    });

});

So, are my preconceived notions about RAF correct? And how can the above code example be void of callbacks and not as nested?


Solution

  • I'm not sure to get your own explanation of requestAnimationFrame, but to put it simply, it is just a setTimeout(fn, the_time_until_painting_frame).

    To put it a little bit less simply, it puts fn in a list of callbacks that will all get executed when the next painting event loop will occur. This painting event loop is a special event loop, where browsers will call their own painting operations (CSS animations, WebAnimations etc. They will mark one event loop every n-th time to be such a painting frame. This is generally in sync with screen refresh-rate.

    So our rAF callbacks will get executed in this event loop, just before browser's execute its painting operations.


    Now, what you stumbled upon is how browser do calculate CSS transitions: They take the current computed values of your element.
    But these computed values are not updated synchronously. Browsers will generally wait this very painting frame in order to do the recalc, because to calculate one element, you need to recalculate all the CSSOM tree.

    Fortunately for you, there are some DOM methods which will trigger such a reflow synchronously, because they do need updated box-values, for instance, Element.offsetTop getter.
    So simply calling one of these methods after you did set the initial styles (including the transition one), you will be able to trigger your transition synchronously:

    _this.style.height = "50px";
    
    
    _this.style.setProperty("transform", "translateY(140px)");
    _this.style.setProperty("transition", "transform 0s");
    
    _this.offsetTop; // trigger reflow
    
    _this.style.setProperty("transform", "");
    _this.style.setProperty("transition", "transform .5s ease-out");
    #_this {
      background: red;
      width: 50px;
    }
    <div id="_this"></div>