来自You Don't Know JS的Kyle Simpson asyncify函数:Async &表演
Kyle Simpson asyncify function from You Don't Know JS: Async & Performance
如果有人能详细解释一下这个函数是做什么的?这部分在做什么:fn = orig_fn.bind。应用(orig_fn
谢谢。
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 );
}
};
}
代码似乎是一种非常复杂的表达方式:
function asyncify(cb) {
return function() {
setTimeout(function() {
cb.apply(this, arguments);
}, 0);
}
}
但是我应该强调"似乎"。也许我在上面的来回对话中遗漏了一些重要的细微差别。
对于bind.apply
,这有点难以解释。两者都是每个函数的方法,允许您使用指定的上下文(this
)调用它,并且在apply
的情况下,它以数组的形式接受参数。
当我们"应用"bind
时,绑定本身是正在应用的函数—而不是应用所依赖的对象,它可以是任何东西。因此,如果我们像这样重写这一行,可能会更容易理解它:
Function.prototype.bind.apply(...)
Bind有一个这样的签名:.bind(context, arg1, arg2...)
参数是可选的——通常用于柯里化,这是bind
的主要用例之一。在这种情况下,作者希望将原始函数绑定到(1)当前this
上下文,(2)调用"asyncified"函数的参数。因为我们事先不知道需要传递多少参数,所以必须使用apply,其中的参数可以是数组或实际的arguments
对象。下面是对这一节非常详细的重写,可能有助于说明发生了什么:
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);
实际上…
我可以解释我提供的简单版本有何不同。在再次阅读时,我发现了导致作者在顶部进行奇怪舞蹈的缺失的细微差别。它实际上并不是一个使另一个函数"始终异步"的函数。这个函数只保证在一次是异步的——它防止在创建回调的同一时间内执行回调,但在此之后,它将同步执行。
我认为仍然可以用更友好的方式来写:
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);
}
}
现在我应该注意到上面的代码实际上并没有按照它所说的去做。实际上,函数执行超时和同步的次数是随机的,不管是用这个还是用原始版本。这是因为setTimeout是一个蹩脚的(但有时很好)setimate的替代品,这显然是这个函数真正想要的(但也许不能有,如果它需要在Moz中运行;Chrome)。
这是因为传递给setTimeout的毫秒值是一种"软目标"。它实际上不会是零;事实上,如果我没记错的话,它总是至少4ms,这意味着任何数量的滴答声都可能通过。
想象一下,你在一个神奇的仙境中,在那里ES6的东西工作,没有奇怪的焦虑是否实现一个像setimate一样基本的实用程序,它可以被重写成这样,然后它会有可预测的行为,因为不像setTimeout与0,setimate确实确保执行发生在下一个tick,而不是一些以后:
const asyncify = cb => {
var inInitialTick = true;
setImmediate(() => inInitialTick = false);
return function() {
if (inInitialTick)
setImmediate(() => cb.apply(this, arguments));
else
cb.apply(this, arguments);
}
};
实际上…
还有一个区别。在原始代码中,如果在"当前滴答声实际上是任意数量的连续滴答声"期间调用,它仍然只执行一次初始时间,并带有最后一组参数。这实际上闻起来有点像无意的行为,但没有上下文我只是猜测;这也许正是他想要的。这是因为在第一个超时完成之前的每次调用中,fn都会被覆盖。这种行为有时被称为节流,但在这种情况下,与"正常"节流不同,它只会在创建后大约4ms的未知时间内发生,此后将不进行节流和同步。祝那些调试Zalgo的人好运!)
我想把我的5个硬币放在这个asyncify
函数示例的愿景上。
以下是You Don't Know JS: Async & Performance
的一个示例,其中包含我的注释和一些更改:
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++;
...
它是如何工作的呢?让我们一起探索。
我建议考虑两种情况——synchronous
和asynchronous
。
让我们假设,someCoolFunc
是synchronous
。
someCoolFunc
看起来像这样:
function someCoolFunc(callback) {
callback();
}
在这种情况下,控制台日志将按以下顺序触发:"1"→"1.1";→"2";→"a"1 .
为什么呢,我们来挖吧。
首先称为asyncify(result)
函数。在函数内部,我们要求setTimeout
将这个函数
function () {
console.log("2");
intv = null;
if (fn) fn();
}
在任务队列的末尾,并在事件循环的下一个滴答点调用该函数(异步),让我们记住它。
之后asyncify
函数返回internal
函数
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);
}
};
此结果将由someCoolFunc
处理。我们决定假设someCoolFunc
是synchronous
。这会导致立即调用internal
。
function someCoolFunc(callback) {
callback();
}
在这种情况下,if
语句的分支将被触发:
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
}
在这个分支中,我们将fn
的值重新赋给origin_fn.bind(this, [].slice.call(arguments));
。它保证fn
函数与origin_fn
函数具有相同的上下文和相同的参数。
之后,我们从someCoolFunc
返回到a++
加1的行。
所有同步代码都已完成。我们同意记住这个片段,它被setTimeout延迟了。是时候了。
function () {
console.log("2");
intv = null;
if (fn) fn();
}
上面的代码片段从事件循环的任务队列中弹出并被调用(我们在控制台中看到)。fn
存在,我们在internal
函数中定义了它,所以传递if
语句,调用fn
函数。
对,就是这样:)
但是…有些东西被留下了。哦,是的,我们只考虑了someCoolFunc
的synchronous
方案。
让我们填补空白,假设someCoolFunc
是asynchronous
,看起来像这样:
function someCoolFunc(callback) {
setTimeout(callback, 0);
}
在这种情况下,控制台日志将按以下顺序触发:"2"→"1.2";→"1";→"a"1 .
与第一种情况一样,首先称为asyncify
函数。它做同样的事情——时间表
function () {
console.log("2");
intv = null;
if (fn) fn();
}
在事件循环的下一个滴答点被调用的代码片段(首先是同步的东西)。
从这一步开始,事情就不同了。现在internal
不会立即被调用。现在它被放到事件循环的任务队列中。任务队列已经有两个延迟的任务——setTimeout和internal
函数的回调。
之后,我们从someCoolFunc
返回到行,我们增加a++
。
setTimeout
的回调调用(我们看到"2"在控制台中):
function () {
console.log("2");
intv = null;
if (fn) fn();
}
intv
设置为null
, fn
等于null
,因此,跳过if
语句。任务从任务队列中弹出。
左最后一步。我们记得在任务队列左internal
函数中,它现在被调用了。
在这种情况下,触发if
语句的分支,因为intv
被设置为null
:
else {
console.log("1.2");
origin_fn.apply(this, arguments);
}
在控制台中,我们看到"1.2",然后使用apply调用origin_fn
。在本例中,origin_fn
等于result
函数。控制台显示我们";a"1 .
。
正如我们所看到的,someCoolFunc
- synchronous
或asynchronous
的行为无关紧要。在这两种情况下,当a
等于1时,result
函数将被调用。