时间:2026-01-05 11:08
人气:
作者:admin
点赞 + 收藏 === 学会????????????
ResizeObserver 是一个浏览器原生的 JavaScript API,用于监听 DOM 元素尺寸的变化。它类似于 MutationObserver,但专门用于观察元素的大小(宽高)变化,而无需依赖 window.resize 事件(后者只对视口变化有效)。
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
console.log(`元素尺寸:${width} x ${height}`);
// entry.target 是被观察的 DOM 元素
console.log('目标元素:', entry.target);
}
});
// 开始观察某个元素
resizeObserver.observe(document.querySelector('#my-element'));
// 可选:观察多个元素
// resizeObserver.observe(element1);
// resizeObserver.observe(element2);
entry.contentRect:表示内容区域(不包括 padding、border、margin),类似于 getComputedStyle().width/height 的计算结果。entry.target.getBoundingClientRect() 使用。// 停止观察某个元素 resizeObserver.unobserve(element); // 停止观察所有元素并释放资源 resizeObserver.disconnect();
建议:在组件销毁(如 React 的
useEffect清理函数、Vue 的onBeforeUnmount)时调用disconnect(),避免内存泄漏。
window.onresize:更精确地响应特定元素的尺寸变化,而非整个窗口。兼容性已非常广泛,现代项目可放心使用。
可通过 GitHub - juggle/resize-observer 提供的 polyfill:
npm install @juggle/resize-observer
import ResizeObserver from '@juggle/resize-observer';
// 如果原生不支持,则使用 polyfill
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 <div ref={containerRef}>可变尺寸容器</div>;
}
IntersectionObserver 是一个强大的浏览器原生 API,用于异步监听目标元素与祖先元素(或视口)的交叉(相交)状态变化。它常用于实现懒加载、无限滚动、曝光统计、动画触发等场景,性能远优于传统的 scroll 事件监听。
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// entry.target:被观察的 DOM 元素
// entry.isIntersecting:是否与根(viewport 或 root)相交
// entry.intersectionRatio:相交区域占目标元素的比例(0 ~ 1)
// entry.intersectionRect:相交区域的矩形信息
// entry.boundingClientRect:目标元素相对于视口的位置
// entry.rootBounds:根元素的边界(通常是视口)
if (entry.isIntersecting) {
console.log('元素进入视口:', entry.target);
// 例如:加载图片、触发动画
} else {
console.log('元素离开视口');
}
});
});
// 开始观察某个元素
observer.observe(document.querySelector('#my-element'));
const options = {
root: null, // 默认为视口(viewport);可设为某个祖先元素
rootMargin: '0px', // 类似 CSS margin,扩展或收缩根的边界(支持负值)
threshold: 0.5 // 触发回调的相交比例阈值(0 ~ 1),可为数字或数组
};
const observer = new IntersectionObserver(callback, options);
threshold 示例:threshold: 0:只要有一点进入就触发(默认)。threshold: 1:完全进入才触发。threshold: [0, 0.25, 0.5, 0.75, 1]:在 0%、25%、50%... 时都触发。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; // 从 data-src 加载真实图片
imgObserver.unobserve(img); // 加载后停止观察
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imgObserver.observe(img);
});
观察一个“哨兵”元素(如分页加载提示),当它进入视口时触发加载。
当广告或内容区域进入视口一定比例时,上报“曝光”事件。
元素进入视口时添加 CSS 动画类。
现代浏览器支持良好,移动端也广泛可用。
官方推荐 polyfill(由 W3C 团队维护):
npm install intersection-observer
// 在应用入口引入(自动填充 window.IntersectionObserver) import 'intersection-observer';
注意:polyfill 会回退到
scroll+getBoundingClientRect(),性能较差,仅用于兼容。
ResizeObserver / MutationObserver 对比
三者互补,常结合使用。
rootMargin: '100px' 可以提前触发(在元素距离视口还有 100px 时就加载)。<img loading="lazy"> 普及的今天,简单图片懒加载可直接用 HTML 属性,但复杂逻辑仍需 IntersectionObserver。Page Visibility API 是一个浏览器原生 API,用于检测当前网页是否对用户可见(即是否处于前台标签页或被最小化/切换到后台)。它可以帮助开发者优化性能、节省资源,或实现特定业务逻辑(如暂停视频、停止轮询、统计停留时长等)。
document.visibilityState返回当前页面的可见性状态,可能值包括:

实际开发中主要关注
'visible'和'hidden'。
document.hidden(已废弃,建议用 visibilityState)true:页面不可见false:页面可见⚠️ 虽仍可用,但 MDN 建议使用
visibilityState。
visibilitychange 事件当页面可见性状态改变时触发。
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)。focus/blur 更精准判断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)。
现代浏览器(Chrome 89+ 等)支持分享文件(如图片、PDF):
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
title: '图片分享',
files: [file] // File 对象数组
});
}
注意:文件必须来自用户选择(如
<input type="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 (
<button onClick={handleShare}>
分享
</button>
);
}
Wake Lock API 是一个现代 Web API,允许网页防止设备进入休眠状态(如屏幕变暗、锁屏),常用于需要长时间保持活跃的场景,例如:
screen)// 1. Screen Wake Lock(屏幕唤醒锁) ← 当前唯一广泛支持的类型 // 2. System Wake Lock(系统唤醒锁) ← 尚未标准化,基本不可用
目前 只有
screen类型 在主流浏览器中可用。
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);
⚠️ 必须由用户手势触发(如
click),不能在页面加载时自动请求。
if (wakeLock) {
await wakeLock.release(); // 显式释放
wakeLock = null;
}
锁会在以下情况自动释放:
- 页面进入后台(
visibilitychange→hidden)- 浏览器标签页关闭
- 用户手动锁屏
- 页面失去焦点(部分浏览器)

移动端 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):
requestFullscreen())有时可延长屏幕活跃时间(非可靠)<video playsinline webkit-playsinline> 等属性优化体验if ('wakeLock' in navigator) {
// 支持 Wake Lock API
}
BroadcastChannel 是一个现代 Web API,允许同源(same-origin)的不同浏览器上下文(如多个标签页、iframe、Web Worker)之间进行简单、高效的跨文档通信。 它类似于“发布-订阅”模式:一个上下文发送消息,所有监听同一频道的其他上下文都能收到。
// 所有页面/worker 使用相同的频道名
const channel = new BroadcastChannel('my-app-channel');
// 监听来自其他上下文的消息
channel.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
// 或使用 onmessage
// channel.onmessage = (event) => { ... };
// 任意同源页面或 worker 中
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 广播消息。

⚠️ Safari 在 15.4 之前完全不支持,如需兼容旧版 iOS,需使用
localStorage+storage事件作为 fallback。
利用 localStorage 的 storage 事件实现类似广播:
// 发送消息(fallback)
function broadcastFallback(message) {
localStorage.setItem('broadcast-msg', JSON.stringify({
...message,
timestamp: Date.now()
}));
}
// 接收消息(其他标签页会触发 storage 事件)
window.addEventListener('storage', (e) => {
if (e.key === 'broadcast-msg') {
const message = JSON.parse(e.newValue);
console.log('Fallback 收到:', message);
}
});
缺点:只能传递字符串,且
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 <button onClick={() => changeTheme('dark')}>切换深色</button>;
}
BroadcastChannel和 Vuex / Redux

用 BroadcastChannel:
channel.postMessage({ type: 'LOGIN', user }) 广播。用 Vuex:
store.state.user。✅ 所以:Vuex 管“页面内”,BroadcastChannel 管“页面间” 。
实际项目中,两者常配合使用:
// 在 Vuex 的 action 中监听 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-shared-mutations:通过 localStorage 或 BroadcastChannel 同步 Vuex 的 mutations。storage 事件或 BroadcastChannel,触发本地 store 更新。但核心思想不变:跨标签页通信靠 BroadcastChannel(或 storage),状态管理靠 Vuex。

PerformanceObserver 是一个强大的 Web API,用于异步监听性能相关的事件和指标,而无需轮询 performance.getEntries()。它是现代 Web 性能监控(如 Core Web Vitals)的核心工具。
监听浏览器自动记录的 Performance Timeline(性能时间线) 中的新条目,例如:
resource)navigation)longtask)element,实验性)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),否则不会触发回调。
entryTypes 及用途
✅ 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):
if (PerformanceObserver.supportedEntryTypes.includes('longtask')) {
// 启用观察
}
resource, navigation),Web Vitals 支持较弱推荐使用 Google 的
web-vitals库 跨浏览器采集 Core Web Vitals。
performance.getEntries() 对比
✅ 现代性能监控应优先使用
PerformanceObserver。
<head> 中或页面顶部初始化,避免漏掉早期指标。buffered: true:确保捕获 FCP、LCP、CLS 等可能在监听前已发生的指标。disconnect(),因为性能条目是一次性的。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-vitals
useWebVitals// hooks/useWebVitals.ts
import { 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<string, unknown>;
};
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);
}
};
// 启动监听(Web Vitals 内部使用 PerformanceObserver)
getCLS(report, reportAll);
getFCP(report, reportAll);
getLCP(report, reportAll);
getFID(report); // FID 只触发一次
getINP(report, reportAll); // INP 替代 FID(未来标准)
// 注意:web-vitals 的指标是自动管理生命周期的,无需 cleanup
}, [onReport, reportAll]);
};
// App.tsx
import { useWebVitals } from './hooks/useWebVitals';
function App() {
useWebVitals({
onReport: (metric) => {
// 上报到分析平台(如 GA4、Sentry、自建 API)
fetch('/api/performance', {
method: 'POST',
body: JSON.stringify(metric),
headers: { 'Content-Type': 'application/json' }
});
}
});
return <div>你的应用</div>;
}
✅ 优点:自动处理浏览器兼容性、只上报有效指标、支持开发环境日志。
useWebVitals// composables/useWebVitals.ts
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<string, unknown>;
};
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);
});
}
<!-- App.vue -->
<script setup>
import { useWebVitals } from './composables/useWebVitals';
useWebVitals((metric) => {
fetch('/api/performance', {
method: 'POST',
body: JSON.stringify(metric),
headers: { 'Content-Type': 'application/json' }
});
});
</script>
<template>
<div>你的应用</div>
</template>
如果你还想监控 JS/CSS/图片等资源加载性能,可以额外封装一个 Hook:
useResourcePerformance// hooks/useResourcePerformance.ts
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]);
};
ue 版本类似,用 onMounted + onUnmounted 管理生命周期。
reportAll: false)。web-vitals 是异步非阻塞的)。requestIdleCallback 是一个浏览器提供的 API,用于在浏览器主线程空闲时执行低优先级任务,避免影响关键操作(如用户输入、动画、布局等),从而提升页面流畅性和响应性。
???? 它是实现“协作式调度(Cooperative Scheduling) ”的关键工具,React 16+ 的 Fiber 架构就受其启发(尽管 React 最终未直接使用它)。
function doLowPriorityWork(deadline) {
// deadline.timeRemaining():返回当前空闲时段还剩多少毫秒(通常 < 50ms)
// deadline.didTimeout:是否因超时而强制执行(配合 timeout 使用)
while (deadline.timeRemaining() > 0 || deadline.didTimeout) {
if (hasWork()) {
performUnitOfWork();
} else {
break; // 没有更多工作,退出
}
}
// 如果还有剩余任务,继续调度
if (hasMoreWork()) {
requestIdleCallback(doLowPriorityWork);
}
}
// 启动任务
requestIdleCallback(doLowPriorityWork, { timeout: 2000 });
deadlinedeadline.timeRemaining():返回一个估算值(单位:毫秒),表示当前帧剩余的空闲时间(通常 ≤ 50ms)。deadline.didTimeout:如果设置了 timeout 且超时,则为 true,此时应尽快完成任务。{
timeout: 2000 // 最大等待时间(毫秒)。超时后即使没有空闲也会执行回调。
}
⚠️
timeout会降低优先级优势,仅用于“最终必须执行”的兜底场景。
requestIdleCallback(() => {
// 预加载下一页数据、图片、代码分割 chunk
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/Edge 支持,Firefox 和 Safari 永远不会支持!
可通过 caniuse.com/requestidle… 查看。
由于兼容性差,生产环境必须提供 fallback。
setTimeout 模拟(简单但不精确)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);
};
requestAnimationFrame + 时间切片(更接近原生行为)适用于需要精细控制的任务调度(如 React Fiber 的思路)。
setTimeout(fn, 0) 或 queueMicrotask适用于非关键但需异步执行的任务,但无法利用“空闲时间”。
timeRemaining() 返回较大值,也应分片处理。timeRemaining() 是估算值,可能突然变为 0。requestAnimationFrame 对比
✅ 两者互补:
rAF保证流畅动画,rIC避免阻塞动画。
rIC 启发,但使用自定义实现(因兼容性问题)。function scheduleIdleWork(workFn, timeout = 2000) {
if ('requestIdleCallback' in window) {
return requestIdleCallback((deadline) => {
if (deadline.timeRemaining() > 0 || deadline.didTimeout) {
workFn();
}
}, { timeout });
} else {
// fallback: 稍后执行(不阻塞当前任务)
return setTimeout(workFn, 0);
}
}
// 使用
const id = scheduleIdleWork(() => {
console.log('在空闲时执行');
});
// 取消(如组件卸载时)
// cancelIdleCallback(id) 或 clearTimeout(id)
作用:在浏览器空闲时执行低优先级任务,提升用户体验。
现状:仅 Chrome/Edge 支持,Firefox/Safari 已放弃。
建议:
AbortController 是 Web 平台提供的一个标准接口,用于中止(取消)一个或多个异步操作,比如 fetch() 请求、定时器、自定义任务等。它提供了一种统一、可组合的方式来处理取消逻辑,避免内存泄漏或无效操作。
AbortController:控制器对象,用于触发中止。AbortSignal:信号对象,与控制器关联,传递“是否已中止”的状态,并可监听 abort 事件。const controller = new AbortController(); const signal = controller.signal; // 只读的 AbortSignal
// 示例:自定义异步任务
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);
}
});
// 1 秒后取消
setTimeout(() => {
controller.abort(); // 触发 abort 事件,signal.aborted 变为 true
}, 1000);
fetch 请求(最常见)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 / setInterval 结合虽然 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 是其在任务调度场景下的扩展。// TaskController 用法(实验性)
const taskCtrl = new TaskController({ priority: 'background' });
scheduler.postTask(myTask, { signal: taskCtrl.signal });
// 也可以直接 abort()
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 // 传入 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 <div>加载中...</div>;
return <div>用户名:{user?.name}</div>;
}
✅ 这样即使组件卸载,也不会尝试调用
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:
// hooks/useAbortableFetch.js
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 工具函数(见前文)。
在路由切换时自动取消请求:
// 不需要额外操作!只要在 useEffect 中正确使用 AbortController, // 路由切换导致组件卸载时,清理函数会自动执行。
AbortError.catch() 中要判断是否是 AbortError,避免把“正常取消”当作错误处理。AbortController,除非你明确需要批量取消。AbortController 只对异步、可中断的操作有效。AbortController 能确保第一次请求被正确取消,是正常行为,不是 bug。AbortController。AbortController 是其实现细节之一。
