当前位置: 首页 > Web前端 > JavaScript

RequestAnimationFrame执行机制探索

时间:2023-03-26 23:42:52 JavaScript

1.什么是requestAnimationFramewindow.requestAnimationFrame()告诉浏览器你要执行一个动画,要求浏览器在下次重绘前调用指定的回调函数来更新动画。该方法需要传入一个回调函数作为参数,回调函数会在下次浏览器重绘前执行。根据上述MDN定义,requestAnimationFrame是浏览器提供的逐帧重绘网页的API。让我们看看下面的例子,看看它是如何工作的:consttest=document.querySelector("#test")!;leti=0;functionanimation(){if(i>200)return;test.style.marginLeft=`${i}px`;window.requestAnimationFrame(动画);i++;}window.requestAnimationFrame(动画);上面的代码1s大概执行了60次,因为一般屏幕硬件设备的刷新率都是60Hz,然后每次执行大概16.6ms。使用requestAnimationFrame时,只需反复调用即可实现动画效果。同时requestAnimationFrame会返回一个请求ID,该ID在回调函数列表中是唯一的值。您可以使用cancelAnimationFrame通过传入请求ID来取消回调函数。consttest=document.querySelector("#test")!;leti=0;letrequestId:number;functionanimation(){test.style.marginLeft=`${i}px`;requestId=requestAnimationFrame(动画);我++;如果(i>200){cancelAnimationFrame(requestId);}}动画片();下图1是上面例子的执行结果:2.对requestAnimationFrame执行的困惑使用JavaScript实现动画也可以使用setTimeout,下面是实现代码:consttest=document.querySelector("#test")!;leti=0;lettimerId:number;functionanimation(){test.style.marginLeft=`${i}px`;//设置执行间隔为0模仿requestAnimationFrametimerId=setTimeout(animation,0);我++;如果(i>200){clearTimeout(timerId);}}动画片();这里设置setTimeout的执行间隔为0,模仿requestAnimationFrame。单从代码实现的方式来看,并没有什么区别,但是从下面的具体实现结果可以看出明显的差距。下图2是setTimeout的执行结果:完整示例点击codesandbox。很明显setTimeout比requestAnimationFrame实现的动画要“快”很多。是什么原因?你可能已经猜到了,EventLoop和requestAnimationFrame在执行时有一些特殊的机制。下面我们来探讨一下EventLoop和requestAnimationFrame之间的关系。3.EventLoop和requestAnimationFrameEventLoop(事件循环)是一种浏览器内部机制,用于协调事件、用户交互、脚本、渲染和网络。浏览器中的EventLoop有几种类型:windoweventloop、workereventloop、workleteventloop,这里主要讨论windoweventloop。即浏览器渲染过程中由主线程控制的EventLoop。3.1任务队列一个EventLoop有一个或多个任务队列。任务队列是任务的集合。注意:任务队列是数据结构上的集合,而不是队列,因为事件循环处理模型会从选中的任务队列中获取第一个可运行任务(runnabletask),而不是让第一个任务出Team。以上内容来自HTML规范。这里让人疑惑的是,明明是集合,为什么还叫“队列”呢?T.T3.2一个任务可以有多个任务源(tasksources)。有哪些任务来源?我们来看看规范中的Generic任务源:DOM操作任务源,比如一个元素以非阻塞的方式插入到文档中用户交互任务源,用户操作(比如点击)事件网络任务源,网络I/O响应回调历史遍历任务源,如history.back(),除了Timers(setTimeout、setInterval等)外,IndexDB操作也是任务源。3.3微任务一个事件循环有一个微任务队列,但这个“队列”确实是“FIFO”队列。规范没有指定哪些是微任务的任务源。一般认为以下是微任务:promisesMutationObserverObject.observeprocess.nextTick(这个东西是Node.js的API,暂不讨论))必须包含一个可运行的任务。如果没有这样的任务队列,请跳到下面的微任务步骤。使taskQueue中最旧的任务(oldestTask)成为第一个可执行任务,然后将其从taskQueue中删除。将上面的oldestTask设置为事件循环中的运行任务。执行最旧的任务。将事件循环中的运行任务设置为空。执行微任务检查点(即执行微任务队列中的任务)。将hasARenderingOpportunity设置为false。更新渲染。如果当前窗口事件循环在任务队列中没有任务且微任务队列为空,且渲染机会变量hasARenderingOpportunity为false,则执行空闲期(requestIdleCallback)。返回第一步。以上是规范中事件循环处理流程的精简版,省略了部分内容,完整版在这里。一般来说,事件循环就是不断的去寻找任务队列中是否有可执行的任务。如果有,就会被压入调用栈(执行栈)执行,适时更新渲染。下图3(来源)是运行在浏览器主线程上的事件循环的清晰流程:主线程干什么又是一个大话题,有兴趣的同学可以看看浏览器内部秘密系列文章。在上面规范的描述中,渲染过程是在执行microtasksqueue之后,再来看看渲染过程。3.5更新渲染迭代当前浏览上下文中的所有文档,并且必须按照在列表中找到的顺序处理每个文档。渲染机会:如果当前浏览上下文没有渲染机会,则删除所有docs,取消渲染(这里是否有渲染机会由浏览器自己决定,基于硬件刷新率限制,页面性能或者页面是否在后台等因素)。如果当前文档不为空,则将hasARenderingOpportunity设置为true。不必要的渲染:如果浏览器认为更新文档浏览上下文的渲染不会有可见效果,并且文档的动画帧回调为空,则取消渲染。(最后看到requestAnimationFrame从docs中删除浏览器认为出于其他原因最好跳过更新呈现的文档。如果浏览上下文是顶级浏览上下文,则刷新文档的自动对焦候选者。处理resize事件,传入performance.now()时间戳。处理滚动事件,传入performance.now()时间戳。处理媒体查询,传入performance.now()时间戳。运行CSS动画,传入performance.now()时间戳。处理全屏事件并传入performance.now()时间戳。执行requestAnimationFrame回调并传入performance.now()时间戳。执行intersectionObserver回调并传入performance.now()时间戳。绘制每个文档。更新ui并渲染。图下图4(source)是一个清晰的流程:至此,requestAnimationFrame的回调时机已经很清楚了,会在style/layout/paint之前调用。回到setTimeout动画快于tha的问题n文章开头提到的requestAnimationFrame动画,这个解释的很好。首先,浏览器渲染存在一个渲染机会(Renderingopportunity)问题,即浏览器会根据上下文判断是否渲染,它会尽量高效,只在需要的时候渲染。如果没有界面变化,则不会渲染。根据规范,由于硬件刷新率限制、页面性能、页面是否有背景等因素,有可能执行setTimeout任务发现还没到渲染时间,回调setTimeout几次。进行渲染,此时设置的marginLeft与上次渲染前的marginLeft相差必须大于1px。下面的图5显示了setTimeout的执行。红圈是两个渲染,中间四个是处理setTimout任务。因为屏幕的刷新率为60Hz,所以在渲染之前执行多个setTimeout任务大约需要16.6ms。计时和执行渲染。requestAnimationFrame帧动画的区别在于每次渲染前都会调用,此时设置的marginLeft与上次渲染前的marginLeft相差1px。下图6是requestAnimationFrame的执行过程,每次调用后都会进行渲染:看来setTimeout“快”多了。4、不同浏览器的实现以上例子都是在Chrome下测试的,本例呈现的结果在所有浏览器下基本一致。看看下面这个例子,它是jakearchilbald早在2017年就提出的问题:test.style.transform='translate(0,0)';document.querySelector('button').addEventListener('click',()=>{consttest=document.querySelector('.test');test.style.transform='translate(400px,0)';requestAnimationFrame(()=>{test.style.transition='transform3slinear';test.style.transform='translate(200px,0)';});});这段代码在Chrome和Firefox中执行如图7所示:简单说明一下,本例中requestAnimationFrame回调中设置的transform覆盖了点击监听器中设置的transform,因为requestAnimationFrame是在计算css(style)之前调用的,所以动画向右移动200像素。注意:以上代码是在Chrome隐藏模式下执行的。当你的Chrome浏览器有很多插件或者打开很多标签时,也可能会出现从右向左滑动的现象。safari的执行如图8所示:edge的执行结果和之前safari的执行结果是一样的,现在已经修复了。这样做的原因是safari在渲染完1帧后才执行requestAnimationFrame回调,所以当前帧调用的requestAnimationFrame会在下一帧渲染。所以safari开始向右渲染400px,然后向左移动200px。关于eventloop和requestAnimationFrame的执行机制更详细的解释,jake在jsconf中有专门的演讲,推荐大家看看。5.其他执行规则继续看前面jake提出的例子。如果想在标准规范下实现safari呈现的效果(即从右向左移动),需要怎么做呢?答案是再添加一层requestAnimationFrame调用:test.style.transform='translate(0,0)';document.querySelector('button').addEventListener('click',()=>{consttest=document.querySelector('.test');test.style.transform='translate(400px,0)';requestAnimationFrame(()=>{requestAnimationFrame(()=>{test.style.transition='transform3slinear';test.style.transform='translate(200px,0)';});});});上面代码的执行结果和Safari是一致的,因为requestAnimationFrame每帧只执行一次,新定义的requestAnimationFrame会在下一帧渲染前执行。6.其他应用从上面的例子我们知道,使用setTimeout来执行动画等视觉变化可能会导致丢帧和卡顿,所以应该尽量避免使用setTimeout来执行动画。推荐使用requestAnimationFrame来代替。requestAnimationFrame不仅用来实现动画效果,还可以用来实现大任务的拆分执行。从图4的渲染流程图可以知道,JavaScript任务是在渲染之前执行的。如果JavaScript在一帧内执行时间过长,渲染就会被阻塞,同样会造成丢帧卡顿。在这种情况下,可以将JavaScript任务分成小块,并使用requestAnimationFrame()在每一帧上运行。如以下示例(来源)所示:vartaskList=breakBigTaskIntoMicroTasks(monsterTaskList);requestAnimationFrame(processTaskList);functionprocessTaskList(taskStartTime){do{//假设下一个任务被压入调用栈varnextTask=taskList.pop();//执行下一个任务processTask(nextTask);//多长时间足够继续执行下一个任务FinishTime=window.performance.now();}while(taskFinishTime-taskStartTime<3);如果(taskList.length>0){requestAnimationFrame(processTaskList);}}7.参考WHATWGHTML标准现代浏览器内部揭示了JavaScript主线程。Dissected.requestAnimationFrameSchedulingForNerdsjakejsconfspeechoptimizejavascriptexecutionexplorejavaScriptasynchronousandbrowserupdaterenderingtimingfromeventloopspecification欢迎关注傲兔实验室博客:aotu.io或关注傲兔实验室公众号(傲兔实验室),不定期发文时间。