单线程下的动画 JavaScript

Animations under single threaded JavaScript

JavaScript 是一种单线程语言,因此它一次执行一个命令。异步编程正在通过 Web APIs 实现(DOM 用于事件处理,XMLHttpRequest 用于 AJAX 调用,WindowTimers 用于 setTimeout ) 和由浏览器管理的 事件队列 。到目前为止,一切都很好!现在考虑以下非常简单的代码:

$('#mybox').hide(17000);
console.log('Previous command has not yet terminated!');
... 

有人可以向我解释一下上述的基本机制吗?由于 .hide() 还没有完成(动画持续 17 秒),JS 引擎正在处理它,它可以一次执行一个命令,它是通过什么方式转到下一行并继续 运行剩余代码?

如果你的答案是动画创造承诺问题保持不变:JavaScript如何处理更多同时不止一件事(执行动画本身,在承诺的情况下观察动画队列并继续执行后面的代码...)。

此外,如果 jQuery 中的 promise 必须 观察其父 Deferred 对象直到它被解析或拒绝,我无法解释它们是如何工作的 表示代码执行同时执行剩下的代码。在单线程方法中这怎么可能?我可以毫无问题地理解 AJAX 调用,因为我知道它们已从 JS 引擎中移除...

你在javascript中有几种功能: 阻塞和非阻塞。

非阻塞函数将 return 立即并且事件循环继续执行,同时它在后台工作等待调用回调函数(如 Ajax 承诺)。

动画依赖于 setInterval and/or setTimeout 和这两个方法 return 立即允许代码恢复。回调被推回事件循环堆栈,执行,主循环继续。

希望这会有所帮助。

You can have more information here or here

事件循环

JavaScript 使用所谓的 event loop。事件循环就像一个 while(true) 循环。

为简化起见,假设 JavaScript 有一个巨大的数组,用于存储所有事件。事件循环在这个事件循环中循环,从最旧的事件开始到最新的事件。也就是说,JavaScript 做这样的事情:

while (true) {
     var event = eventsArray.unshift();

     if (event) {
       event.process();
     }
}

如果在处理事件 (event.process) 期间触发了一个新事件(我们称之为 eventA),则新事件将保存在 eventsArray 中,并且当前的执行继续。当前事件处理完成后,处理下一个事件,依此类推,直到我们到达 eventA.

来到你的示例代码,

$('#mybox').hide(17000);
console.log('Previous command has not yet terminated!');

执行第一行时,会创建一个事件侦听器并启动一个计时器。假设 jQuery 使用 100 毫秒帧。创建一个100ms的定时器,带有回调函数。计时器在后台启动 运行ning(此实现在浏览器内部),同时控制权交还给您的脚本。因此,当计时器在后台 运行ning 时,您的脚本继续执行第二行。 100 毫秒后,计时器结束,并触发一个事件。此事件保存在上面的 eventsArray 中,不会立即执行。一旦您的代码执行完毕,JavaScript 检查 eventsArray 并发现有一个新事件,然后执行它。

然后是事件 运行,您的 div 或它所在的任何元素移动了几个像素,一个新的 100 毫秒计时器开始。

请注意,这是一个简化,而不是整个事情的实际工作。整个事情有一些复杂性,比如堆栈等等。请参阅 MDN 文章 here 了解更多信息。

tl;dr; 如果没有外部帮助,在严格的单线程环境中是不可能的。


我想我明白你的问题了。让我们解决一些问题:

JavaScript 总是同步的

语言规范中未定义异步 API。 Array.prototype.mapString.fromCharCode 等所有函数始终 运行 同步*。

代码会总是运行完成。代码不会停止 运行ning,直到被 return、隐式 return(到达代码末尾)或 throw(突然)终止。

a();
b();
c();
d(); // the order of these functions executed is always a, b, c, d and nothing else will 
     // happen until all of them finish executing

JavaScript 住在一个平台里

JavaScript 语言定义了一个概念,称为 host environment:

In this way, the existing system is said to provide a host environment of objects and facilities, which completes the capabilities of the scripting language.

在浏览器中 JavaScript 为 运行 的宿主环境称为 DOM 或文档对象模型。它指定您的浏览器 window 如何与 JavaScript 语言交互。例如,在 NodeJS 中,主机环境完全不同。

虽然所有 JavaScript 对象和函数 运行 同步完成 - 宿主环境可能公开其自身的函数,这些函数不一定在 JavaScript 中定义。它们没有标准 JavaScript 代码所具有的相同限制,并且可能会定义不同的行为 - 例如 document.getElementsByClassName 的结果是实时的 DOM NodeList,其行为与您的普通 JavaScript 代码:

var els = document.getElementsByClassName("foo"); 
var n = document.createElement("div");
n.className = "foo";
document.body.appendChild(n);
els.length; // this increased in 1, it keeps track of the elements on the page
            // it behaves differently from a JavaScript array for example. 

其中一些主机功能必须执行 I/O 操作,例如安排计时器、执行网络请求或执行文件访问。与所有其他 API 一样,这些 API 必须 运行 完成 。这些 API 由主机平台提供 - 它们调用功能 你的 代码没有 - 通常(但不一定)它们是用 C++ 编写的,并使用线程和操作系统设施 运行并发和并行地处理事情。这种并发性可以只是后台工作(如安排计时器)或实际并行性(如 WebWorkers - 同样是 DOM 的一部分而不是 JavaScript)。

因此,当您调用 DOM 上的操作(如 setTimeout),或应用导致 CSS 动画的 class 时,它不受您的代码所具有的相同要求的约束。它可以使用线程或操作系统 async io.

当您执行以下操作时:

setTimeout(function() {
   console.log("World");
});
console.log("Hello");

实际发生的是:

  • 主机函数 setTimeout 被调用时带有函数类型的参数。它将函数推入 queue in the host environment.
  • 同步执行console.log("Hello")
  • 所有其他同步代码是运行(注意,setTimeout 调用在这里是完全同步的)。
  • JavaScript 已完成 运行ning - 控制权已转移到主机环境。
  • 主机环境注意到它在定时器队列中有一些东西并且已经过去了足够的时间所以它调用它的参数(函数) - console.log("World") 被执行。
  • 函数中的所有其他代码 运行 同步。
  • 控制权交还给主机环境(平台)。
  • 主机环境中发生了其他事情(鼠标单击、AJAX 请求返回、计时器触发)。宿主环境调用用户传递给这些操作的处理程序。
  • 所有 JavaScript 都是同步 运行。
  • 等等等等...

您的具体情况

$('#mybox').hide(17000);
console.log('Previous command has not yet terminated!');

这里的代码是运行同步的。之前的命令 终止,但它实际上并没有做太多 - 而是它在平台上安排了一个回调(在 .hide(17000) 中,然后执行了 console.log再一次 - 所有 JavaScirpt 代码 运行 始终同步。

即 - hide 执行很少的工作,运行s 执行几毫秒,然后安排更多工作稍后完成。它 not 运行 持续 17 秒。

现在 hide 的实现类似于:

function hide(element, howLong) {
    var o = 16 / howLong; // calculate how much opacity to reduce each time
    //  ask the host environment to call us every 16ms
    var t = setInterval(function
        // make the element a little more transparent
        element.style.opacity = (parseInt(element.style.opacity) || 1) - o;
        if(parseInt(element.style.opacity) < o) { // last step
           clearInterval(t); // ask the platform to stop calling us
           o.style.display = "none"; // mark the element as hidden
        }
    ,16);
}

所以基本上我们的代码是单线程的——它要求平台每秒调用它 60 次,并且每次都使元素的可见性降低一点。一切总是 运行 完成,但除了第一次代码执行之外,平台代码(主机环境)正在调用 我们的 代码,反之亦然。

因此,对您的问题的实际直接回答是计算的时间是 "taken away" 来自您的代码,就像您发出 AJAX 请求时一样。直接回答:

如果没有外部帮助,在单线程环境中是不可能的。

外部是使用线程或操作系统异步设施的封闭系统——我们的宿主环境。如果没有它,在纯标准 ECMAScript 中就无法完成。

* 随着 ES2015 包含承诺,该语言将任务委托回平台(主机环境)——但这是一个例外。

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() 内部调用 jQuery.fn.animate 调用 jQuery.Animation 哪个 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.

有关 Promise 的说明,请参阅 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);
}

调用 .hide() 时,将创建处理动画任务的 jQuery.Deferred()

这就是调用 console.log() 的原因。

如果包含 .hide()start 选项可以查看 .hide() 在下一行调用 console.log() 之前开始,但不会阻止用户界面执行异步任务。

$("#mybox").hide({
  duration:17000,
  start:function() {
    console.log("start function of .hide()");
  }
});
console.log("Previous command has not yet terminated!");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<div id="mybox">mybox</div>

本机 Promise 实现

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;
}
<div id="mybox"></div>