为什么我必须使用匿名函数而不是将附加参数传递给“setTimeout”?

Why do I have to use an anonymous function instead of passing an additional argument to `setTimeout`?

我想在 1 秒后停止 requestAnimationFrame

rAF = requestAnimationFrame(draw);

所以我用setTimeout.

当我将 cancelAnimationFrame(rAF) 包裹在箭头函数中时,它工作正常:

setTimeout(() => cancelAnimationFrame(rAF), 1000);

但是,当我使用 cancelAnimationFrame 作为函数本身并将 rAF 作为第三个参数传递给 setTimeout 时,它不起作用:

setTimeout(cancelAnimationFrame, 1000, rAF);

我以为我一开始不知道 setTimeout 的确切语法。但是,我认为语法没有错,因为这段代码工作正常:

setTimeout(alert, 1000, "Hello");

为什么不起作用?

不同之处在于计算 rAF 的时间。

说明

据推测,您的代码如下所示:

let rAF;
const draw = () => {
    // Do some work.
    
    rAF = requestAnimationFrame(draw);
  };

rAF = requestAnimationFrame(draw);

当你这样做时:

setTimeout(cancelAnimationFrame, 1000, rAF);

您将三个值传递给 setTimeout:一个函数、一个数字和另一个数字。 当执行此语句时 call is evaluated,这意味着首先,setTimeout 标识符被解析为函数引用。 其次,评估三个参数:cancelAnimationFrame 标识符被解析为函数引用,然后数字文字是一个数字基元,然后 rAF 标识符被解析为另一个数字基元。 然后,call is performed.

这就是 setTimeout 看到的全部内容。 在 JavaScript 中,您不能像在 C 中那样传递对数字的引用。

让我们假设 rAF 最初是 1。 在一秒钟的过程中,rAF 重复递增并最终达到 61 左右的值。

因为你一开始就注册了setTimeout,语句

setTimeout(cancelAnimationFrame, 1000, rAF);

等同于

setTimeout(cancelAnimationFrame, 1000, 1);

然而,声明

setTimeout(() => cancelAnimationFrame(rAF), 1000);

等同于

setTimeout(() => cancelAnimationFrame(1), 1000);

函数体只有在被调用时才会被评估。 这意味着,JS 不会“窥视函数内部”并尝试评估变量。 该语句本质上意味着 “使用其他函数和数字 1000 作为参数调用某个函数”

当一秒结束并且是时候取消动画帧时,setTimeout 执行其回调。 如果回调是() => cancelAnimationFrame(rAF),那么它会被执行,所以函数体被求值:cancelAnimationFrame(rAF)等价于cancelAnimationFrame(61).

然而,在 non-working 的情况下,cancelAnimationFrame 保持不变,参数 1(相当于 setTimeout 时的 rAF 最初是称为)保持不变。 当您已经在第 61 帧时,您无法取消第 1 帧。

当然 setTimeout(alert, 1000, "Hello"); 有效,因为 "Hello" 是静态的,只计算一次,永远不会改变。

相关

下面是可以检查此行为的更一般情况:

let greeting = "Hello";
const greet = (theGreeting) => console.log(`${theGreeting}, world!`);
const boundGreeting = greet.bind(null, greeting);

greeting = "Goodbye";

boundGreeting(); // Logs "Hello, world!".
greet(greeting); // Logs "Goodbye, world!".

bind 将第二个参数 (greeting) 作为第一个参数传递给 greet(忽略 null)。 这很像使用 setTimeout(greet, 0, greeting);,只是这与超时无关,我们自己调用(绑定)greet

可以传递类似引用的东西,即一个对象,如果你有一个也接受对象的函数:

const timing = {
    rAF: null
  },
  cancelTiming = ({ rAF }) => cancelAnimationFrame(rAF),
  draw = () => {
    // Do some work.
    
    timing.rAF = requestAnimationFrame(draw);
  };

timing.rAF = requestAnimationFrame(draw);
setTimeout(cancelTiming, 1000, timing);

这是有效的,因为在传递 timing 时,传递的值是一个引用。 改变它,例如通过更新 rAF 属性,在该引用可见的任何地方都可见。 但这使得编程相当麻烦。

这与 Is JavaScript a pass-by-reference or pass-by-value language? 切线相关。

备选

有一种方法可以替代 setTimeout。 当requestAnimationFrame calls its callback function, it passes a DOMHighResTimeStamp, similar to what performance.nowreturns。 因此,您可以在 draw 函数中进行检查:

const timeoutTimestamp = performance.now() + 1000,
  draw = (now) => {
    // Do some work.
    
    if(now < timeoutTimestamp){
      requestAnimationFrame(draw);
    }
  };

requestAnimationFrame(draw);

相关:Stop requestAnimationFrame after a couple of seconds.