質問

Here's the jsperf: http://jsperf.com/promise-vs-callback

callback case (211 Ops/s):

// async test
var d = deferred;

function getData(callback) {
  setTimeout(function() {
    callback('data')
  }, 0)
}

getData(function(data) {
  d.resolve()
})

Promise case(614 ops/s):

// async test
var d = deferred;

function getData() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve('data')
    }, 0);
  })
}

getData().then(function(data) {
  d.resolve()
})

As you see promise are way faster, but they have more code. The question is why this happens.

Here deferred is to defined by jsperf to show it as the completion of the async test.

役に立ちましたか?

解決

As it seems the magic trick lies with in how chrome sets the minimum delay for setTimeout(fn, 0).

I searched for it and I found this: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/Hn3GxRLXmR0/XP9xcY_gBPQJ

I quote the important part:

The way timer clamping works is every task has an associated timer nesting level. If the task originates from a setTimeout() or setInterval() call, the nesting level is one greater than the nesting level of the task that invoked setTimeout() or the task of the most recent iteration of that setInterval(), otherwise it's zero. The 4ms clamp only applies once the nesting level is 4 or higher. Timers set within the context of an event handler, animation callback, or a timer that isn't deeply nested are not subject to the clamping.

In the callback case, setTimeout is called recursively, in a context of another setTimeout , so the minimum timeout is 4ms. In the promise case, setTimeout is actually not called recursively, so the minimum timeout is 0(It wouldn't be actually 0, because other stuff has to run too).

So how do we know setTimeout is called recursively? well we can just conduct an experiment in jsperf or just using benchmark.js:

// async test
deferred.resolve()

Which will result in Uncaught RangeError: Maximum call stack size exceeded. Which means, once deferred.resolve is called, the test is run again on the same tick/stack. So in the callback case setTimeout is called in it's own calling context and nested in another setTimeout, which will set the minimum timeout to 4ms.

But in the promise case, .then callback is called after the next tick according to promise spec, and v8 doesn't use setTimeout calling the callback after the next tick. It uses something that must be similar to process.nextTick in nodejs or setImmediate, and not setTimeout. Which resets the setTimeout nesting level to 0 again and makes the setTimeout delay 0ms.

他のヒント

First of all, your benchmark is designed wrong. It is only going to measure the minimal setTimeout value, not the perf difference between callbacks and promises.

The minimal delay is 4ms so the result cannot be more than 250 operations per second. Somehow calling new Promise is removing the minimal 4ms delay.

If you wanted to measure promise and callback difference, you need to remove such unnatural bottlenecks. So not only are you measuring at concurrency level of 1, you are waiting 4ms between each call.

JSPErf doesn't make it easy to set concurrency, but here is with concurrency = 1000:

http://jsperf.com/promise-vs-callback/7

As Esailija pointed it's related to weird setTimeout optimizaition in Promise. See also same benchmark made with faster setTimeout alternative: http://jsperf.com/promise-vs-callback/8 it gives more expected results

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top