Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kyle Simpson asyncify function from You Don't Know JS: Async & Performance

Tags:

javascript

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 );
            }
        };
    }
like image 328
George Smith Avatar asked May 21 '15 20:05

George Smith


2 Answers

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 completes, 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 :)

like image 77
Semicolon Avatar answered Nov 15 '22 00:11

Semicolon


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) {
      // commented 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) {
      // commented 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) {
  // commented 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.

like image 33
Виталий Сатановский Avatar answered Nov 14 '22 22:11

Виталий Сатановский