Animations under single threaded JavaScript

前端 未结 4 703
既然无缘
既然无缘 2021-02-14 18:04

JavaScript is a single threaded language and therefore it executes one command at a time. Asynchronous programming is being implemented via Web APIs (DOM fo

4条回答
  •  渐次进展
    2021-02-14 18:40

    Could someone please explain to me the underlying mechanism of the above? Since .hide() has not yet finished (the animation lasts 17 seconds) and JS engine is dealing with it and it is capable of executing one command at a time, in which way does it go to the next line and continues to run the remaining code?

    jQuery.fn.hide() internally calls jQuery.fn.animate which calls jQuery.Animation which returns a jQuery deferred.promise() object; see also jQuery.Deferred()

    The deferred.promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request.

    For description of Promise see Promises/A+ , promises-unwrapping , Basic Javascript promise implementation attempt ; also , What is Node.js?


    jQuery.fn.hide:

    function (speed, easing, callback) {
        return speed == null || typeof speed === "boolean" 
        ? cssFn.apply(this, arguments) 
        : this.animate(genFx(name, true), speed, easing, callback);
    }
    

    jQuery.fn.animate:

    function animate(prop, speed, easing, callback) {
        var empty = jQuery.isEmptyObject(prop),
            optall = jQuery.speed(speed, easing, callback),
            doAnimation = function () {
            // Operate on a copy of prop so per-property easing won't be lost
            var anim = Animation(this, jQuery.extend({},
            prop), optall);
    
            // Empty animations, or finishing resolves immediately
            if (empty || jQuery._data(this, "finish")) {
                anim.stop(true);
            }
        };
        doAnimation.finish = doAnimation;
    
        return empty || optall.queue === false ? this.each(doAnimation) : this.queue(optall.queue, doAnimation);
    }
    

    jQuery.Animation:

    function Animation(elem, properties, options) {
        var result, stopped, index = 0,
            length = animationPrefilters.length,
            deferred = jQuery.Deferred().always(function () {
            // don't match elem in the :animated selector
            delete tick.elem;
        }),
            tick = function () {
            if (stopped) {
                return false;
            }
            var currentTime = fxNow || createFxNow(),
                remaining = Math.max(0, animation.startTime + animation.duration - currentTime),
            // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
            temp = remaining / animation.duration || 0,
                percent = 1 - temp,
                index = 0,
                length = animation.tweens.length;
    
            for (; index < length; index++) {
                animation.tweens[index].run(percent);
            }
    
            deferred.notifyWith(elem, [animation, percent, remaining]);
    
            if (percent < 1 && length) {
                return remaining;
            } else {
                deferred.resolveWith(elem, [animation]);
                return false;
            }
        },
            animation = deferred.promise({
            elem: elem,
            props: jQuery.extend({},
            properties),
            opts: jQuery.extend(true, {
                specialEasing: {}
            },
            options),
            originalProperties: properties,
            originalOptions: options,
            startTime: fxNow || createFxNow(),
            duration: options.duration,
            tweens: [],
            createTween: function (prop, end) {
                var tween = jQuery.Tween(elem, animation.opts, prop, end, animation.opts.specialEasing[prop] || animation.opts.easing);
                animation.tweens.push(tween);
                return tween;
            },
            stop: function (gotoEnd) {
                var index = 0,
                // if we are going to the end, we want to run all the tweens
                // otherwise we skip this part
                length = gotoEnd ? animation.tweens.length : 0;
                if (stopped) {
                    return this;
                }
                stopped = true;
                for (; index < length; index++) {
                    animation.tweens[index].run(1);
                }
    
                // resolve when we played the last frame
                // otherwise, reject
                if (gotoEnd) {
                    deferred.resolveWith(elem, [animation, gotoEnd]);
                } else {
                    deferred.rejectWith(elem, [animation, gotoEnd]);
                }
                return this;
            }
        }),
            props = animation.props;
    
        propFilter(props, animation.opts.specialEasing);
    
        for (; index < length; index++) {
            result = animationPrefilters[index].call(animation, elem, props, animation.opts);
            if (result) {
                return result;
            }
        }
    
        jQuery.map(props, createTween, animation);
    
        if (jQuery.isFunction(animation.opts.start)) {
            animation.opts.start.call(elem, animation);
        }
    
        jQuery.fx.timer(
        jQuery.extend(tick, {
            elem: elem,
            anim: animation,
            queue: animation.opts.queue
        }));
    
        // attach callbacks from options
        return animation.progress(animation.opts.progress).done(animation.opts.done, animation.opts.complete).fail(animation.opts.fail).always(animation.opts.always);
    }
    

    When .hide() is called , a jQuery.Deferred() is created that processes the animation tasks.

    This is the reason console.log() is called.

    If include start option of .hide() can review that .hide() begins before console.log() is called on next line, though does not block the user interface from performing asynchronous tasks.

    $("#mybox").hide({
      duration:17000,
      start:function() {
        console.log("start function of .hide()");
      }
    });
    console.log("Previous command has not yet terminated!");
    
    
    mybox

    Native Promise implementation

    function init() {
    
      function $(id) {
        return document.getElementById(id.slice(1))
      }
    
      function hide(duration, start) {
        element = this;
        var height = parseInt(window.getComputedStyle(element)
                     .getPropertyValue("height"));
        
        console.log("hide() start, height", height);
    
        var promise = new Promise(function(resolve, reject) {
          var fx = height / duration;
          var start = null;
          function step(timestamp) {        
            if (!start) start = timestamp;
            var progress = timestamp - start;
            height = height - fx * 20.5;        
            element.style.height = height + "px";
            console.log(height, progress);
            if (progress < duration || height > 0) {
              window.requestAnimationFrame(step);
            } else {
              resolve(element);
            }
          }
          window.requestAnimationFrame(step);
        });
        return promise.then(function(el) {
          console.log("hide() end, height", height);
          el.innerHTML = "animation complete";
          return el
        })
      }
      
      hide.call($("#mybox"), 17000);
      console.log("Previous command has not yet terminated!");
      
    }
    
    window.addEventListener("load", init)
    #mybox {
      position: relative;
      height:200px;
      background: blue;
    }

提交回复
热议问题