神级JS API,谁用谁好用

B站影视 港台电影 2025-10-28 21:30 1

摘要:ResizeObserver 是一个浏览器原生的 JavaScript API,用于监听 DOM 元素尺寸的变化。它类似于 MutationObserver ,但专门用于观察元素的大小(宽高)变化,而无需依赖 window.resize 事件(后者只对视口变化

前端圈一年 365 天,天天出新框架,但真正能让你 少写代码、少引依赖、少踩性能坑 的,其实是浏览器爸爸早已偷偷内置的「 原生外挂 」。

ResizeObserver 是一个浏览器原生的 JavaScript API,用于 监听 DOM 元素尺寸的变化 。它类似于 MutationObserver ,但专门用于观察元素的大小(宽高)变化,而无需依赖 window.resize 事件(后者只对视口变化有效)。

const resizeObserver = new ResizeObserver(entries => {for (let entry of entries) {const { width, height } = entry.con #技术分享tentRect;console.log(`元素尺寸:${width} x ${height}`);console.log('目标元素:', entry.target);}});resizeObserver.observe(document.querySelector('#my-element'));entry.contentRect :表示 内容区域 (不包括 padding、border、margin),类似于 getComputedStyle.width/height 的计算结果。如果你需要包括 border 和 padding 的尺寸,可以结合 entry.target.getBoundingClientRect 使用。resizeObserver.unobserve(element);resizeobserver.disconnect;

建议 :在组件销毁(如 React 的 useEffect 清理函数、Vue 的 onBeforeUnmount )时调用 disconnect ,避免内存泄漏。

响应式组件 :当容器尺寸变化时动态调整子元素(如图表、Canvas、视频)。自定义滚动条或布局 :监听内容区域变化以更新 UI。替代 window.onresize :更精确地响应 特定元素 的尺寸变化,而非整个窗口。Web Components / 封装组件 :内部自动适配父容器大小。

兼容性已非常广泛,现代项目可放心使用。

可通过 GitHub - juggle/resize-observer 提供的 polyfill:

npm install @juggle/resize-observerimport ResizeObserver from '@juggle/resize-observer';if (!window.ResizeObserver) { window.ResizeObserver = ResizeObserver; }import { useEffect, useRef } from 'react';function MyComponent { const containerRef = useRef(null);useEffect( => { const observer = new ResizeObserver(entries => { for (let entry of entries) { console.log('新宽度:', entry.contentRect.width); } });if (containerRef.current) { observer.observe(containerRef.current); }return => { observer.disconnect; }; }, );return 可变尺寸容器; }

IntersectionObserver 是一个强大的浏览器原生 API,用于 异步监听目标元素与祖先元素(或视口)的交叉(相交)状态变化 。它常用于实现 懒加载、无限滚动、曝光统计、动画触发 等场景,性能远优于传统的 scroll 事件监听。

const observer = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {if (entry.isIntersecting) { console.log('元素进入视口:', entry.target); } else { console.log('元素离开视口'); } }); });observer.observe(document.querySelector('#my-element'));const options = {root: null,rootMargin: '0px',threshold: 0.5};const observer = new IntersectionObserver(callback, options);threshold: 0 :只要有一点进入就触发(默认)。threshold: 1 :完全进入才触发。threshold: [0, 0.25, 0.5, 0.75, 1] :在 0%、25%、50%... 时都触发。javascript编辑observer.unobserve(element);observer.disconnect;

建议 :在组件销毁时调用 disconnect ,防止内存泄漏。

const imgObserver = new IntersectionObserver((entries) => {entries.forEach(entry => {if (entry.isIntersecting) {const img = entry.target;img.src = img.dataset.src;imgObserver.unobserve(img);}});});document.querySelectorAll('img[data-src]').forEach(img => { imgObserver.observe(img); });

观察一个“哨兵”元素(如分页加载提示),当它进入视口时触发加载。

当广告或内容区域进入视口一定比例时,上报“曝光”事件。

元素进入视口时添加 CSS 动画类。

浏览器兼容性

现代浏览器支持良好,移动端也广泛可用。

官方推荐 polyfill(由 W3C 团队维护):

npm install intersection-observerimport 'intersection-observer';

注意:polyfill 会回退到 scroll + getBoundingClientRect ,性能较差,仅用于兼容。

| API | 用途 | | ---

| IntersectionObserver | 监听元素是否进入/离开视口(或指定容器) | | ResizeObserver | 监听元素尺寸变化 | | MutationObserver | 监听 DOM 结构或属性变化 |

三者互补,常结合使用。

小技巧

Page visibility API 是一个浏览器原生 API,用于检测 当前网页是否对用户可见 (即是否处于前台标签页或被最小化/切换到后台)。它可以帮助开发者优化性能、节省资源,或实现特定业务逻辑(如暂停视频、停止轮询、统计停留时长等)。

返回当前页面的可见性状态,可能值包括:

| 值 | 含义 | | ---

| 'visible' | 页面可见(处于前台标签页) | | 'hidden' | 页面不可见(切换到其他标签页、最小化窗口、锁屏等) | | 'prerender' | 页面正在预渲染(已废弃,现代浏览器基本不用) | | 'unloaded' | 页面即将卸载(极少使用) |

实际开发中主要关注 'visible' 和 'hidden' 。

true :页面不可见false :页面可见

⚠️ 虽仍可用,但 MDN 建议使用 visibilityState 。

当页面可见性状态改变时触发。

✅ 基本用法示例function handleVisibilityChange {if (document.visibilityState === 'visible') {console.log('页面回到前台');} else if (document.visibilityState === 'hidden') {console.log('页面进入后台');}}document.addEventListener('visibilitychange', handleVisibilityChange);const video = document.querySelector('video');document.addEventListener('visibilitychange', => { if (document.hidden) { video.pause; } else { video.play; } });let intervalId;function startPolling { intervalId = setInterval(fetchData, 5000); }function stopPolling { clearInterval(intervalId); }document.addEventListener('visibilitychange', => { if (document.hidden) { stopPolling; } else { startPolling; } });startPolling;let startTime = Date.now;let totalVisibleTime = 0;document.addEventListener('visibilitychange', => { if (document.hidden) { totalVisibleTime += Date.now - startTime; } else { startTime = Date.now; } });window.addEventListener('beforeunload', => { totalVisibleTime += Date.now - startTime; sendToAnalytics({ visibleTime: totalVisibleTime }); });

在页面不可见时暂停渲染循环,减少 CPU/GPU 消耗。

浏览器兼容性

兼容性极佳,几乎所有现代浏览器都支持。

⚠️ 注意事项不保证精确性 :在某些系统(如 macOS 快速切换)中,状态切换可能有微小延迟。不是用户活跃度检测 :页面可见 ≠ 用户正在看(用户可能切到其他应用但浏览器窗口仍在前台)。与 blur / focus 事件的区别 :window.onfocus / window.onblur :监听 窗口焦点 (如切换到其他应用)。visibilitychange :监听 标签页是否可见 (即使窗口有焦点,但标签页在后台也算 hidden)。两者可结合使用以获得更全面的状态判断。let isPageVisible = !document.hidden;let isWindowFocused = !document.hasFocus;window.addEventListener('focus', => { isWindowFocused = true; if (isPageVisible) { console.log('用户很可能正在看页面'); } });window.addEventListener('blur', => { isWindowFocused = false; });document.addEventListener('visibilitychange', => { isPageVisible = !document.hidden; });

Web Share API 是一个现代浏览器提供的原生 API,允许网页 调用操作系统级别的分享功能 ,让用户将内容(如链接、文本、标题等)快速分享到设备上安装的其他应用(如微信、邮件、短信、笔记等)。

✅ 基本用法if (navigator.share) {navigator.share({title: '分享标题',text: '分享的描述文字',url: 'HTTPS://example.com'}).then( => {console.log('分享成功');}).catch((error) => {if (error.name === 'AbortError') {console.log('用户取消了分享');} else {console.error('分享失败:', error);}});} else {alert('您的浏览器不支持 Web Share API,请手动复制链接');}

⚠️ 必须在用户手势触发的上下文中调用 (如点击事件),否则会抛出安全错误。

仅限安全上下文 :必须在 HTTPS (或 localhost )下使用。用户手势要求 :只能在 click 、 touchend 等用户操作回调中调用。字段非全部必需 :但至少要提供 title 、 text 、 url 中的一个(推荐提供 url )。无法控制目标应用 :分享目标由操作系统决定,开发者无法指定(如“只分享到微信”)。

| 平台 | 浏览器 | 支持情况 | | ---

| Android | Chrome 61+

| iOS | Safari 12.2+

| Windows | Chrome 76+

| macOS | Safari 13+

| Linux | 部分浏览器 | ⚠️ 有限支持 |

可通过 caniuse.com/web-share 查看最新兼容性。

现代浏览器(Chrome 89+ 等)支持分享 文件 (如图片、PDF):

if (navigator.canShare && navigator.canShare({ files: [file] })) {await navigator.share({title: '图片分享',files: [file]});}

注意:文件必须来自用户选择(如 )或由网页生成,不能是任意网络文件。

当不支持 Web Share 时,可提供复制链接或自定义分享按钮:

function fallbackShare(url) {const input = document.createElement('input');input.value = url;document.body.appendChild(input);input.select;document.execCommand('copy');document.body.removeChild(input);alert('链接已复制到剪贴板');}function ShareButton({ url, title, text }) {const handleShare = async => {if (navigator.share) {try {await navigator.share({ url, title, text });} catch (err) {console.warn('分享被取消或失败', err);}} else {fallbackShare(url);}};return ( 分享 ); } 优势

Wake Lock API 是一个现代 Web API,允许网页 防止设备进入休眠状态 (如屏幕变暗、锁屏),常用于需要长时间保持活跃的场景,例如:

视频播放器(避免播放时屏幕关闭)导航应用(持续显示路线)扫码/AR 应用(保持摄像头活跃)阅读器/电子书(长时间阅读不锁屏)let wakeLock = null;async function requestWakeLock { try { wakeLock = await navigator.wakeLock.request('screen'); console.log('Wake Lock 已激活');wakeLock.addEventListener('release', => { console.log('Wake Lock 已释放'); });} catch (err) { console.error('Wake Lock 请求失败:', err); } }document.getElementById('keepAwakeBtn').addEventListener('click', requestWakeLock);

锁会在以下情况 自动释放

页面进入后台( visibilitychange → hidden )

浏览器标签页关闭

用户手动锁屏

页面失去焦点(部分浏览器)

| 浏览器 | 支持情况 | | ---

| Chrome | ✅ 84+(Android & Desktop) | | Edge | ✅ 84+

| Safari | ❌ 不支持(iOS/macOS 均未实现) | | Firefox | ❌ 默认禁用(需手动开启 dom.wakelock.enabled) |

移动端 Chrome(Android)支持最好 ,iOS Safari 完全不支持

可通过 caniuse.com/wake-lock 查看最新状态。

const video = document.querySelector('video');video.addEventListener('play', async => { if ('wakeLock' in navigator) { try { wakeLock = await navigator.wakeLock.request('screen'); } catch (err) { console.warn('无法保持屏幕常亮:', err); } } });video.addEventListener('pause', => { if (wakeLock) wakeLock.release; });document.addEventListener('visibilitychange', => {if (document.hidden && wakeLock) {wakeLock.release;}});

在不支持 Wake Lock 的环境(如 iOS):

提示用户“请手动关闭自动锁屏”使用全屏 API( requestFullscreen )有时可延长屏幕活跃时间(非可靠)对于视频,可尝试使用 等属性优化体验不要滥用 :长时间保持唤醒会显著增加耗电。始终提供关闭选项 :让用户能手动禁用“保持唤醒”。测试真实设备 :模拟器行为可能与真机不同。

BroadcastChannel 是一个现代 Web API,允许 同源(same-origin)的不同浏览器上下文

它类似于“发布-订阅”模式:一个上下文发送消息,所有监听同一频道的其他上下文都能收到。

const channel = new BroadcastChannel('my-app-channel');channel.addEventListener('message', (event) => { console.log('收到消息:', event.data); });channel.postMessage({ type: 'USER_LOGIN', userId: 123 });window.addEventListener('beforeunload', => {channel.close;});

自动广播 :消息会发送给 所有 监听 'my-app-channel' 的同源上下文(包括发送者自己,除非你过滤)。

安全限制同源策略 :只有协议 + 域名 + 端口完全相同的页面才能通信。https://example.com/page1 和 https://example.com/page2 ✅https://example.com 和 https://sub.example.com ❌http://localhost:3000 和 http://localhost:8080 ❌不支持跨域 :不能用于跨域 iframe 通信(此时应考虑 postMessage + origin 验证)。

当用户在一个标签页登录,其他标签页自动更新状态:

channel.postMessage({ type: 'AUTH_CHANGED', user: { id: 1, name: 'Alice' } });channel.onmessage = (e) => { if (e.data.type === 'AUTH_CHANGED') { if (e.data.user) { updateUI(e.data.user); } else { logoutAllTabs; } } };

例如后台管理页更新后,通知前台页面重新拉取配置。

主线程和多个 worker 可通过 BroadcastChannel 广播消息。

浏览器兼容性(截至 2025 年)

| 浏览器 | 支持情况 | | ---

| Chrome | ✅ 54+

| Edge | ✅ 79+

| Firefox | ✅ 38+

| Safari | ✅ 15.4+(iOS 15.4+

| iOS WebView | ✅ 15.4+ |

⚠️ Safari 在 15.4 之前完全不支持 ,如需兼容旧版 iOS,需使用 localStorage + storage 事件作为 fallback。

利用 localStorage 的 storage 事件实现类似广播:

function broadcastFallback(message) {localStorage.setItem('broadcast-msg', JSON.stringify({...message,timestamp: Date.now}));}window.addEventListener('storage', (e) => { if (e.key === 'broadcast-msg') { const message = JSON.parse(e.newValue); console.log('Fallback 收到:', message); } });

缺点:只能传递字符串,且 storage 事件 不会在当前标签页触发 (正好避免自己收到自己发的消息)。

| 方式 | 适用场景 | 跨域 | 多标签 | Worker | | ---

| BroadcastChannel | 同源多上下文广播 | ❌ | ✅ | ✅ | | window.postMessage | 精确点对点通信 | ✅(需验证 origin) | ✅(需持有 window 引用) | ✅ | | SharedWorker | 多页面共享逻辑 | ❌ | ✅ | ✅(作为中介) | | localStorage + storage | 简单广播(旧浏览器) | ❌ | ✅ | ❌ |

小技巧避免无限循环 :如果多个页面都响应消息并再次广播,可能形成循环。建议使用 type 字段区分消息来源或添加防重机制。结构化克隆 : postMessage 支持传输 ArrayBuffer 、 Blob 、 Map 等(遵循 结构化克隆算法 ),不只是 JSON。import { useEffect } from 'react';function useBroadcastChannel(channelName, onMessage) { useEffect( => { const channel = new BroadcastChannel(channelName); channel.onmessage = onMessage;return => { channel.close; }; }, [channelName, onMessage]); }function App { useBroadcastChannel('theme-channel', (e) => { if (e.data.type === 'THEME_CHANGE') { document.body.className = e.data.theme; } });const changeTheme = (theme) => { new BroadcastChannel('theme-channel').postMessage({ type: 'THEME_CHANGE', theme }); };return changeTheme('dark')}>切换深色; }

BroadcastChannel 和 Vuex / Redux

核心区别用 BroadcastChannel :标签页 A 登录 → 通过 channel.postMessage({ type: 'LOGIN', user }) 广播。标签页 B、C(即使没用 Vue/React)监听到消息 → 各自更新自己的 UI 。每个页面 独立维护自己的状态 ,只是通过消息“同步”了登录事件。用 Vuex :只在 当前标签页内 ,多个 Vue 组件共享 store.state.user 。标签页 A 的 Vuex 无法直接影响 标签页 B 的 Vuex。如果你打开两个标签页,它们有 两个完全独立的 Vuex 实例

✅ 所以: Vuex 管“页面内”,BroadcastChannel 管“页面间”

实际项目中,两者常配合使用

const channel = new BroadcastChannel('auth-channel');const store = new Vuex.Store({ state: { user: null }, mutations: { SET_USER(state, user) { state.user = user; } }, actions: { login({ commit }, user) { commit('SET_USER', user); channel.postMessage({ type: 'LOGIN', user }); } } });channel.onmessage = (e) => { if (e.data.type === 'LOGIN') { store.commit('SET_USER', e.data.user); } else if (e.data.type === 'LOGOUT') { store.commit('SET_USER', null); } };

这样:

页面内:Vuex 管理状态,组件自动响应。页面间:BroadcastChannel 同步关键事件。

有!社区有一些库尝试结合两者,例如:

vuex-shared-mutations :通过 localStorage 或 BroadcastChannel 同步 Vuex 的 mutations。自定义方案:监听 storage 事件或 BroadcastChannel ,触发本地 store 更新。

但核心思想不变:跨标签页通信靠 BroadcastChannel(或 storage),状态管理靠 Vuex

✅ 总结

| 你想做…… | 该用…… | | ---

| 组件之间共享状态、触发更新 | Vuex / Redux / Context / Zustand | | 多个浏览器标签页同步登录状态 | BroadcastChannel(或 localStorage +

| 让 Vuex 状态在多标签页同步 | BroadcastChannel + Vuex 结合 |

PerformanceObserver 是一个强大的 Web API,用于 异步监听性能相关的事件和指标 ,而无需轮询 performance.getEntries 。它是现代 Web 性能监控(如 Core Web Vitals)的核心工具。

核心作用

监听浏览器自动记录的 Performance Timeline(性能时间线) 中的新条目,例如:

资源加载( resource )导航 timing( navigation )长任务( longtask )元素曝光( element ,实验性)最重要:CLS、LCP、FCP、INP 等 Web Vitals 指标 基本用法const observer = new PerformanceObserver((list) => {for (const entry of list.getEntries) {console.log(entry.name, entry.entryType, entry.startTime, entry.duration);}});observer.observe({ entryTypes: ['resource', 'navigation', 'paint'] });

⚠️ 必须指定 entryTypes (或 type ),否则不会触发回调。

| entryType | 说明 | 典型用途 | | ---

| 'navigation' | 页面导航性能(如 DNS、TCP、DOMContentLoaded) | 分析首屏加载瓶颈 | | 'resource' | 所有资源加载(JS、CSS、图片、XHR、fetch) | 监控第三方资源、慢请求 | | 'paint' | 首次绘制(FP)、首次内容绘制(FCP) | 衡量视觉加载速度 | | 'longtask' | 超过 50ms 的主线程任务 | 识别卡顿、影响交互响应的原因 | | 'largest-contentful-paint' (LCP) | 最大内容元素渲染时间 | 核心 Web Vitals 指标 | | 'layout-shift' (CLS) | 累积布局偏移 | 检测页面“抖动” | | 'first-input' (FID) / 'event' (INP) | 首次输入延迟 / 交互到下次绘制 | 衡量交互响应性 |

LCP、CLS、INP 等现代指标必须通过 PerformanceObserver 获取 ,无法通过 getEntries 静态读取。

let lcpReported = false;new PerformanceObserver((entryList) => { const lcpEntry = entryList.getEntries.at(-1); if (!lcpReported) { console.log('LCP:', lcpEntry.startTime); sendToAnalytics({ metric: 'LCP', value: lcpEntry.startTime }); lcpReported = true; } }).observe({ type: 'largest-contentful-paint', buffered: true });

buffered: true 表示获取 已发生但未被观察到的历史条目 (对 LCP/CLS 必须加!)。

let clsValue = 0;new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries) { if (!entry.hadRecentInput) { clsValue += entry.value; } } console.log('当前 CLS:', clsValue); }).observe({ type: 'layout-shift', buffered: true });new PerformanceObserver((list) => {for (const resource of list.getEntries) {if (resource.duration > 2000) {console.warn('慢资源:', resource.name, resource.duration + 'ms');}}}).observe({ entryTypes: ['resource'] });new PerformanceObserver((list) => {for (const task of list.getEntries) {if (task.duration > 100) {console.log('长任务:', task.duration + 'ms', task.attribution);}}}).observe({ entryTypes: ['longtask'] });

需要先注册长任务支持(部分浏览器需 polyfill):

```javascript

if (PerformanceObserver.supportedEntryTypes.includes('longtask')) {

}

```

浏览器兼容性✅ Chrome / Edge:全面支持(包括 Web Vitals)✅ Firefox:支持基础类型( resource , navigation ),Web Vitals 支持较弱✅ Safari 15+:支持 LCP、CLS、FCP 等核心指标❌ IE:不支持

推荐使用 Google 的 web-vitals 库 跨浏览器采集 Core Web Vitals。

| 方式 | 优点 | 缺点 | | ---

| PerformanceObserver | 异步、实时、支持 Web Vitals、不阻塞主线程 | 需要提前注册监听 | | performance.getEntries | 简单直接、可查询历史 | 无法获取动态指标(如 LCP 在发生时才能确定)、需轮询 |

现代性能监控应优先使用 PerformanceObserver

最佳实践尽早注册 :在 中或页面顶部初始化,避免漏掉早期指标。使用 buffered: true :确保捕获 FCP、LCP、CLS 等可能在监听前已发生的指标。避免内存泄漏 :通常不需要 disconnect ,因为性能条目是一次性的。结合 RUM(真实用户监控) :将数据上报到分析平台(如 GA4、Sentry、自建服务)。web-vitals npm 包 :Google 官方封装,一行代码获取 Web Vitals。import { getLCP, getCLS, getFCP } from 'web-vitals';getLCP(console.log);

React(使用 Hook)Vue 3(使用 Composition API)

✅ 共同前提

我们使用 Google 官方的 web-vitals 库,它已封装好 PerformanceObserver 的兼容逻辑。

npm install web-vitalsimport { useEffect } from 'react';import { getCLS, getFCP, getLCP, getFID, getINP } from 'web-vitals';type WebVitalsMetric = { id: string; name: string; value: number; delta: number; entries: PerformanceEntry; attribution: Record; };type WebVitalsOptions = { onReport?: (metric: WebVitalsMetric) => void; reportAll?: boolean; };export const useWebVitals = ({ onReport, reportAll = false }: WebVitalsOptions = {}) => { useEffect( => { const report = (metric: WebVitalsMetric) => { onReport?.(metric); if (process.env.NODE_ENV === 'development') { console.log('Web Vitals:', metric); } };getCLS(report, reportAll); getFCP(report, reportAll); getLCP(report, reportAll); getFID(report); getINP(report, reportAll);}, [onReport, reportAll]); }; 使用示例import { useWebVitals } from './hooks/useWebVitals';function App { useWebVitals({ onReport: (metric) => { fetch('/api/performance', { method: 'POST', body: JSON.stringify(metric), headers: { 'Content-Type': 'application/json' } }); } });return 你的应用; }

优点 :自动处理浏览器兼容性、只上报有效指标、支持开发环境日志。

import { onMounted } from 'vue';import { getCLS, getFCP, getLCP, getFID, getINP } from 'web-vitals';type WebVitalsMetric = { id: string; name: string; value: number; delta: number; entries: PerformanceEntry; attribution: Record; };export function useWebVitals( onReport?: (metric: WebVitalsMetric) => void, reportAll = false ) { onMounted( => { const report = (metric: WebVitalsMetric) => { onReport?.(metric); if (import.meta.env.DEV) { console.log('Web Vitals:', metric); } };getCLS(report, reportAll); getFCP(report, reportAll); getLCP(report, reportAll); getFID(report); getINP(report, reportAll); }); } 使用示例import { useWebVitals } from './composables/useWebVitals';useWebVitals((metric) => { fetch('/api/performance', { method: 'POST', body: JSON.stringify(metric), headers: { 'Content-Type': 'application/json' } }); }); 你的应用 高级:监控慢资源加载(自定义 PerformanceObserver)

如果你还想监控 JS/CSS/图片等资源加载性能,可以额外封装一个 Hook:

import { useEffect } from 'react';export const useResourcePerformance = (onSlowResource: (entry: PerformanceResourceTiming) => void) => { useEffect( => { if (!PerformanceObserver.supportedEntryTypes.includes('resource')) return;const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries as PerformanceResourceTiming) { if (entry.duration > 2000) { onSlowResource(entry); } } });observer.observe({ entryTypes: ['resource'] });return => { observer.disconnect; }; }, [onSlowResource]); };

Vue 版本类似,用 onMounted + onUnmounted 管理生命周期。

上报建议LCP、FCP、CLS :每个页面会话上报一次( reportAll: false )。INP/FID :用户每次交互可能触发,可采样上报。慢资源 :可聚合后批量上报,避免频繁请求。

requestIdleCallback 是一个浏览器提供的 API,用于 在浏览器主线程空闲时执行低优先级任务 ,避免影响关键操作(如用户输入、动画、布局等),从而提升页面流畅性和响应性。

它是实现“ 协作式调度(Cooperative Scheduling) ”的关键工具,React 16+ 的 Fiber 架构就受其启发(尽管 React 最终未直接使用它)。

基本用法function doLowPriorityWork(deadline) {while (deadline.timeRemaining > 0 || deadline.didTimeout) { if (hasWork) { performUnitOfWork; } else { break; } }if (hasMoreWork) { requestIdleCallback(doLowPriorityWork); } }requestIdleCallback(doLowPriorityWork, { timeout: 2000 });deadline.timeRemaining :返回一个 估算值 (单位:毫秒),表示当前帧剩余的空闲时间(通常 ≤ 50ms)。deadline.didTimeout :如果设置了 timeout 且超时,则为 true ,此时应尽快完成任务。

⚠️ timeout 会降低优先级优势 ,仅用于“最终必须执行”的兜底场景。

requestIdleCallback( => {import('./NextPageComponent');});let logs = ;function sendLogs { if (logs.length > 0) { navigator.sendBeacon('/log', JSON.stringify(logs)); logs = ; } }function addLog(event) { logs.push(event); requestIdleCallback(sendLogs, { timeout: 5000 }); }

在用户停止滚动后,利用空闲时间预计算可视区域外的 item 尺寸。

如统计停留时长、点击热力图聚合等。

| 浏览器 | 支持情况 | | ---

| Chrome | ✅ 47+

| Edge | ✅ 79+

| Firefox | ❌ 不支持(已明确拒绝实现) | | Safari | ❌ 不支持 | | iOS / Android WebView | ❌ 基本不可用 |

现实:仅 Chrome/Edge 支持,Firefox 和 Safari 永远不会支持!

可通过 caniuse.com/requestidle… 查看。

由于兼容性差,生产环境必须提供 fallback

const requestIdleCallback =window.requestIdleCallback ||function (callback) {const start = Date.now;return setTimeout( => {callback({didTimeout: false,timeRemaining: => Math.max(0, 50 - (Date.now - start))});}, 1);};const cancelIdleCallback = window.cancelIdleCallback || function (id) { clearTimeout(id); };方案 2:使用 requestAnimationFrame + 时间切片(更接近原生行为)

适用于需要精细控制的任务调度(如 React Fiber 的思路)。

方案 3:直接使用 setTimeout(fn, 0) 或 queueMicrotask

适用于非关键但需异步执行的任务,但无法利用“空闲时间”。

不要执行高优先级任务 :如用户输入响应、动画更新。避免长时间运行 :即使 timeRemaining 返回较大值,也应分片处理。不要依赖精确时间 : timeRemaining 是估算值,可能突然变为 0。移动端效果有限 :低端设备空闲时间极少,可能长期不触发。

| API | 时机 | 用途 | | ---

| requestAnimationFrame | 每一帧开始前(约 16ms 一次) | 动画、视觉更新 | | requestIdleCallback | 每一帧结束后,若有空闲 | 低优先级后台任务 |

✅ 两者互补: rAF 保证流畅动画, rIC 避免阻塞动画。

React :内部调度器受 rIC 启发,但使用自定义实现(因兼容性问题)。Vue / Svelte :一般不直接使用,但可用于自定义性能优化逻辑。推荐 :在业务代码中谨慎使用,并做好降级。function scheduleIdleWork(workFn, timeout = 2000) {if ('requestIdleCallback' in window) {return requestIdleCallback((deadline) => {if (deadline.timeRemaining > 0 || deadline.didTimeout) {workFn;}}, { timeout });} else {return setTimeout(workFn, 0);}}const id = scheduleIdleWork( => { console.log('在空闲时执行'); }); 总结作用 :在浏览器空闲时执行低优先级任务,提升用户体验。现状仅 Chrome/Edge 支持 ,Firefox/Safari 已放弃。建议 :可用于 非关键优化 (如预加载、日志上报)。必须提供降级方案 。不要用于核心功能。

AbortController 是 Web 平台提供的一个标准接口,用于 中止(取消)一个或多个异步操作 ,比如 fetch 请求、定时器、自定义任务等。它提供了一种统一、可组合的方式来处理取消逻辑,避免内存泄漏或无效操作。

核心概念AbortController :控制器对象,用于触发中止。AbortSignal :信号对象,与控制器关联,传递“是否已中止”的状态,并可监听 abort 事件。const controller = new AbortController;const signal = controller.signal;function myAsyncTask(signal) {return new Promise((resolve, reject) => {if (signal.aborted) {reject(new DOMException('操作已中止', 'AbortError'));return;}signal.addEventListener('abort', => { reject(new DOMException('操作已中止', 'AbortError')); });const timer = setTimeout( => { resolve('任务完成'); }, 3000);signal.addEventListener('abort', => { clearTimeout(timer); }); }); }myAsyncTask(controller.signal).then(console.log).catch(e => {if (e.name === 'AbortError') {console.log('任务被用户取消');} else {console.error('其他错误', e);}});setTimeout( => { controller.abort; }, 1000);const controller = new AbortController;fetch('/api/data', { signal: controller.signal }) .then(res => res.json) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.log('请求被取消'); } else { console.error('网络错误', err); } });controller.abort;

✅ 所有现代浏览器都支持 fetch 的 signal 选项。

一个 AbortController 可以控制多个异步任务:

const controller = new AbortController;fetch('/api/1', { signal: controller.signal }); fetch('/api/2', { signal: controller.signal }); myAsyncTask(controller.signal);controller.abort;

虽然 setTimeout 本身不支持 signal ,但可以手动集成:

function delay(ms, signal) {return new Promise((resolve, reject) => {if (signal?.aborted) {reject(new DOMException('已中止', 'AbortError'));return;}const id = setTimeout(resolve, ms); signal?.addEventListener('abort', => { clearTimeout(id); reject(new DOMException('已中止', 'AbortError')); }); }); }const ctrl = new AbortController; delay(5000, ctrl.signal).catch(console.error); ctrl.abort; 与 TaskController (来自 scheduler.postTask )的关系TaskController 是 AbortController 的 子类 ,专为调度任务设计。它额外支持 priority 设置,并返回 TaskSignal (继承自 AbortSignal )。因此, AbortController 是更通用的取消机制,而 TaskController 是其在任务调度场景下的扩展。const taskCtrl = new TaskController({ priority: 'background' });scheduler.postTask(myTask, { signal: taskCtrl.signal });taskCtrl.abort;⚠️ 注意事项abort 只能调用一次 ,多次调用无副作用。中止后, signal.aborted 永远为 true 。被中止的操作 不会自动停止 ,你需要在代码中 主动监听并清理资源 (如清除定时器、关闭流等)。不要重复使用同一个 AbortController 实例处理不相关的任务,建议按逻辑分组使用。

在 React 中,AbortController 是处理 组件卸载后仍可能完成的异步操作 (如 fetch 请求、定时器、动画等)的关键工具。它的主要目的是 避免“内存泄漏”或“状态更新已卸载组件” 的警告(例如经典的 Can't perform a React state update on an unmounted component )。

当组件在请求完成前被卸载(如用户快速切换路由),应取消请求。

import { useEffect, useState } from 'react';function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);useEffect( => { const controller = new AbortController;const fetchUser = async => { try { const res = await fetch(`/api/users/${userId}`, { signal: controller.signal }); const data = await res.json; setUser(data); } catch (err) { if (err.name !== 'AbortError') { console.error('请求失败:', err); } } finally { setLoading(false); } };fetchUser;return => { controller.abort; }; }, [userId]);if (loading) return 加载中...; return 用户名:{user?.name}; }

✅ 这样即使组件卸载,也不会尝试调用 setUser ,避免警告。

useEffect( => {const controller = new AbortController;Promise.all([ fetch('/api/posts', { signal: controller.signal }), fetch('/api/comments', { signal: controller.signal }) ]) .then .catch(err => { if (err.name !== 'AbortError') { } });return => controller.abort; }, );

可以创建一个可复用的 useAbortableFetch :

import { useEffect, useState } from 'react';export function useAbortableFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);useEffect( => { const controller = new AbortController;const fetchData = async => { try { const res = await fetch(url, { signal: controller.signal }); if (!res.ok) throw new Error('请求失败'); const json = await res.json; setData(json); } catch (err) { if (err.name !== 'AbortError') { setError(err); } } finally { setLoading(false); } };fetchData;return => controller.abort; }, [url]);return { data, loading, error }; }

使用:

function App {const { data, loading } = useAbortableFetch('/api/data');}

虽然 setTimeout 不原生支持 signal ,但可以手动集成:

useEffect( => {const controller = new AbortController;const timer = setTimeout( => { if (!controller.signal.aborted) { setData('更新了!'); } }, 3000);return => { controller.abort; clearTimeout(timer); }; }, );

或者封装一个支持 signal 的 delay 工具函数(见前文)。

⚠️ 注意事项不要忽略 AbortError
在 .catch 中要判断是否是 AbortError ,避免把“正常取消”当作错误处理。每个 effect 使用独立的 controller
避免多个 effect 共用同一个 AbortController ,除非你明确需要批量取消。不适用于同步操作
AbortController 只对异步、可中断的操作有效。React 18 严格模式下的双重调用
在开发模式下,React 18 的严格模式会故意 mount → unmount → remount 组件,此时 AbortController 能确保第一次请求被正确取消,是 正常行为 ,不是 bug。

来源:墨码行者

相关推荐