你应该知道的requestIdleCallback

本文介绍了requestIdleCallback API,探讨了其用途及与requestAnimationFrame的区别。并给出了使用示例及注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们都知道React 16实现了新的调度策略(Fiber), 新的调度策略提到的异步、可中断,其实就是基于浏览器的 requestIdleCallback和requestAnimationFrame两个API。所以这里我们有必要了解一下这两个API,关于Fiber部分后面会单开几篇讲。

什么是requestIdleCallback?

当关注用户体验,不希望因为一些不重要的任务(如统计上报)导致用户感觉到卡顿的话,就应该考虑使用requestIdleCallback。因为requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。

requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive.

requestIdleCallback用法示例

    requestIdelCallback(myNonEssentialWork);
    
    
    function myNonEssentialWork (deadline) {
    
      // deadline.timeRemaining()可以获取到当前帧剩余时间
      while (deadline.timeRemaining() > 0 && tasks.length > 0) {
        doWorkIfNeeded();
      }
      if (tasks.length > 0){
        requestIdleCallback(myNonEssentialWork);
      }
    }

requestIdleCallback和requestAnimationFrame有什么区别?

requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。
我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,那么在一帧里面浏览器都要做哪些事情呢,如下所示:

图片描述

图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。
假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调,如下图所示:

图片描述

由于requestIdleCallback利用的是帧的空闲时间,所以就有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,这其实也并不是我们期望的结果(如上报丢失),那么这种情况我们就需要在调用requestIdleCallback的时候传入第二个配置参数timeout了?

requestIdleCallback(myNonEssentialWork, { timeout: 2000 });

function myNonEssentialWork (deadline) {
  // 当回调函数是由于超时才得以执行的话,deadline.didTimeout为true
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
         tasks.length > 0) {
       doWorkIfNeeded();
    }
  if (tasks.length > 0) {
    requestIdleCallback(myNonEssentialWork);
  }
}

如果是因为timeout回调才得以执行的话,其实用户就有可能会感觉到卡顿了,因为一帧的执行时间必然已经超过16ms了

requestIdleCallback里面可以执行DOM修改操作吗?

强烈建议不要,从上面一帧的构成里面可以看到,requestIdleCallback回调的执行说明前面的工作(包括样式变更以及布局计算)都已完成。如果我们在callback里面做DOM修改的话,之前所做的布局计算都会失效,而且如果下一帧里有获取布局(如getBoundingClientRect、clientWidth)等操作的话,浏览器就不得不执行强制重排工作,这会极大的影响性能,另外由于修改dom操作的时间是不可预测的,因此很容易超出当前帧空闲时间的阈值,故而不推荐这么做。推荐的做法是在requestAnimationFrame里面做dom的修改,可以在requestIdleCallback里面构建Document Fragment,然后在下一帧的requestAnimationFrame里面应用Fragment。

除了不推荐DOM修改操作外,Promise的resolve(reject)操作也不建议放在里面,因为Promise的回调会在idle的回调执行完成后立刻执行,会拉长当前帧的耗时,所以不推荐。

推荐放在requestIdleCallback里面的应该是小块的(microTask)并且可预测时间的任务。关于microTask推荐看这里

requestIdleCallback的兼容情况

图片描述
推荐使用npm包request-idle-callback

参考资料

https://developers.google.com...
https://medium.com/@paul_iris...
https://juejin.im/entry/59082...
https://insights.thoughtworks...

好的,我明白你的意思了,我的插件也是通过点击来获取文件中diff,但是这样会导致diff页面跳转到对应点击文件。所有是否有办法拿到对应文件的diff而不进行跳转呢 下面是插件异步处理方法 // 温和的异步点击获取diff(进一步避免页面跳动) async function gentleAsyncClickToLoadDiff(file, maxWaitMs = 8000) { console.log(`[温和异步点击] 开始温和异步点击获取文件 ${file.fileName} 的diff`); // 保存当前页面状态 const currentScrollPosition = window.scrollY; const currentActiveElement = document.activeElement; const currentSelection = window.getSelection().toString(); console.log(`[温和异步点击] 保存当前页面状态: scrollY=${currentScrollPosition}, activeElement=${currentActiveElement?.tagName}`); // 检查是否有匹配的diff(文件名必须完全匹配) const exactMatch = fileDiffMap[file.fileName]; if (exactMatch) { console.log(`[温和异步点击] 文件 ${file.fileName} 已有完全匹配的diff缓存`); return exactMatch; } // 检查是否有智能匹配的diff const availableFiles = Object.keys(fileDiffMap); const matchingDiffFile = availableFiles.find(availableFile => { if (availableFile === '__single__') return false; const pageFileName = file.fileName; const apiFileName = availableFile; const getFileName = (path) => path.split('/').pop().split('\\').pop(); const pageFileBase = getFileName(pageFileName); const apiFileBase = getFileName(apiFileName); const matches = [ pageFileName === apiFileName, pageFileBase === apiFileBase, apiFileName.includes(pageFileBase), pageFileName.includes(apiFileBase), pageFileName.includes(apiFileName) || apiFileName.includes(pageFileName) ]; return matches.some(match => match); }); if (matchingDiffFile) { console.log(`[温和异步点击] 文件 ${file.fileName} 找到智能匹配的diff: ${matchingDiffFile}`); return fileDiffMap[matchingDiffFile]; } // 清空之前的diff缓存,确保获取到当前文件的diff const previousDiffMap = { ...fileDiffMap }; fileDiffMap = {}; console.log(`[温和异步点击] 清空diff缓存,准备加载 ${file.fileName} 的diff`); try { // 使用更温和的异步点击方式,进一步避免页面跳动 console.log(`[温和异步点击] 使用更温和的异步点击方式: ${file.fileName}`); // 方法1: 使用更长的延迟和更温和的方式 const titleElement = file.element.querySelector('.teamix-title'); if (titleElement) { console.log(`[温和异步点击] 找到文件标题元素:`, titleElement.tagName, titleElement.className); // 使用更长的延迟和更温和的方式 return new Promise((resolve) => { const gentleCallback = () => { // 使用更温和的点击方式 try { // 方法1: 使用dispatchEvent而不是click() const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, detail: 1, screenX: 0, screenY: 0, clientX: 0, clientY: 0 }); titleElement.dispatchEvent(clickEvent); console.log(`[温和异步点击] 温和点击完成: ${file.fileName}`); } catch (error) { console.warn(`[温和异步点击] 温和点击失败,尝试直接点击:`, error); titleElement.click(); } // 等待更长时间,确保diff加载完成 setTimeout(() => { // 检查是否成功加载到匹配的diff const newExactMatch = fileDiffMap[file.fileName]; if (newExactMatch) { console.log(`[温和异步点击] 成功: 已获取到 ${file.fileName} 的diff`); resolve(newExactMatch); return; } // 检查智能匹配 const newMatchingDiffFile = Object.keys(fileDiffMap).find(availableFile => { if (availableFile === '__single__') return false; const pageFileName = file.fileName; const apiFileName = availableFile; const getFileName = (path) => path.split('/').pop().split('\\').pop(); const pageFileBase = getFileName(pageFileName); const apiFileBase = getFileName(apiFileName); const matches = [ pageFileName === apiFileName, pageFileBase === apiFileBase, apiFileName.includes(pageFileBase), pageFileName.includes(apiFileBase), pageFileName.includes(apiFileName) || apiFileName.includes(pageFileName) ]; return matches.some(match => match); }); if (newMatchingDiffFile) { console.log(`[温和异步点击] 成功: 找到智能匹配的diff: ${newMatchingDiffFile}`); resolve(fileDiffMap[newMatchingDiffFile]); return; } // 如果没有找到匹配的diff,返回null console.log(`[温和异步点击] 未找到匹配的diff: ${file.fileName}`); resolve(null); }, 3000); // 增加等待时间到3秒,确保diff加载完成 }; // 使用更长的延迟,确保页面完全稳定 setTimeout(() => { if (window.requestIdleCallback) { window.requestIdleCallback(gentleCallback, { timeout: 5000 }); } else { setTimeout(gentleCallback, 500); } }, 1000); // 额外等待1秒,确保页面稳定 }); } // 方法2: 如果找不到标题元素,尝试其他可点击元素 console.log(`[温和异步点击] 方法2: 尝试其他可点击元素 ${file.fileName}`); const clickableElement = file.element.querySelector('a, button, [role="button"], span'); if (clickableElement && !clickableElement.classList.contains('next-tree-node-indent-unit')) { console.log(`[温和异步点击] 找到可点击元素:`, clickableElement.tagName, clickableElement.className); return new Promise((resolve) => { const gentleCallback = () => { try { const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, detail: 1, screenX: 0, screenY: 0, clientX: 0, clientY: 0 }); clickableElement.dispatchEvent(clickEvent); console.log(`[温和异步点击] 温和点击可点击元素完成: ${file.fileName}`); } catch (error) { console.warn(`[温和异步点击] 温和点击失败,尝试直接点击:`, error); clickableElement.click(); } setTimeout(() => { // 检查结果 const newExactMatch = fileDiffMap[file.fileName]; if (newExactMatch) { console.log(`[温和异步点击] 方法2成功: 已获取到 ${file.fileName} 的diff`); resolve(newExactMatch); return; } const newMatchingDiffFile = Object.keys(fileDiffMap).find(availableFile => { if (availableFile === '__single__') return false; const pageFileName = file.fileName; const apiFileName = availableFile; const getFileName = (path) => path.split('/').pop().split('\\').pop(); const pageFileBase = getFileName(pageFileName); const apiFileBase = getFileName(apiFileName); const matches = [ pageFileName === apiFileName, pageFileBase === apiFileBase, apiFileName.includes(pageFileBase), pageFileName.includes(apiFileBase), pageFileName.includes(apiFileName) || apiFileName.includes(pageFileName) ]; return matches.some(match => match); }); if (newMatchingDiffFile) { console.log(`[温和异步点击] 方法2成功: 找到智能匹配的diff: ${newMatchingDiffFile}`); resolve(fileDiffMap[newMatchingDiffFile]); return; } console.log(`[温和异步点击] 方法2失败: 未找到匹配的diff: ${file.fileName}`); resolve(null); }, 3000); }; setTimeout(() => { if (window.requestIdleCallback) { window.requestIdleCallback(gentleCallback, { timeout: 5000 }); } else { setTimeout(gentleCallback, 500); } }, 1000); }); } // 方法3: 如果都找不到可点击元素,尝试直接点击文件元素 console.log(`[温和异步点击] 方法3: 尝试直接点击文件元素 ${file.fileName}`); if (file.element && typeof file.element.click === 'function') { return new Promise((resolve) => { const gentleCallback = () => { try { const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, detail: 1, screenX: 0, screenY: 0, clientX: 0, clientY: 0 }); file.element.dispatchEvent(clickEvent); console.log(`[温和异步点击] 温和点击文件元素完成: ${file.fileName}`); } catch (error) { console.warn(`[温和异步点击] 温和点击失败,尝试直接点击:`, error); file.element.click(); } setTimeout(() => { // 检查结果 const newExactMatch = fileDiffMap[file.fileName]; if (newExactMatch) { console.log(`[温和异步点击] 方法3成功: 已获取到 ${file.fileName} 的diff`); resolve(newExactMatch); return; } const newMatchingDiffFile = Object.keys(fileDiffMap).find(availableFile => { if (availableFile === '__single__') return false; const pageFileName = file.fileName; const apiFileName = availableFile; const getFileName = (path) => path.split('/').pop().split('\\').pop(); const pageFileBase = getFileName(pageFileName); const apiFileBase = getFileName(apiFileName); const matches = [ pageFileName === apiFileName, pageFileBase === apiFileBase, apiFileName.includes(pageFileBase), pageFileName.includes(apiFileBase), pageFileName.includes(apiFileName) || apiFileName.includes(pageFileName) ]; return matches.some(match => match); }); if (newMatchingDiffFile) { console.log(`[温和异步点击] 方法3成功: 找到智能匹配的diff: ${newMatchingDiffFile}`); resolve(fileDiffMap[newMatchingDiffFile]); return; } console.log(`[温和异步点击] 方法3失败: 未找到匹配的diff: ${file.fileName}`); resolve(null); }, 3000); }; setTimeout(() => { if (window.requestIdleCallback) { window.requestIdleCallback(gentleCallback, { timeout: 5000 }); } else { setTimeout(gentleCallback, 500); } }, 1000); }); } console.log(`[温和异步点击] 所有方法都失败: ${file.fileName}`); return null; } catch (error) { console.error(`[温和异步点击] 点击文件 ${file.fileName} 时发生错误:`, error); // 恢复之前的diff缓存 fileDiffMap = { ...previousDiffMap }; return null; } }
最新发布
08-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值