admin管理员组

文章数量:1421263

If someone can explain in detail what this function does? What is this part doing: fn = orig_fn.bind.apply(orig_fn,

Thanks.

function asyncify(fn) {
        var orig_fn = fn,
            intv = setTimeout( function(){
                intv = null;
                if (fn) fn();
            }, 0 )
        ;

        fn = null;

        return function() {
            // firing too quickly, before `intv` timer has fired to
            // indicate async turn has passed?
            if (intv) {
                fn = orig_fn.bind.apply(
                    orig_fn,
                    // add the wrapper's `this` to the `bind(..)`
                    // call parameters, as well as currying any
                    // passed in parameters
                    [this].concat( [].slice.call( arguments ) )
                );
            }
            // already async
            else {
                // invoke original function
                orig_fn.apply( this, arguments );
            }
        };
    }

If someone can explain in detail what this function does? What is this part doing: fn = orig_fn.bind.apply(orig_fn,

Thanks.

function asyncify(fn) {
        var orig_fn = fn,
            intv = setTimeout( function(){
                intv = null;
                if (fn) fn();
            }, 0 )
        ;

        fn = null;

        return function() {
            // firing too quickly, before `intv` timer has fired to
            // indicate async turn has passed?
            if (intv) {
                fn = orig_fn.bind.apply(
                    orig_fn,
                    // add the wrapper's `this` to the `bind(..)`
                    // call parameters, as well as currying any
                    // passed in parameters
                    [this].concat( [].slice.call( arguments ) )
                );
            }
            // already async
            else {
                // invoke original function
                orig_fn.apply( this, arguments );
            }
        };
    }
Share Improve this question asked May 21, 2015 at 20:16 George SmithGeorge Smith 1233 bronze badges 10
  • The check for intv is because if the resulting next-tick'd function was called in the same tick that asyncify() was called, intv would still be defined. But I can't say what the point of orig_fn vs fn is. when fn is bound to orig_fn, fn is never used again except in that assignment -- in other words the assignment is pointless (unless I'm just missing something...) – Semicolon Commented May 21, 2015 at 20:25
  • @Semicolon: fn is called in the setTimeout callback, which may or many not happen after the function it returns that sets fn is called. – Ry- Commented May 21, 2015 at 20:26
  • Yup, I just saw it after I typed that and was about to ment-edit :) – Semicolon Commented May 21, 2015 at 20:26
  • (That said, I am finding the point of parts of what's done here hard to grasp. That is, without seeing examples of how it's meant to be used, it's tough to work out, for example, why it both initially executes the callback but also returns a function for doing so again). – Semicolon Commented May 21, 2015 at 20:28
  • Also I should point out to the OP that orig_fn.bind.apply is pretty odd. It's actually identical to Function.prototype.bind.apply so the reference to orig_fn may be confusing there; it's just being pulled from that for convenience, and then orig_fn is supplied as the context on invocation of apply). – Semicolon Commented May 21, 2015 at 20:30
 |  Show 5 more ments

2 Answers 2

Reset to default 7

The code seems to be an very convoluted way of saying this:

function asyncify(cb) {
    return function() {
        setTimeout(function() {
            cb.apply(this, arguments);
        }, 0);
    }
}

But I should emphasize ‘seems.’ Perhaps I’m missing some important nuance in the back-and-forth above.

As for bind.apply, that’s a little trickier to explain. Both are methods on every function, which allow you to invoke it with a specified context (this) and in the case of apply, it accepts arguments as an array.

When we "apply" bind, bind itself is the function which is being applied -- not the object apply lived on, which could have been anything. Therefore, it might be easier to begin making sense of this line if we rewrite it like this:

Function.prototype.bind.apply(...)

Bind has a signature like this: .bind(context, arg1, arg2...)

The arguments are optional -- often they are used for currying, which is one of the main use cases for bind. In this case, the author wishes to bind the original function to (1) the current this context, (2) the arguments which the "asyncified" function was invoked with. Because we don’t know in advance how many arguments need to be passed along, we must use apply, where the arguments can be an array or an actual arguments object. Here's a very verbose rewrite of this section that may help illuminate what occurs:

var contextOfApply = orig_fn;
var contextWithWhichToCallOriginalFn = this;

var argumentArray = Array.prototype.slice.call(arguments);

argumentArray.unshift(contextWithWhichToCallOriginalFn);

// Now argument array looks like [ this, arg1, arg2... ]
// The 'this' is the context argument for bind, and therefore the
// context with which the function will end up being called.

fn = Function.prototype.bind.apply(contextOfApply, argumentArray);

Actually...

I can explain how the simple version I provided differs. On reviewing it again I caught what that missing nuance was that led its author to go through that weird dance at top. It is not actually a function for making another function "always async". It is a function that only ensures it is async once -- it guards against executing the callback during the same tick in which it was created but thereafter, it executes synchronously.

It’s still possible to write this in a friendlier way though, I think:

function asyncify(cb) {
    var inInitialTick = true;

    setTimeout(function() { inInitialTick = false; }, 0);

    return function() {
        var self = this;
        var args = arguments;

        if (inInitialTick)
            setTimeout(function() { cb.apply(self, args); }, 0);
        else
            cb.apply(self, args);
    }
}

Now I should note that the above does not actually do what it says. In fact the number of times that the function will execute using a timeout vs. synchronously is kind of random using either this or the original version. That's because setTimeout is a lame (but sometimes fine) substitute for setImmediate, which is clearly what this function really wants (but perhaps can't have, if it needs to run in Moz & Chrome).

This is because the millisecond value passed to setTimeout is kind of a "soft target". It won't actually be zero; in fact it will always be at least 4ms if I recall right, meaning any number of ticks may pass.

Imagining for a moment that you’re in a magical wonderland where ES6 stuff works and there’s no weird hand-wringing over whether to implement a utility as fundamental as setImmediate, it could be rewritten like this, and then it would have predictable behavior, because unlike setTimeout with 0, setImmediate really does ensure the execution occurs on the next tick and not some later one:

const asyncify = cb => {
    var inInitialTick = true;

    setImmediate(() => inInitialTick = false);

    return function() {
        if (inInitialTick)
            setImmediate(() => cb.apply(this, arguments));
        else
            cb.apply(this, arguments);
    }
};

Actually Actually...

There's one more difference. In the original, if called during the "current tick which is actually an arbitrary number of successive ticks" it will still only execute one initial time, with the final set of arguments. This actually smells a little like it might be unintended behavior, but without context I'm only guessing; this may be exactly what was intended. This is because on each call before the first timeout pletes, fn is overwritten. This behavior is sometimes called throttling but in this case, unlike "normal" throttling, it will only take place for an unknown length of time around 4ms after its creation, and thereafter will be unthrottled and synchronous. Good luck to whoever has to debug the Zalgo thus invoked :)

I want to put my 5 coins to the vision of this asyncify function example.

Here an example from You Don't Know JS: Async & Performance with my remarks and some changes:

function asyncify(fn) {
  var origin_fn = fn,
      intv = setTimeout(function () {
        console.log("2");
        intv = null;
        if (fn) fn();
      }, 0);
  fn = null;
  return function internal() {
    console.log("1");
    if (intv) {
      // mented line is presented in the book
      // fn = origin_fn.bind.apply(origin_fn, [this].concat([].slice.call(arguments)));
      console.log("1.1");
      fn = origin_fn.bind(this, [].slice.call(arguments)); // rewritten line above
    }
    else {
      console.log("1.2");
      origin_fn.apply(this, arguments);
    }
  };
}

var a = 0;
function result(data) {
  if (a === 1) {
    console.log("a", a);
  }
}
...
someCoolFunc(asyncify(result));
a++;
...

So, how does it work? Let's explore together.

I suggest to consider two scenarios - synchronous and asynchronous.

Let's suppose, that someCoolFunc is synchronous.

someCoolFunc looks like that:

function someCoolFunc(callback) {
   callback();
}

In this case console logs will fire in this order: "1" -> "1.1" -> "2" -> "a" 1.

Why so, let's dig dipper.

Firstly, is called asyncify(result) function. Inside the function we ask setTimeout to put this function

function () {
  console.log("2");
  intv = null;
  if (fn) fn();
}

at the end of tasks queue and invoke the function at the next tick of event loop (asynchronously), let's keep it in mind.

After that asyncify function returns internal function

return function internal() {
    console.log("1");
    if (intv) {
      // mented line is presented in the book
      // fn = origin_fn.bind.apply(origin_fn, [this].concat([].slice.call(arguments)));
      console.log("1.1");
      fn = origin_fn.bind(this, [].slice.call(arguments)); // rewritten line above
    }
    else {
      console.log("1.2");
      origin_fn.apply(this, arguments);
    }
  };

This result will be handled by someCoolFunc. We decided to assume, that someCoolFunc is synchronous. It leads to calling internal immediately.

function someCoolFunc(callback) {
   callback();
}

In this case this branch of if statement will be fired:

if (intv) {
  // mented line is presented in the book
  // fn = origin_fn.bind.apply(origin_fn, [this].concat([].slice.call(arguments)));
  console.log("1.1");
  fn = origin_fn.bind(this, [].slice.call(arguments)); // rewritten line above
}

In this branch we reassign fn value to origin_fn.bind(this, [].slice.call(arguments));. It guarantees, that fn function will have the same context as origin_fn function and the same arguments.

After that we get back from someCoolFunc to the line, were we increment a++.

All synchronous code was done. We agreed to keep in mind the snippet, which was postponed by setTimeout. It's time for it.

function () {
  console.log("2");
  intv = null;
  if (fn) fn();
}

The snippet above pooped up from tasks queue of event loop and invoked (we see in the console "2"). fn exists, we defined it in internal function, so if statement is passed and fn function is invoked.

Yey, that's it :)

But... something was left. Oh, yeah, we considered only synchronous scenario of someCoolFunc.

Let's fill the gaps and assume, that someCoolFunc is asynchronous and looks like taht:

function someCoolFunc(callback) {
  setTimeout(callback, 0);
}

In this case console logs will fire in this order: "2" -> "1.2" -> "1" -> "a" 1.

As in the first case, at first is called asyncify function. It makes the same things - schedules

function () {
  console.log("2");
  intv = null;
  if (fn) fn();
}

snippet to be invoked at the next tick of event loop (firstly synchronous stuff).

From this step things go differ. Now internal isn't invoked immediately. Now it's put to the tasks queue of event loop. Tasks queue already has two postponed tasks - callback of setTimeout and internal function.

After that we get back from someCoolFunc to the line, were we increment a++.

Synchronous stuff is done. It's time for postponed tasks, in order as they were put there. First is invoked callback of setTimeout (we see "2" in the console):

function () {
  console.log("2");
  intv = null;
  if (fn) fn();
}

intv is set to null, fn equals to null, so, if statement is skipped. Task is poped up from tasks queue.

Left last step. As we remember in the tasks queue left internal function, it's invoked now.

In this case is fired this branch of if statement, due to intv was set to null:

else {
  console.log("1.2");
  origin_fn.apply(this, arguments);
}

In the console we see "1.2", then origin_fn is called using apply. origin_fn equals to result function in our case. Console shows us "a" 1.

That's it.

As we can see, it doesn’t matter, how someCoolFunc behaves - synchronous or asynchronous. In both cases result function will be invoked at the time, when a equals to 1.

本文标签: javascriptKyle Simpson asyncify function from You Don39t Know JS Async amp PerformanceStack Overflow