为什么在循环开始时调用requestAnimationFrame不会导致无限递归?

12

发生了什么使得循环的其余部分执行,并且让requestAnimationFrame在下一帧执行?

我误解了这个方法的工作方式,无法在任何地方找到清晰的解释。我尝试阅读此处的时间规范http://www.w3.org/TR/animation-timing/,但无法理解它的工作方式。

例如,这段代码来自于threejs文档。

var render = function () { 
  requestAnimationFrame(render); 
  cube.rotation.x += 0.1; 
  cube.rotation.y += 0.1;
  renderer.render(scene, camera); 
};

2
你能贴一些代码吗? - Vivin Paliath
2
requestAnimationFrame不是递归的,它是异步的(就像一个短的setTimeout但更有效),并且在调用它的函数体之后执行。然而,在for循环中放置它没有太多意义。顺便说一下,递归发生在render函数中,而不是requestAnimationFrame中。你不应该在render中调用render。 - mpm
4个回答

18

请告诉我是否完全错误;我以前没有使用过动画效果。我看到使用requestAnimationFrame的示例是:

(function animloop(){
  requestAnimFrame(animloop);
  render();
})();

您是否想知道为什么将 animloop 传递给 requestAnimFrame 并在随后调用它时不会导致无限循环呢?

这是因为此函数并非真正的递归。您可能认为当您调用 requestAnimFrame 时,animloop 会立即被调用。但实际上不是这样的!requestAnimFrame 是异步的。因此,语句按您看到的顺序执行。这意味着主线程不会等待调用 requestAnimFrame 返回,就在调用 render()之前。因此,render() 几乎是立即调用的。但是回调函数(在本例中为 animloop)并不会立即被调用。它可能会在某个时间点调用,而此时您已经从第一个 animloop 调用中退出了。这个新的对 animloop 的调用有它自己的上下文和堆栈,因为它实际上没有从第一个 animloop 调用的执行上下文中被调用。这就是为什么您不会出现无限递归和堆栈溢出的原因。


1
好的,现在我更明白了,我的回调函数使用理解还不够透彻,所以我需要进行一些研究。谢谢 :) - dnv
1
这是一个非常简洁的答案,解决了我对同样问题的困惑。我认为它应该被接受作为答案 :) - bideowego

6
这是发生的事情:
你声明一个函数定义,调用requestAnimationFrame函数。
它会安排你的函数在适当的时间再次被调用和执行,通常是下一帧,大约16毫秒后。此外,这个调度是异步的。它不会停止下面代码的执行。所以不像这行代码下面的代码要等到16毫秒过去才能工作。
然而,在大多数情况下,函数在3-4毫秒内执行。
但是如果函数需要更长时间来完成,下一帧就会延迟,从而不执行预定任务,即调用相同的函数。
从某种意义上说,动画是无限循环。这也是requestAnimationFrame的目的。然而,这个非阻塞的无限循环受到帧率/fps的限制。

根据我的测试和有限的理解,raf在第一次调用时不一定会等待下一帧。它通常几乎立即运行(我在Chrome中测试时只需0.4毫秒),这是因为它在下一帧之前尽可能快地运行。但是当它在递归式循环中被调用时,然后循环内的第二个及所有后续调用将被防抖到帧速率。我可能完全错了,只是我对正在发生的事情的最佳猜测。我很难相信浏览器会在0.4毫秒内呈现一帧,特别是当它没有必要时。 - Owen Masback
raf 尝试通过不运行不会被显示的代码来节省资源。假设帧以 10 毫秒更新一次,而您的代码循环时间为 5 毫秒。那么您的代码会告诉浏览器绘制两次,但它只会执行最后一次,这就是 raf 防止的情况。如果浏览器可以更快地更新帧,则代码将运行得更快。反之,如果您的代码需要 20 毫秒,而浏览器可以在 10 毫秒内更新,则如果没有任何变化,更新没有意义,浏览器将延迟更新。从而节省计算资源。 - Muhammad Umer

4
由于与使用setTimeout在循环中调度回调不会导致无限递归的原因相同,它会在JS事件循环调度下一个调用而不是立即执行它。
该调用不是在当前上下文中进行的,因此严格意义上来说它不是递归,并且不会导致堆栈限制超出错误。 Event loop diagram (来源: dartlang.org) 这个图表是为Dart设计的,但在JS中概念是相同的。如果您有兴趣阅读更多关于事件循环、计时器调度以及微任务和宏任务之间的区别,请查看this question

2
我自己也遇到了类似的问题,但是没有找到令人满意的答案。这个答案是针对那个问题的,但它也与这里有关。
  1. 最初,window.rAF被传递到调用栈中。

  2. 根据定义,rAF等待下一次窗口重绘,对于60fps屏幕而言,这是16ms。它就像是setTimeOut,但更好。

img1

  1. 执行window.rAF(),因此它从调用堆栈中移除。调用堆栈为空。同时,在经过16毫秒后,main()被放入回调队列中。

img2

现在让我们来看看事件循环。事件循环是一个持续运行的进程,不断检查调用栈是否为空。如果调用栈为空,它会将回调队列中的函数移动到调用栈中并执行它。因此,main()函数被执行。

img3

再次调用window.rAF(main)。

img4

window.rAF()现在已经执行。

  1. 现在,在window.rAF()下的代码运行(if条件等等)。之后,16毫秒后,main()被传递到回调队列中。

img5

主函数被执行。

img6

图片来源:https://nainacodes.com/blog/understand-the-event-loop-in-javascript

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接