【问题标题】:Animations under single threaded JavaScript单线程 JavaScript 下的动画
【发布时间】:2016-05-23 14:15:44
【问题描述】:

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

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

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

如果您的答案是动画创建承诺问题仍然存在:JavaScript 如何同时处理多个事情(执行动画本身,在有 promise 的情况下查看动画队列并继续执行下面的代码......)。

此外,如果 jQuery 中的 Promise 必须观察其父 Deferred 对象直到它被解决或拒绝执行剩余代码的时间。在单线程方法中这怎么可能?我理解 AJAX 调用没有问题,因为我知道它们是从 JS 引擎中拿走的......

【问题讨论】:

  • 我不确定我现在要说什么,但对我来说,似乎 Js 只是注册你的隐藏动作。完成后,继续进行下一步,显示您的 console.log,女巫在注册堆栈上的隐藏之前注册。然后,当 js 在他的栈上打到你的 hide 时,就会开始启动过渡效果。不知道我是否清楚,如果没有,请告诉我,您也许可以找到一种方法来向您展示这一点
  • 许多现代 js 动画例程都使用 requestAnimationFrame() 函数。这允许更流畅的动画并且是非阻塞的。
  • @Bug jQuery 不使用 requestAnimationFrame 因为它在非活动选项卡上的行为令人惊讶。计时器也是如此。
  • @BenjaminGruenbaum jQuery 3.0 使用 requestAnimationFrame ,请参阅 blog.jquery.com/2015/07/13/…
  • @guest271314 谢谢,我已经纠正了。

标签: javascript jquery-animate single-threaded


【解决方案1】:

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


我想我理解你的问题。让我们解决一些问题:

JavaScript 始终是同步的

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

代码将始终运行到完成。代码在被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 的概念:

这样,可以说现有系统提供了对象和设施的宿主环境,从而完善了脚本语言的能力。

在浏览器中运行 JavaScript 的宿主环境称为 DOM 或文档对象模型。它指定您的浏览器窗口如何与 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 动画的类时,它与您的代码所具有的要求不同。它可以使用线程或操作系统async io

当您执行以下操作时:

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

实际发生的是:

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

您的具体情况

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

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

也就是说,hide 执行的工作非常少,运行几毫秒,然后安排更多的工作稍后完成。它确实运行了 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 次,并且每次都会使元素不那么可见。一切总是运行完成,但除了第一次代码执行之外,平台代码(主机环境)正在调用我们的代码,反之亦然。

因此,对您的问题的实际直接答案是,计算的时间已从您的代码中“删除”,就像您发出 AJAX 请求时一样。直接回答:

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

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

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

【讨论】:

  • 在哪里可以看到宿主环境提供了哪些功能/api?
  • @Imray dom.spec.whatwg.org 用于浏览器 - 例如 MutationObserver。顺便说一下 php.net/manual/en/book.dom.php 也有其他语言的 DOM 宿主环境的实现。对于 NodeJS nodejs.org/api 例如文件系统 nodejs.org/api/fs.html 。所有这些,以及使用它们构建的功能都可以与宿主环境交互。
  • @Benjamin 非常感谢您的回答。但是,如果我们用停止计时器更改第二行(console.log),例如var now=new Date().getTime(); var stop=now+17000; while (stop&gt; new Date().getTime()){},根据您的分析,我们应该期待什么? 先执行17秒的等待,然后处理事件队列和里面的动画回调。但是,我们得到的是动画与停止计时器同时完成;这意味着没有进入事件队列。所以发生了什么事?还有别的,你在哪里找到函数 hide() 的代码?
  • @ilias 我认为您对我的回答中让您感到困惑的同一件事感到困惑。 JQuery 使用插值(而不是平均划分动画),因此它给人的印象是 .hide 动画一直在后台运行,但实际上并没有。
  • @ILIAS JavaScript 代码总是 同步运行。如果我们首先执行 17 秒的忙等待 - 然后屏幕将冻结 17 秒,因为 JavaScript 总是同步运行直到完成,所以不会发生其他任何事情。
【解决方案2】:

您在 javascript 中有几种函数: 阻塞和非阻塞。

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

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

希望这会有所帮助。

您可以herehere了解更多信息

【讨论】:

    【解决方案3】:

    事件循环

    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 使用 100ms 帧。创建一个 100 毫秒的计时器,带有回调函数。计时器开始在后台运行(其实现在浏览器内部),而控制权交还给您的脚本。因此,当计时器在后台运行时,您的脚本将继续执行第二行。 100 毫秒后,计时器结束,并触发一个事件。此事件保存在上面的eventsArray 中,不会立即执行。代码执行完毕后,JavaScript 会检查 eventsArray 并查看有一个新事件,然后执行它。

    然后运行该事件,并且您的 div 或它所包含的任何元素移动几个像素,并启动一个新的 100 毫秒计时器。

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

    【讨论】:

    • 我没有要求我足够了解的事件循环解释。我敢肯定你没有仔细阅读我的问题。此外,您的答案不正确。如果动画按照您描述的方式实现,它将被冻结直到整个脚本执行!但是,很容易注意到动画在执行剩余代码的同时继续运行。这与您的答案不兼容,这正是我要解释的内容...
    • jQuery 动画并没有完全那样实现(我确信他们使用间隔而不是超时,并使用一些基于时间的插值来使动画更流畅),但我不知道你为什么这么认为答案不正确。 ...继续下面
    • 而且,不,动画不会在代码执行的同时继续运行。与您的“理解”相反,动画实际上是等待剩余代码完成后再继续。这一切都发生得太快了,你无法注意到。作为一个实验,你可以运行下面的代码来看看会发生什么:$('#mybox').hide(17000); var start = new Date(); while(true) { var now = new Date(); if (now-start &gt; 20000) { break; } }
    • 上面的代码基本上是让$('#mybox').hide(17000);之后的代码运行得更慢,从而减慢了事件循环中下一个事件的执行速度。
    • 我没有看过实现动画的jQuery代码。但是,从行为来看,我可以说我 99.9% 确定他们在做什么。我使用过许多 JS 动画库(包括 3D 库),这种模式很常见。事实是 JS 不能同时运行两个代码,不管你怎么看。
    【解决方案4】:

    有人可以向我解释一下 更多?由于 .hide() 尚未完成(动画持续 17 秒)和 JS 引擎正在处理它,它能够 一次执行一个命令,它以哪种方式转到下一个 行并继续运行剩余的代码?

    jQuery.fn.hide() 内部调用jQuery.fn.animate 调用jQuery.Animation 返回一个jQuery deferred.promise() 对象;另见jQuery.Deferred()

    deferred.promise() 方法允许异步函数 防止其他代码干扰其进度或状态 内部请求。

    Promise 的描述见 Promises/A+ , promises-unwrapping , Basic Javascript promise implementation attempt ;还有,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;
    }
    &lt;div id="mybox"&gt;&lt;/div&gt;

    【讨论】:

    • 这在技术上都是正确的,但与 OP 提出的实际问题完全无关。
    猜你喜欢
    • 2017-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多