来自You Don't Know JS的Kyle Simpson asyncify函数:Async &表演

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

本文关键字:asyncify Simpson Kyle 函数 表演 Async JS Don You Know 来自      更新时间:2023-09-26

如果有人能详细解释一下这个函数是做什么的?这部分在做什么: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++;
...

它是如何工作的呢?让我们一起探索。

我建议考虑两种情况——synchronousasynchronous

让我们假设,someCoolFuncsynchronous

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处理。我们决定假设someCoolFuncsynchronous。这会导致立即调用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函数。

对,就是这样:)

但是…有些东西被留下了。哦,是的,我们只考虑了someCoolFuncsynchronous方案。

让我们填补空白,假设someCoolFuncasynchronous,看起来像这样:

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 - synchronousasynchronous的行为无关紧要。在这两种情况下,当a等于1时,result函数将被调用。