9

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

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

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

如果您的答案是动画创建了 Promise那么问题仍然存在:JavaScript 如何同时处理不止一件事(执行动画本身,在出现 Promise 的情况下观察动画队列并继续执行以下代码。 ..)。

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

4

4 回答 4

21

tl;博士; 在没有外部帮助的情况下,在严格的单线程环境中是不可能的。


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

JavaScript 总是同步的

语言规范中没有定义异步 API。所有功能都喜欢Array.prototype.mapString.fromCharCode始终同步运行*。

代码将始终运行完成。return代码在被 a 、隐式return(到达代码末尾)或 a 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 语言定义了一个叫做宿主环境的概念:

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

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

您的具体情况

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

这里代码是同步运行的。前面的命令已经终止,但它实际上并没有做太多 - 相反它在平台 a 上安排了一个回调(.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 包含 Promise,该语言将任务委托回平台(宿主环境)——但这是一个例外。

于 2016-02-18T23:14:15.737 回答
2

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

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

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

希望这会有所帮助。

您可以 在此处此处获得更多信息

于 2016-02-15T13:06:56.493 回答
0

事件循环

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

为了简化它,假设 JavaScript 有一个巨大的数组来存储所有事件。事件循环循环通过这个事件循环,从最旧的事件开始到最新的事件。也就是说,JavaScript 做了这样的事情:

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

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

如果在处理event.process事件(当前事件处理完毕后,再处理下一个事件,以此类推,直到到达.eventAeventsArrayeventA

来到你的示例代码,

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

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

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

请注意,这是一个简化,而不是整个事情的实际工作。整个事情有一些复杂性,比如堆栈等等。请在此处查看 MDN 文章以获取更多信息。

于 2016-02-18T13:26:05.560 回答
0

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

jQuery.fn.hide()内部调用jQuery.fn.animatewhich 调用jQuery.Animation返回 jQuerydeferred.promise()对象;也可以看看jQuery.Deferred()

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

Promise参见Promises/A+ , promises-unwrapping , Basic Javascript promise implementation attempt的描述;还有,什么是 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()会创建处理动画任务的 a。

这就是console.log()所谓的原因。

如果在下一行调用之前开始的can review的includestart选项,但不会阻止用户界面执行异步任务。.hide().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>

于 2016-02-19T00:29:22.467 回答