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 );
}
};
}
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 :)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With