<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【JS】685- ReqeustIdleCallback解析

          共 7044字,需瀏覽 15分鐘

           ·

          2020-08-15 21:12

          它和 requestAnimationFrame 一樣嗎?

          最初我以為這個(gè)函數(shù)就是和實(shí)現(xiàn)動(dòng)畫(huà)的 requestAnimationFrame 擁有相同的行為,因?yàn)樗鼈兊氖褂梅椒ǚ浅n愃?,但?shí)際使用后發(fā)現(xiàn)它們的差別還是蠻大的。本文主要對(duì)這個(gè)神秘的函數(shù)進(jìn)行一些說(shuō)明和分析。

          定義和用法

          首先來(lái)看一下它的定義和用法,MDN是這樣定義它的:

          這是一個(gè)實(shí)驗(yàn)中的功能,window.requestIdleCallback() 將一個(gè)(即將)在瀏覽器空閑時(shí)間執(zhí)行的函數(shù)加入隊(duì)列,這使得開(kāi)發(fā)者在主事件循環(huán)中可以執(zhí)行低優(yōu)先級(jí)工作,而不影響對(duì)延遲敏感的事件,如動(dòng)畫(huà)和輸入響應(yīng)。

          通過(guò)這個(gè)定義,我們發(fā)現(xiàn)它的執(zhí)行時(shí)機(jī)在瀏覽器的“空閑”狀態(tài),那么怎樣定義這個(gè)狀態(tài)呢?

          瀏覽器每一幀都需要完成這些任務(wù):

          1. 處理用戶交互
          2. JS執(zhí)行
          3. 一幀的開(kāi)始,處理視窗變化、頁(yè)面滾動(dòng)等
          4. requestAnimationFrame(rAF)
          5. 重排(layout)
          6. 繪制(draw)

          在這些步驟完成后,如果時(shí)間消耗還沒(méi)超過(guò)16ms,則瀏覽器還有余力去處理其他的任務(wù),我們?cè)?reqeustIdleCallback 中傳入的回調(diào)將在此時(shí)執(zhí)行;相反,如果時(shí)間消耗太大,則回調(diào)不執(zhí)行,任務(wù)會(huì)順延到下個(gè)幀瀏覽器空閑的時(shí)候再執(zhí)行。而如果瀏覽器一直都很忙,那任務(wù)就會(huì)一再被推遲,很可能需要消耗大量時(shí)間后才得到執(zhí)行。為了解決這個(gè)問(wèn)題,可以在注冊(cè)任務(wù)的時(shí)候提供一個(gè) timeout 參數(shù)指定超時(shí)時(shí)間,在超時(shí)時(shí)間之內(nèi),該任務(wù)會(huì)被優(yōu)先放在瀏覽器的執(zhí)行隊(duì)列中。

          下面來(lái)看下它的用法:

          // 這些只為了表明一些參數(shù)定義
          type Dealine = {
          timeRemaining: () => number // 當(dāng)前剩余的可用時(shí)間
          didTimeout: boolean // 是否超時(shí)
          }
          type Tick = (deadline: Dealine) => void;
          type Options = { timeout: number }; // 可以提供一個(gè)超時(shí)時(shí)間,配合上面的 didTimeout 一起用
          type RequestIdleCallback = (tick: Tick, options?: Options) => number // 類似于rAF返回一個(gè)句柄,可以把它傳入cancelIdleCallback取消掉任務(wù)

          一個(gè)常見(jiàn)的用法是,當(dāng)有剩余時(shí)間或者timeout發(fā)生時(shí)執(zhí)行一些任務(wù),通常將任務(wù)保存在一個(gè)隊(duì)列中便于進(jìn)行調(diào)度。

          // 待執(zhí)行的任務(wù)隊(duì)列
          const taskQueue = [];

          const tick = function(deadline) {
          const remaining = deadline.timeRemaining();
          while (remaining > 0 || didTimeout) {
          // 如果超時(shí),或者還有剩余執(zhí)行時(shí)間,則執(zhí)行這里的任務(wù)
          // 執(zhí)行任務(wù)隊(duì)列中的任務(wù)
          const currentTask = taskQueue.shift();
          exec(currentTask);
          }
          // 再次注冊(cè),在下一個(gè)間隙繼續(xù)執(zhí)行taskQueue中的任務(wù)
          requestIdleCallback(tick, { timeout: 500 });
          };

          requestIdleCallback(tick, { timeout: 500 });

          reqeustIdleCallback 的執(zhí)行行為

          requestAnimationFrame 大家經(jīng)常拿來(lái)實(shí)現(xiàn)動(dòng)畫(huà),因?yàn)樗且粋€(gè)“靠譜的”函數(shù),如果頁(yè)面沒(méi)有阻塞,那么這個(gè)函數(shù)每16ms左右調(diào)用一次;requestIdleCallback 則不同,它的執(zhí)行間隔是不固定的,取決于瀏覽器此時(shí)正在執(zhí)行的任務(wù),下面舉幾個(gè)例子來(lái)看下。

          我們簡(jiǎn)單地在頁(yè)面中注冊(cè) requestIdleCallback,先不提供timeout參數(shù)

          const tick = function(deadline) {
          const remaining = deadline.timeRemaining();
          if (remaining > 0) {
          // do some stuff
          }
          requestIdleCallback(tick);
          };

          requestIdleCallback(tick);

          場(chǎng)景一,頁(yè)面中同時(shí)使用 requestAnimationFrame 函數(shù)循環(huán)注冊(cè)一個(gè)事件,使頁(yè)面發(fā)生重繪。

          const cutiePie = document.querySelector('.cutie-pie');
          const t = () => {
          cutiePie.style.transform = `translate(${1 - Math.random() * 100}px, 0)`;
          requestAnimationFrame(t);
          }
          requestAnimationFrame(t);

          通過(guò)時(shí)間軸查看 requestIdleCallback 在 requestAnimationFrame、重排和繪制之后執(zhí)行,執(zhí)行間隔和 requestAnimationFrame 相應(yīng),在16ms左右,這符合上文提到的每一幀中瀏覽器執(zhí)行任務(wù)的順序。

          場(chǎng)景二,我們?cè)趫?chǎng)景一的基礎(chǔ)上停止動(dòng)畫(huà),

          此時(shí)頁(yè)面完全靜止,重排和繪制都停止了,但是瀏覽器仍然在注冊(cè) requestIdleCallback 并執(zhí)行其回調(diào),執(zhí)行間隔在50ms左右,并沒(méi)有以類似 requestAnimationFrame 的16ms間隔執(zhí)行。

          場(chǎng)景三,在場(chǎng)景一和場(chǎng)景二的基礎(chǔ)上,頁(yè)面分別不定時(shí)執(zhí)行一個(gè)超過(guò)16ms的任務(wù)。

          從上面兩個(gè)場(chǎng)景可以看出,無(wú)論頁(yè)面處于動(dòng)態(tài)還是靜止,如果有任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng),則這一幀中 requestIdleCallback 不會(huì)被執(zhí)行,而是被延遲到下一幀。

          場(chǎng)景四,上面的情形都沒(méi)有附加timeout參數(shù),現(xiàn)在我們?cè)趫?chǎng)景二靜止的頁(yè)面中給 requestIdleCallback 加上timeout參數(shù)再看看:

          const tick = function(deadline) {
          const remaining = deadline.timeRemaining();
          if (remaining > 0) {
          // do some stuff
          }
          requestIdleCallback(tick, {timeout: 500});
          };

          requestIdleCallback(tick, {timeout: 500});

          執(zhí)行間隔變到5-20ms左右,變得相當(dāng)混亂,原因可能是瀏覽器增加了額外的工作檢驗(yàn)任務(wù)是否已經(jīng)超時(shí),可見(jiàn)附加timeout屬性想讓它變得“靠譜”是要付出代價(jià)的,其調(diào)用頻率將大幅上升。

          通過(guò)以上分析,我們得知 requestAnimationFrame 的執(zhí)行規(guī)律符合上文對(duì)瀏覽器空閑時(shí)間的描述,如果一幀中任務(wù)的執(zhí)行時(shí)間超過(guò)了一定的時(shí)間(粗略估計(jì)在20ms左右),則任務(wù)會(huì)順延到下一幀中執(zhí)行。那利用它進(jìn)行卡頓監(jiān)控是否可行呢?即收集兩次執(zhí)行回調(diào)的間隔以判斷有無(wú)消耗時(shí)間較長(zhǎng)的任務(wù)阻塞線程。首先如果不加timeout參數(shù)是不可行的,試想如果頁(yè)面每一幀執(zhí)行時(shí)間都在20ms左右,則我們注冊(cè)的任務(wù)會(huì)持續(xù)被順延,而此時(shí)頁(yè)面并不卡頓(fps還在50左右),但是如果添加了timeout參數(shù),則這個(gè)函數(shù)的調(diào)用頻率大幅提高,甚至比 requestAnimationFrame 還要頻繁,然后結(jié)合其兼容性來(lái)看,綜合性能可能還不如后者。

          最長(zhǎng)執(zhí)行時(shí)間

          如果 requestIdleCallback 的執(zhí)行阻塞線程太久,就可能發(fā)生卡頓了,每一幀中requestIdleCallback 回調(diào)的最長(zhǎng)的執(zhí)行時(shí)間是50ms(這是建議的,但是你也可以做壞事),即回調(diào)中deadline.timeRemaining()的最大值小于50,這個(gè)閾值是RAIL模型定義的。

          通常人類對(duì)100ms以內(nèi)的延遲無(wú)感,而一旦超過(guò)這個(gè)閾值,則可能感覺(jué)到卡頓(jank)。下表中列舉了一些延遲時(shí)間和用戶體驗(yàn)的對(duì)應(yīng)關(guān)系:

          時(shí)間范圍用戶體驗(yàn)
          0-16ms頁(yè)面是絲滑的,每秒繪制60幀,即16ms每幀,其中包括瀏覽器繪制的時(shí)間(Raster和GPU等的時(shí)間消耗),生成一幀的時(shí)間在10ms左右
          0-100ms在此期間對(duì)用戶的交互作出響應(yīng),用戶感覺(jué)是立即得到結(jié)果,否則就會(huì)認(rèn)為沒(méi)有立即得到反饋
          100-1000ms被用戶理解為一項(xiàng)連續(xù)而自然的任務(wù),就像加載頁(yè)面或者改變視圖
          1000ms以上用戶對(duì)正在執(zhí)行的任務(wù)失去關(guān)注
          10000ms以上用戶感到沮喪,很可能放棄正在執(zhí)行的任務(wù),而且他們可能不會(huì)再回來(lái)了

          試想在某種理想情況下,瀏覽器開(kāi)始執(zhí)行 requestIdleCallback 中的回調(diào)任務(wù),同時(shí)用戶立即輸入一些文字,此時(shí)瀏覽器在處理回調(diào)任務(wù),輸入事件被掛起,等回調(diào)執(zhí)行完成后,用戶輸入事件對(duì)應(yīng)的回調(diào)得到執(zhí)行(oninput, onchange等),最后發(fā)生layout和repaint,用戶輸入的內(nèi)容才能出現(xiàn)在屏幕上。以上這一切都要在100ms之內(nèi)完成,RAIL模型將其分為了兩段,每一段50ms,分別用于處理兩個(gè)階段的任務(wù),具體見(jiàn)下圖:

          longtask的定義也是基于此模型,它表示執(zhí)行時(shí)間50ms以上的任務(wù),阻塞線程50ms以上可能引起交互時(shí)間延遲,造成紊亂的動(dòng)畫(huà)和滾動(dòng),在performance面板中任務(wù)右上角有一個(gè)清晰的角標(biāo)。

          使用建議

          基于此API的特殊性,提供一些使用建議:

          1. 只在低優(yōu)先級(jí)的任務(wù)中使用它,因?yàn)槟銦o(wú)法控制它的執(zhí)行時(shí)機(jī)。比如給后臺(tái)發(fā)送一些不怎么重要的監(jiān)控?cái)?shù)據(jù),或者進(jìn)行某種頁(yè)面檢查。
          2. 不要在其中修改DOM元素,因?yàn)樗谝粋€(gè)任務(wù)周期的layout結(jié)束之后才執(zhí)行,如果你修改了DOM,則會(huì)再次引發(fā)重排,這會(huì)對(duì)性能產(chǎn)生一定的影響。推薦的做法是創(chuàng)建一個(gè)documentFragment保存對(duì)dom的修改,并注冊(cè)requestAnimationFrame來(lái)應(yīng)用這些修改。
          3. 不在其中執(zhí)行難以預(yù)測(cè)執(zhí)行時(shí)間的任務(wù),比如以Promise的形式執(zhí)行某個(gè)接口請(qǐng)求。
          4. 只在必需的時(shí)候使用timeout選項(xiàng),瀏覽器會(huì)花費(fèi)額外的開(kāi)銷在檢查是否超時(shí)上,產(chǎn)生一些性能損失。

          React如何polyfill

          React16.6之后在任務(wù)調(diào)度中意圖使用 requestIdleCallback 這個(gè)函數(shù),但是它的兼容性并不好,Safari、安卓8.1以下、IE等都是重災(zāi)區(qū),所以React做了一個(gè)Polyfill,它是怎么做的呢?這里簡(jiǎn)要介紹下 React16.13.1 中實(shí)現(xiàn)的步驟。

          React維護(hù)了兩個(gè)小頂堆taskQueue和timerQueue,前者保存等待被調(diào)度的任務(wù),后者保存調(diào)度中的任務(wù),它們的排列依據(jù)分別是任務(wù)的超時(shí)時(shí)間和過(guò)期時(shí)間。到達(dá)超時(shí)時(shí)間的任務(wù)會(huì)從timerQueue移動(dòng)到taskQueue中,而在過(guò)期時(shí)間之內(nèi)taskQueue中的任務(wù)期望得到執(zhí)行,React調(diào)度的核心主要是以下幾點(diǎn):1. 何時(shí)把超時(shí)的任務(wù)從timerQueue轉(zhuǎn)移到taskQueue;2. taskQueue中任務(wù)的執(zhí)行時(shí)機(jī),以及后續(xù)任務(wù)的銜接;3. 何時(shí)暫停執(zhí)行任務(wù),把資源回交給瀏覽器。

          使用 unstable_scheduleCallback 注冊(cè)任務(wù)的時(shí)候可以提供兩個(gè)參數(shù),delay表示任務(wù)的超時(shí)時(shí)長(zhǎng),timeout表示任務(wù)的過(guò)期時(shí)長(zhǎng)(如果沒(méi)有指定,根據(jù)優(yōu)先程度任務(wù)會(huì)被分配默認(rèn)的timeout時(shí)長(zhǎng))。如果沒(méi)有提供delay,則任務(wù)被直接放到taskQueue中等待處理;如果提供了delay,則任務(wù)被放置在timerQueue中,此時(shí)如果taskQueue為空,且當(dāng)前任務(wù)在timerQueue的堆頂(當(dāng)前任務(wù)的超時(shí)時(shí)間最近),則使用 requestHostTimeout 啟動(dòng)定時(shí)器(setTimeout),在到達(dá)當(dāng)前任務(wù)的超時(shí)時(shí)間時(shí)執(zhí)行 handleTimeout ,此函數(shù)調(diào)用 advanceTimers 將timerQueue中的任務(wù)轉(zhuǎn)移到taskQueue中,此時(shí)如果taskQueue沒(méi)有開(kāi)啟執(zhí)行則調(diào)用 requestHostCallback 啟動(dòng)它,否則繼續(xù)遞歸地執(zhí)行 handleTimeout 處理下一個(gè)timerQueue中的任務(wù)。

          那么taskQueue如何啟動(dòng)呢?在支持 MessageChannel 的環(huán)境中是利用它來(lái)實(shí)現(xiàn)的:

          const channel = new MessageChannel();
          const port = channel.port2;
          // performWorkUntilDeadline:執(zhí)行我們注冊(cè)的任務(wù)
          channel.port1.onmessage = performWorkUntilDeadline;

          // 啟動(dòng)taskQueue的執(zhí)行,但是沒(méi)有立即執(zhí)行,而是使用Message Channel,因?yàn)樗膱?zhí)行時(shí)機(jī)是在瀏覽器每幀的繪制之后
          requestHostCallback = function(callback) {
          ...
          port.postMessage(null);
          };

          在上面的代碼中,port1收到message后開(kāi)始執(zhí)行 performWorkUntilDeadline,此時(shí)處于一幀繪制結(jié)束、下一幀即將開(kāi)始之際,這個(gè)函數(shù)先依據(jù)當(dāng)前的時(shí)間戳估算出該幀的過(guò)期時(shí)間(deadline默認(rèn)是在當(dāng)前時(shí)間戳的基礎(chǔ)上加5ms),然后調(diào)用flushWork,這個(gè)函數(shù)在taskQueue中任務(wù)執(zhí)行之前重置一些狀態(tài),再進(jìn)行一波性能分析,接著它調(diào)用了 workLoop 執(zhí)行taskQueue中的任務(wù)。

          終于可以執(zhí)行我們注冊(cè)的任務(wù)了!但在執(zhí)行任務(wù)之前,還要做一件事,就是調(diào)用我們上面提到過(guò)的 advanceTimers,將timerQueue中超時(shí)的任務(wù)轉(zhuǎn)移到taskQueue中。此時(shí)我們終于可以在5ms的時(shí)間分片里執(zhí)行taskQueue中的任務(wù)了,每執(zhí)行完一項(xiàng)任務(wù),都會(huì)執(zhí)行一下advanceTimers拉取超時(shí)任務(wù),然后如果此時(shí)還沒(méi)到達(dá)分片的時(shí)間閾值,則繼續(xù)執(zhí)行下一項(xiàng)任務(wù)直至到達(dá)deadline。此時(shí)如果taskQueue中還有任務(wù),則調(diào)用上文提到的 requestHostCallback 繼續(xù)在下一幀的5ms間隙里執(zhí)行任務(wù)直到任務(wù)窮盡;如果沒(méi)有更多任務(wù)了,則檢查timerQueue中是否有任務(wù),有則使用 requestHostTimeout 啟動(dòng)定時(shí)器,沒(méi)有則任務(wù)完全結(jié)束。

          上面所說(shuō)的時(shí)間分片很好地將瀏覽器繪制之后的空閑時(shí)間利用起來(lái)了,看到這里是不是很像我們的 requestIdleCallback 呢?5ms時(shí)間分片在有頻繁交互、重繪的頁(yè)面確實(shí)是不錯(cuò)的選擇,但如果頁(yè)面基本是靜態(tài)的,可以將一個(gè)時(shí)間分片拉長(zhǎng)嗎?在 React 源碼中確實(shí)是對(duì)此進(jìn)行了考慮的,這里利用了一個(gè)支持度不算太高的BOM API navigator.scheduling.isInputPending, 它表示用戶的輸入是否被掛起,也就是我們上文提到的用戶輸入沒(méi)有及時(shí)得到反饋。如果頁(yè)面沒(méi)有發(fā)生交互,且不需要重繪(needsPaint === false,這是程序內(nèi)的一個(gè)全局變量),則React會(huì)把時(shí)間分片提升到300ms(maxYieldInterval),雖然這個(gè)時(shí)間遠(yuǎn)超反應(yīng)延遲,但是taskQueue中每一項(xiàng)任務(wù)執(zhí)行完成后都會(huì)去檢測(cè)有沒(méi)有用戶交互和重繪,如果有則立即把資源回交給瀏覽器,所以不用擔(dān)心會(huì)因此發(fā)生卡頓。

          整個(gè)過(guò)程的大致圖解如下:

          參考

          1. Making the Most of Idle Moments with requestIdleCallback(): https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
          2. Measure performance with the RAIL model: https://web.dev/rail/
          3. requestIdleCallback和requestAnimationFrame詳解: https://www.jianshu.com/p/2771cb695c81
          4. requestIdleCallback and requestAnimationFrame: https://www.programmersought.com/article/49591734065/)



          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7.?70+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)這,與大家一起分享本文吧~
          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一区二区三区高清无码在线 | 丁香婷婷色视频 | 在线看片黄色免费Z | 亚洲欧美视频一区 | 青青草人妻 |