BOM 和 DOM✅
DOM0 到 DOM3 区别?
答案
级别 | 标准化 | 主要内容与特性 | 事件处理方式 | 事件类型扩展 |
---|---|---|---|---|
DOM0 | 否 | 早期浏览器私有API,未标准化,仅支持基本DOM操作 | 直接赋值事件属性(如onclick) | 少量,类型有限 |
DOM1 | 是 | W3C首个标准,定义DOM核心(XML结构)和DOM HTML模块,支持基本节点操作 | 同DOM0 | 基本事件 |
DOM2 | 是 | 扩展节点API,新增视图、事件、遍历、样式等模块,首次引入标准事件模型 | addEventListener/removeEventListener,支持捕获/冒泡 | 类型增多,支持多监听 |
DOM3 | 是 | 增加XPath、加载/保存、验证等模块,事件类型更丰富,支持自定义事件 | 同DOM2 | UI、键盘、合成等更多 |
事件模型与事件流
- DOM2/3 标准事件模型支持捕获、目标、冒泡三阶段,事件流顺序为:捕获 → 目标 → 冒泡。
- 事件捕获:事件从最外层向目标元素传递。
- 事件冒泡:事件从目标元素向外层父节点传递。
Event对象常见应用
- 获取事件目标(event.target)
- 阻止默认行为(event.preventDefault)
- 阻止冒泡(event.stopPropagation)
自动化事件与遍历函数实现
// 遍历DOM树并对每个节点执行回调
function Traverse (parentElement, visit_callback) {
visit_callback(parentElement)
for (const child of parentElement.children) {
Traverse(child, visit_callback)
}
}
DOM 统计和遍历示例
复杂DOM交互示例
延伸阅读
offsetHeight
、clientHeight
、scrollHeight
有什么区别
答案
- offsetHeight 为边框盒高度
- clientHeight 为 padding 盒高度
- scrollHeight
- 元素的子元素高度不足时为元素高度
- 元素的子元素高度总和+元素的padding+元素的border
dom.contains
答案
在 DOM(文档对象模型)中,要判断元素 a
是否是元素 b
的子元素,您可以使用以下的 JavaScript 代码:
function isChildElement (a, b) {
return b.contains(a)
}
可以这样使用上述函数:
const elementA = document.getElementById('elementA')
const elementB = document.getElementById('elementB')
if (isChildElement(elementA, elementB)) {
console.log('元素 A 是元素 B 的子元素')
} else {
console.log('元素 A 不是元素 B 的子元素')
}
如何优化大规模 dom 操作的场景
答案
核心概念:
大规模DOM操作的性能优化核心在于减少重排重绘、批量操作、异步处理和合理的缓存策略。主要优化手段包括:使用DocumentFragment进行离线操作、虚拟滚动技术、Web Worker处理计算密集任务、requestAnimationFrame分片处理等。
实际示例:
// 1. 使用DocumentFragment批量操作
function batchDOMOperations(data) {
const fragment = document.createDocumentFragment();
data.forEach(item => {
const div = document.createElement('div');
div.textContent = item.text;
div.className = item.className;
fragment.appendChild(div);
});
// 一次性插入,只触发一次重排
document.getElementById('container').appendChild(fragment);
}
// 2. 时间分片处理大量数据
function timeSlicedRender(data, batchSize = 100) {
let index = 0;
function renderBatch() {
const start = performance.now();
while (index < data.length && performance.now() - start < 16) {
renderItem(data[index]);
index++;
}
if (index < data.length) {
requestAnimationFrame(renderBatch);
}
}
requestAnimationFrame(renderBatch);
}
// 3. 虚拟滚动实现
class VirtualScroller {
constructor(container, itemHeight, totalItems) {
this.container = container;
this.itemHeight = itemHeight;
this.totalItems = totalItems;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2;
this.scrollTop = 0;
this.init();
}
init() {
// 设置总高度
this.container.style.height = `${this.totalItems * this.itemHeight}px`;
// 监听滚动
this.container.addEventListener('scroll', this.onScroll.bind(this));
this.render();
}
onScroll() {
this.scrollTop = this.container.scrollTop;
this.render();
}
render() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = Math.min(startIndex + this.visibleItems, this.totalItems);
// 清空现有内容
this.container.innerHTML = '';
// 创建可见项
for (let i = startIndex; i < endIndex; i++) {
const item = document.createElement('div');
item.style.position = 'absolute';
item.style.top = `${i * this.itemHeight}px`;
item.style.height = `${this.itemHeight}px`;
item.textContent = `Item ${i}`;
this.container.appendChild(item);
}
}
}
// 4. 避免强制同步布局
function badExample() {
const elements = document.querySelectorAll('.item');
// 错误:每次循环都会触发重排
elements.forEach(el => {
el.style.width = '100px';
console.log(el.offsetWidth); // 强制同步布局
});
}
function goodExample() {
const elements = document.querySelectorAll('.item');
// 正确:先读取所有布局信息
const widths = Array.from(elements).map(el => el.offsetWidth);
// 再批量修改样式
elements.forEach((el, index) => {
el.style.width = `${widths[index] + 10}px`;
});
}
// 5. 使用 CSS 变换代替位置修改
function optimizedAnimation(element) {
// 好:使用 transform,不会触发重排
element.style.transform = 'translateX(100px)';
// 差:修改 left 会触发重排
// element.style.left = '100px';
}
// 6. 使用事件委托
function setupEventDelegation() {
const container = document.getElementById('list-container');
// 在父容器上绑定一个事件监听器
container.addEventListener('click', (e) => {
if (e.target.matches('.list-item')) {
handleItemClick(e.target);
}
});
}
面试官视角:
要点清单:
- 理解重排重绘的原理和触发条件
- 掌握批量操作的实现方式(DocumentFragment)
- 了解时间分片和虚拟滚动技术
加分项:
- 能够设计虚拟滚动解决方案
- 熟悉浏览器渲染流水线
- 掌握性能监控和优化工具的使用
常见失误:
- 在循环中进行DOM查询和修改
- 混合读写操作导致强制同步布局
- 忽视事件监听器的性能影响
延伸阅读:
- Rendering Performance - Google Developers — 渲染性能优化指南
- DocumentFragment - MDN — 文档片段API
- Intersection Observer - MDN — 交叉观察器API
scrollIntoView api
答案
核心概念:
scrollIntoView
是 DOM 元素的内置方法,能将元素平滑滚动到浏览器可视区域内。该方法支持多种对齐方式和滚动行为配置,常用于表单验证错误定位、通知提示展示、页面导航等场景。
实际示例:
// 基本用法
document.getElementById('target').scrollIntoView()
// 平滑滚动到元素顶部
document.getElementById('target').scrollIntoView({
behavior: 'smooth', // 平滑滚动
block: 'start', // 元素顶部对齐到视口顶部
inline: 'nearest' // 水平方向最近对齐
})
// 表单验证错误定位示例
function scrollToFirstError (formId) {
const form = document.getElementById(formId)
const firstError = form.querySelector('.error, [aria-invalid="true"]')
if (firstError) {
firstError.scrollIntoView({
behavior: 'smooth',
block: 'center', // 元素居中显示
inline: 'center'
})
// 可选:添加视觉提示
firstError.focus()
}
}
// 配置选项详解
const options = {
behavior: 'smooth', // 'auto' | 'smooth'
block: 'start', // 'start' | 'center' | 'end' | 'nearest'
inline: 'nearest' // 'start' | 'center' | 'end' | 'nearest'
}
面试官视角:
要点清单:
- 了解scrollIntoView的基本用法和配置选项
- 知道behavior、block、inline参数的作用
- 理解在表单验证中的实际应用场景
加分项:
- 提及与用户体验相关的滚动优化
- 了解浏览器兼容性和性能考虑
- 知道如何结合focus()增强无障碍体验
常见失误:
- 忽略平滑滚动可能被用户设置覆盖
- 不考虑固定导航栏对滚动位置的影响
- 过度使用可能造成用户困扰
延伸阅读:
- Element.scrollIntoView() - MDN — 完整API文档
- Scroll behavior CSS property — CSS滚动行为控制
- WCAG 2.1 - Focus management — 无障碍焦点管理
documentFragment api 是什么, 有哪些使用场景
答案
核心概念:
DocumentFragment
是 DOM 中的轻量级容器节点,用作临时存储和批量操作 DOM 元素的"虚拟容器"。它不是真实 DOM 树的一部分,不会引起页面重排和重绘,直到被插入到实际 DOM 中。主要用于性能优化,避免频繁的 DOM 操作造成的性能损耗。
实际示例:
// 基本用法:批量创建和插入元素
function createListItems (items) {
const fragment = document.createDocumentFragment()
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item
li.className = 'list-item'
fragment.appendChild(li)
})
// 一次性插入所有元素,只触发一次重排
document.getElementById('list').appendChild(fragment)
}
// 性能对比示例
function inefficientWay (items) {
const list = document.getElementById('list')
// 每次 appendChild 都会触发重排 - 性能差
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item
list.appendChild(li) // 多次重排
})
}
function efficientWay (items) {
const fragment = document.createDocumentFragment()
const list = document.getElementById('list')
// 在 fragment 中构建完整结构
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item
fragment.appendChild(li)
})
// 一次性插入,只触发一次重排
list.appendChild(fragment)
}
// 模板克隆场景
function cloneTemplate (templateId, data) {
const template = document.getElementById(templateId)
const fragment = document.createDocumentFragment()
data.forEach(item => {
const clone = template.content.cloneNode(true)
// 在 fragment 中修改克隆内容
clone.querySelector('.title').textContent = item.title
clone.querySelector('.desc').textContent = item.desc
fragment.appendChild(clone)
})
return fragment
}
面试官视角:
要点清单:
- 理解DocumentFragment的"虚拟容器"概念
- 知道它如何避免频繁DOM操作的性能问题
- 了解批量操作的典型使用场景
加分项:
- 提及与template元素结合使用
- 了解现代框架中的虚拟DOM概念联系
- 知道何时使用DocumentFragment vs 字符串拼接
常见失误:
- 认为DocumentFragment本身就能提升所有DOM性能
- 不了解插入后DocumentFragment会被自动清空
- 过度使用导致代码复杂化
延伸阅读:
- DocumentFragment - MDN — 完整API文档
- DOM performance best practices — DOM性能优化指南
- Template element and DocumentFragment — 模板与片段结合使用
getComputedStyle用法?
答案
核心概念
getComputedStyle
是浏览器 BOM(浏览器对象模型)中 window
对象的方法,用于获取元素应用后的最终样式(即所有 CSS 规则和继承、计算后的结果),返回一个只读的 CSSStyleDeclaration
对象。
详细解释
- 语法:
window.getComputedStyle(element, pseudoElement?)
element
:要获取样式的 DOM 元素。pseudoElement
:可选,伪元素字符串(如::before
),通常为null
。
- 返回值:包含所有计算后样式的对象,属性名为驼峰式(如
backgroundColor
)。 - 常见用途:动态获取元素的实际样式(如宽高、颜色),适用于内联样式、外部样式、继承样式等。
代码示例
// 获取元素的实际颜色
const el = document.getElementById('demo')
const style = window.getComputedStyle(el)
console.log(style.color) // 输出如 rgb(0, 0, 0)
// 获取伪元素样式
const beforeStyle = window.getComputedStyle(el, '::before')
console.log(beforeStyle.content)
常见误区
- 不能直接修改
getComputedStyle
返回的对象,只读。 - 获取的值均为计算后值,单位通常为像素(如
width: "100px"
),部分属性可能返回关键字(如auto
)。 - 与
element.style
区别:element.style
只读内联样式,getComputedStyle
读所有最终样式。
延伸阅读
- MDN: getComputedStyle - 官方文档与示例
- CSSStyleDeclaration - 计算样式对象说明
History API ?
答案
核心概念
History API 是 HTML5 提供的浏览器历史记录管理接口,允许开发者通过 JavaScript 动态修改 URL 和历史记录,而无需页面刷新。常用方法包括 pushState
、replaceState
、go
、back
、forward
,配合 popstate
事件实现前端路由。
详细解释
history.pushState(state, title, url)
:添加一条历史记录并修改 URL,不刷新页面。history.replaceState(state, title, url)
:替换当前历史记录。history.go(n)
、back()
、forward()
:前进/后退历史记录。window.onpopstate
:监听历史记录变化,常用于响应前进/后退操作。
前端路由利用 History API 实现无刷新页面切换,仅更新部分内容,提升用户体验和性能。切换时可保留页面状态,便于返回时恢复。
代码示例
// 添加历史记录并切换路由
history.pushState({ page: 1 }, '', '/page1')
// 替换当前历史记录
history.replaceState({ page: 2 }, '', '/page2')
// 监听路由变化
window.addEventListener('popstate', e => {
// 根据 e.state 和 location.pathname 渲染页面
})
常见误区
- 仅修改 URL 不会自动渲染页面内容,需手动处理视图更新。
- 直接访问新 URL 或刷新页面时,需服务器返回入口文件(如 index.html),否则会 404。
pushState
/replaceState
不会触发popstate
,需主动调用渲染逻辑。
使用 History API 做前端路由时,务必配置服务器 fallback 规则,避免刷新页面 404。
延伸阅读
原生 js 如何进行监听路由的变化
答案
原生 JS 监听路由变化主要依赖 window
对象的 popstate
事件。该事件会在浏览器历史记录发生变化时触发,包括用户点击前进/后退按钮、或通过 history.pushState()
、history.replaceState()
修改 URL 时。通过监听此事件,可以实现前端路由的页面切换和视图更新。
详细解释
popstate
事件不会在页面首次加载时触发,仅在历史记录切换时触发。- 通过
history.pushState(state, title, url)
或history.replaceState(state, title, url)
可手动变更 URL 并添加/替换历史记录,但不会自动触发popstate
,需手动调用处理逻辑。 event.state
或history.state
可获取当前历史记录的状态对象,便于在路由切换时传递和获取数据。
代码示例
// 路由变化监听
function onRouteChange (event) {
console.log('路由发生了变化', history.state)
// 这里可根据新 URL 渲染页面内容
}
window.addEventListener('popstate', onRouteChange)
// 主动变更路由
function goTo (path, state = {}) {
history.pushState(state, '', path)
// 手动触发处理逻辑
onRouteChange()
}
// 初始化时执行一次
onRouteChange()
常见误区
- 仅通过
pushState
/replaceState
修改 URL 不会自动触发popstate
,需手动处理视图更新。 popstate
只响应历史记录切换,不响应 hash 变化(hash 变化用hashchange
事件)。- 需确保服务器配置支持前端路由,避免刷新页面 404。
单页应用(SPA)推荐统一封装路由切换和监听逻辑,保证页面状态与 URL 同步。
延伸阅读
escape、encodeURI、encodeURIComponent 区别
答案
核心概念:
这三个函数都用于URL编码,但编码范围和使用场景不同。escape
已废弃,encodeURI
适合编码完整URL,encodeURIComponent
适合编码URL参数。
详细对比:
函数 | 主要用途 | 编码范围 | 空格编码 | 是否编码保留字符 | 是否推荐 | 典型场景 |
---|---|---|---|---|---|---|
escape | 编码字符串 | ASCII外及部分特殊字符 | + | 否 | 否(已废弃) | 早期通用编码(不建议用) |
encodeURI | 编码完整URL | 除保留字符外的所有字符 | 不编码 | 否 | 是 | 整个URL编码 |
encodeURIComponent | 编码URL片段/参数值 | 所有非字母数字字符 | %20 | 是 | 是 | URL参数、片段编码 |
实际示例:
const url = 'https://example.com/search?q=hello world&type=news'
const param = 'hello world & special chars'
// encodeURI - 编码整个URL
console.log(encodeURI(url))
// "https://example.com/search?q=hello%20world&type=news"
// encodeURIComponent - 编码URL参数
console.log(encodeURIComponent(param))
// "hello%20world%20%26%20special%20chars"
// 实际应用:构建带参数的URL
const baseUrl = 'https://api.example.com/search'
const query = 'hello world'
const category = 'news & events'
const fullUrl = `${baseUrl}?q=${encodeURIComponent(query)}&cat=${encodeURIComponent(category)}`
console.log(fullUrl)
// "https://api.example.com/search?q=hello%20world&cat=news%20%26%20events"
// escape(已废弃,仅作对比)
console.log(escape(param)) // "hello%20world%20%26%20special%20chars"
面试官视角:
此题考查对URL编码的理解和实际应用能力。重点关注候选人是否清楚不同函数的适用场景,以及在构建URL时如何正确选择编码方法。
常见误区:
- 对整个URL使用
encodeURIComponent
会破坏URL结构 - 对URL参数使用
encodeURI
可能导致特殊字符未被正确编码 - 仍然使用已废弃的
escape
函数
延伸阅读:
- MDN: encodeURI - 完整URL编码
- MDN: encodeURIComponent - URL组件编码
- RFC 3986 - URI规范标准
URLSearchParams API
答案
URLSearchParams 是 HTML5 提供的一个 API,用于处理 URL 查询参数。它提供了简单易用的方法来获取、设置、删除和遍历 URL 中的查询字符串参数,避免手动解析字符串的繁琐。
方法 | 作用 | 示例代码 | 说明 |
---|---|---|---|
get | 获取参数值 | urlParams.get('key') | 无则返回null |
set | 设置/更新参数值 | urlParams.set('key','v') | 无则新增 |
append | 追加参数值 | urlParams.append('key','v2') | 同名参数可多值 |
delete | 删除参数 | urlParams.delete('key') | |
has | 判断参数是否存在 | urlParams.has('key') | 返回布尔值 |
forEach | 遍历所有参数 | urlParams.forEach((v,k)=>{...}) | 回调函数处理每个参数 |
补充说明
- 支持从字符串或
window.location.search
创建,便于解析和构建查询参数。 - 可与
URL
对象结合动态操作 URL 查询参数,适合 SPA 场景。 - 仅支持字符串类型,复杂对象需手动序列化。
const params = new URLSearchParams('a=1&b=2')
params.set('c', '3')
console.log(params.toString()) // a=1&b=2&c=3
推荐用 URLSearchParams
替代手动拼接/解析查询字符串,提升代码可读性和健壮性。
延伸阅读
requestIdleCallback api
答案
核心概念:
requestIdleCallback
是一个 Web API,允许开发者在浏览器主线程空闲时执行低优先级任务。它可以提高页面响应性和整体性能,特别适合执行分析、数据整理等不紧急的后台任务。
适用场景:
- 清理工作 - 如删除标记的DOM节点、同步本地存储数据
- 非关键解析 - 如解析大量数据、生成报告
- 状态更新 - 如发送非紧急的状态变更、日志上报
实际示例:
// 基本用法
function performNonCriticalWork (deadline) {
// 检查是否还有剩余时间
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && hasWork()) {
doWorkItem()
}
// 如果还有未完成的工作,请求下一个空闲周期
if (hasWork()) {
requestIdleCallback(performNonCriticalWork)
}
}
// 启动空闲时执行的任务,最多等待5秒
requestIdleCallback(performNonCriticalWork, { timeout: 5000 })
// 实际应用示例:延迟加载图片
function lazyLoadImages (deadline) {
const images = document.querySelectorAll('img[data-src]')
let index = 0
while (deadline.timeRemaining() > 0 && index < images.length) {
const img = images[index]
img.src = img.dataset.src
img.removeAttribute('data-src')
index++
}
if (index < images.length) {
requestIdleCallback(lazyLoadImages)
}
}
requestIdleCallback(lazyLoadImages)
关键参数说明:
- deadline.timeRemaining() - 返回当前空闲期剩余时间(毫秒)
- deadline.didTimeout - 布尔值,指示是否因超时被强制执行
- timeout - 可选参数,指定最大等待时间
面试官视角:
此API考查对浏览器性能优化的理解。优秀回答应涵盖:空闲时间概念、任务调度原理、与setTimeout的区别。注意候选人是否了解兼容性问题(Safari不支持)以及polyfill方案。
常见误区:
- 在空闲回调中执行DOM操作或样式计算(会强制重排)
- 不检查
timeRemaining()
就执行长时间任务 - 混淆
requestIdleCallback
和requestAnimationFrame
的使用场景
延伸阅读:
- MDN: Background Tasks API - 后台任务API详解
- Google: requestIdleCallback - 使用指南和最佳实践
- Can I use: requestIdleCallback - 浏览器兼容性
如何判断页签是否为活跃状态
答案
判断当前浏览器页签(Tab)是否为活跃状态,常用 Page Visibility API。该 API 能检测页面是否处于用户可见状态,适用于暂停动画、停止轮询等场景。
Page Visibility API 通过 document.visibilityState
属性和 visibilitychange
事件,判断页面是“可见”还是“隐藏”。常见状态有 visible
(活跃/可见)、hidden
(非活跃/不可见)。当用户切换标签页、最小化窗口或切换应用时,状态会自动变化。
属性/事件 | 作用 | 常见值/说明 |
---|---|---|
document.visibilityState | 页面可见性 | visible/hidden |
visibilitychange | 可见性变化事件 | 监听页面状态切换 |
// 判断当前页签是否活跃
function isTabActive () {
return document.visibilityState === 'visible'
}
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (isTabActive()) {
console.log('页面处于活跃状态')
} else {
console.log('页面已隐藏')
}
})
延伸阅读
- MDN: Page Visibility API - 官方文档与用法说明
- 前端性能优化:合理利用 Page Visibility - 实战与应用场景
移动端如何实现上拉加载,下拉刷新?
答案
核心概念:
移动端上拉加载和下拉刷新是常见的交互模式,核心实现原理是通过监听触摸事件和滚动事件来检测用户手势。主要技术点包括:触摸事件处理(touchstart/touchmove/touchend)、距离计算、状态管理、动画效果、防抖处理等。
实际示例:
// 现代化实现:支持原生下拉刷新和自定义上拉加载
class PullRefreshComponent {
constructor(container, options = {}) {
this.container = container;
this.options = {
pullDistance: 100, // 下拉触发距离
loadThreshold: 50, // 上拉加载阈值
damping: 0.5, // 阻尼系数
animationDuration: 300, // 动画时长
onRefresh: null, // 刷新回调
onLoad: null, // 加载回调
...options
};
this.state = {
isRefreshing: false,
isLoading: false,
startY: 0,
currentY: 0,
pullDistance: 0
};
this.init();
}
init() {
this.createUI();
this.bindEvents();
}
createUI() {
// 创建下拉刷新UI
this.refreshEl = document.createElement('div');
this.refreshEl.className = 'pull-refresh-indicator';
this.refreshEl.innerHTML = `
<div class="refresh-content">
<div class="refresh-icon">↓</div>
<div class="refresh-text">下拉刷新</div>
</div>
`;
// 创建上拉加载UI
this.loadEl = document.createElement('div');
this.loadEl.className = 'pull-load-indicator';
this.loadEl.innerHTML = `
<div class="load-content">
<div class="load-text">上拉加载更多</div>
</div>
`;
// 插入DOM
this.container.insertBefore(this.refreshEl, this.container.firstChild);
this.container.appendChild(this.loadEl);
// 设置样式
this.setStyles();
}
setStyles() {
const styles = `
.pull-refresh-indicator {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
transform: translateY(-60px);
transition: transform 0.3s ease;
background: #f8f8f8;
}
.refresh-content {
display: flex;
align-items: center;
gap: 8px;
}
.refresh-icon {
font-size: 18px;
transition: transform 0.3s ease;
}
.pull-load-indicator {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f8f8;
opacity: 0;
transition: opacity 0.3s ease;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = styles;
document.head.appendChild(styleEl);
}
bindEvents() {
this.container.addEventListener('touchstart', this.onTouchStart.bind(this));
this.container.addEventListener('touchmove', this.onTouchMove.bind(this));
this.container.addEventListener('touchend', this.onTouchEnd.bind(this));
this.container.addEventListener('scroll', this.onScroll.bind(this));
}
onTouchStart(e) {
this.state.startY = e.touches[0].pageY;
this.state.currentY = this.state.startY;
}
onTouchMove(e) {
if (this.state.isRefreshing || this.state.isLoading) return;
this.state.currentY = e.touches[0].pageY;
const deltaY = this.state.currentY - this.state.startY;
// 下拉刷新逻辑
if (deltaY > 0 && this.container.scrollTop === 0) {
e.preventDefault();
// 应用阻尼效果
this.state.pullDistance = deltaY * this.options.damping;
this.updateRefreshUI();
}
}
onTouchEnd(e) {
if (this.state.pullDistance > this.options.pullDistance && !this.state.isRefreshing) {
this.triggerRefresh();
} else {
this.resetRefreshUI();
}
this.state.pullDistance = 0;
}
onScroll() {
if (this.state.isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = this.container;
// 上拉加载逻辑
if (scrollTop + clientHeight >= scrollHeight - this.options.loadThreshold) {
this.triggerLoad();
}
}
updateRefreshUI() {
const { pullDistance } = this.state;
const { pullDistance: threshold } = this.options;
// 更新位置
this.refreshEl.style.transform = `translateY(${pullDistance - 60}px)`;
// 更新状态
if (pullDistance > threshold) {
this.refreshEl.querySelector('.refresh-text').textContent = '释放刷新';
this.refreshEl.querySelector('.refresh-icon').style.transform = 'rotate(180deg)';
} else {
this.refreshEl.querySelector('.refresh-text').textContent = '下拉刷新';
this.refreshEl.querySelector('.refresh-icon').style.transform = 'rotate(0deg)';
}
}
triggerRefresh() {
if (this.state.isRefreshing) return;
this.state.isRefreshing = true;
// 显示刷新状态
this.refreshEl.style.transform = 'translateY(0)';
this.refreshEl.querySelector('.refresh-text').textContent = '刷新中...';
this.refreshEl.querySelector('.refresh-icon').textContent = '⟳';
// 执行刷新回调
if (this.options.onRefresh) {
this.options.onRefresh().finally(() => {
this.finishRefresh();
});
} else {
setTimeout(() => this.finishRefresh(), 2000);
}
}
finishRefresh() {
this.state.isRefreshing = false;
this.resetRefreshUI();
}
resetRefreshUI() {
this.refreshEl.style.transform = 'translateY(-60px)';
this.refreshEl.querySelector('.refresh-text').textContent = '下拉刷新';
this.refreshEl.querySelector('.refresh-icon').textContent = '↓';
this.refreshEl.querySelector('.refresh-icon').style.transform = 'rotate(0deg)';
}
triggerLoad() {
if (this.state.isLoading) return;
this.state.isLoading = true;
// 显示加载状态
this.loadEl.style.opacity = '1';
this.loadEl.querySelector('.load-text').textContent = '加载中...';
// 执行加载回调
if (this.options.onLoad) {
this.options.onLoad().finally(() => {
this.finishLoad();
});
} else {
setTimeout(() => this.finishLoad(), 2000);
}
}
finishLoad() {
this.state.isLoading = false;
this.loadEl.style.opacity = '0';
this.loadEl.querySelector('.load-text').textContent = '上拉加载更多';
}
}
// 使用 Intersection Observer 优化性能
class SmartPullRefresh extends PullRefreshComponent {
constructor(container, options) {
super(container, options);
this.setupIntersectionObserver();
}
setupIntersectionObserver() {
// 监听底部元素,提前触发加载
this.bottomObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.state.isLoading) {
this.triggerLoad();
}
});
}, {
rootMargin: '100px'
});
this.bottomObserver.observe(this.loadEl);
}
destroy() {
this.bottomObserver?.disconnect();
}
}
// 使用示例
const container = document.querySelector('.scroll-container');
const pullRefresh = new SmartPullRefresh(container, {
onRefresh: async () => {
// 模拟数据刷新
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('刷新完成');
},
onLoad: async () => {
// 模拟数据加载
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('加载更多数据');
}
});
// CSS-only 下拉刷新(适用于移动端浏览器)
function setupNativePullRefresh() {
document.body.style.overscrollBehavior = 'contain';
// 使用 CSS 自定义属性控制刷新状态
document.addEventListener('touchstart', (e) => {
if (window.scrollY === 0) {
document.documentElement.style.setProperty('--pull-start-y', e.touches[0].clientY);
}
});
document.addEventListener('touchmove', (e) => {
if (window.scrollY === 0) {
const startY = parseFloat(document.documentElement.style.getPropertyValue('--pull-start-y') || 0);
const currentY = e.touches[0].clientY;
const pullDistance = Math.max(0, currentY - startY);
document.documentElement.style.setProperty('--pull-distance', `${pullDistance}px`);
}
});
}
面试官视角:
要点清单:
- 理解触摸事件的基本处理流程
- 掌握上拉下拉的判断逻辑和阈值设置
- 了解防抖和节流在滚动事件中的应用
加分项:
- 实现平滑的阻尼效果和动画
- 使用 Intersection Observer 优化性能
- 考虑原生下拉刷新的兼容性处理
常见失误:
- 没有处理滚动边界情况
- 忽视手势冲突和事件防止默认行为
- 状态管理混乱导致重复触发
延伸阅读:
- Touch Events - MDN — 触摸事件详解
- Intersection Observer API - MDN — 交叉观察器
- CSS overscroll-behavior - MDN — 过度滚动行为控制
如何判断dom元素是否在可视区域
答案
核心概念:
判断DOM元素是否在可视区域是前端开发中的常见需求,主要用于懒加载、动画触发、埋点统计等场景。核心方法包括:getBoundingClientRect API(同步检测)和 Intersection Observer API(异步观察)。现代推荐使用 Intersection Observer,性能更好且功能更强大。
实际示例:
// 方法1:getBoundingClientRect - 精确实时检测
class ViewportDetector {
constructor(threshold = 0) {
this.threshold = threshold; // 可见度阈值 0-1
}
// 基础可见性检测
isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowHeight &&
rect.right <= windowWidth
);
}
// 部分可见检测
isPartiallyVisible(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < windowHeight &&
rect.left < windowWidth
);
}
// 可见度百分比计算
getVisibilityPercentage(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const visibleWidth = Math.min(rect.right, windowWidth) - Math.max(rect.left, 0);
const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);
if (visibleWidth <= 0 || visibleHeight <= 0) return 0;
const visibleArea = visibleWidth * visibleHeight;
const totalArea = rect.width * rect.height;
return totalArea > 0 ? visibleArea / totalArea : 0;
}
// 批量检测多个元素
checkMultipleElements(elements) {
return elements.map(element => ({
element,
isVisible: this.isPartiallyVisible(element),
percentage: this.getVisibilityPercentage(element)
}));
}
}
// 方法2:Intersection Observer - 性能优化的异步观察
class IntersectionManager {
constructor(options = {}) {
this.options = {
root: null, // 观察的根元素
rootMargin: '0px', // 根元素边距
threshold: [0, 0.25, 0.5, 0.75, 1], // 触发阈值
...options
};
this.callbacks = new Map();
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
this.options
);
}
handleIntersection(entries) {
entries.forEach(entry => {
const callback = this.callbacks.get(entry.target);
if (callback) {
callback({
element: entry.target,
isIntersecting: entry.isIntersecting,
intersectionRatio: entry.intersectionRatio,
boundingClientRect: entry.boundingClientRect,
intersectionRect: entry.intersectionRect,
rootBounds: entry.rootBounds,
time: entry.time
});
}
});
}
observe(element, callback) {
this.callbacks.set(element, callback);
this.observer.observe(element);
}
unobserve(element) {
this.callbacks.delete(element);
this.observer.unobserve(element);
}
disconnect() {
this.observer.disconnect();
this.callbacks.clear();
}
}
// 方法3:高级应用 - 懒加载实现
class LazyLoader {
constructor(options = {}) {
this.options = {
rootMargin: '50px',
threshold: 0.1,
...options
};
this.observer = new IntersectionObserver(
this.handleLazyLoad.bind(this),
this.options
);
this.loadedElements = new Set();
}
handleLazyLoad(entries) {
entries.forEach(entry => {
if (entry.isIntersecting && !this.loadedElements.has(entry.target)) {
this.loadElement(entry.target);
this.loadedElements.add(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadElement(element) {
// 图片懒加载
if (element.tagName === 'IMG') {
const src = element.dataset.src;
if (src) {
element.src = src;
element.removeAttribute('data-src');
}
}
// 背景图懒加载
const bgImage = element.dataset.bgImage;
if (bgImage) {
element.style.backgroundImage = `url(${bgImage})`;
element.removeAttribute('data-bg-image');
}
// 内容懒加载
const lazyContent = element.dataset.lazyContent;
if (lazyContent) {
element.innerHTML = lazyContent;
element.removeAttribute('data-lazy-content');
}
// 触发自定义加载事件
element.dispatchEvent(new CustomEvent('lazyload', {
bubbles: true,
detail: { element }
}));
}
observe(element) {
this.observer.observe(element);
}
}
// 方法4:滚动性能优化版本
class OptimizedVisibilityChecker {
constructor() {
this.elements = new Set();
this.callbacks = new Map();
this.isChecking = false;
// 使用节流的滚动监听
this.throttledCheck = this.throttle(this.checkAll.bind(this), 100);
this.bindEvents();
}
throttle(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
bindEvents() {
window.addEventListener('scroll', this.throttledCheck);
window.addEventListener('resize', this.throttledCheck);
}
checkAll() {
if (this.isChecking) return;
this.isChecking = true;
requestAnimationFrame(() => {
this.elements.forEach(element => {
const callback = this.callbacks.get(element);
if (callback) {
const detector = new ViewportDetector();
const isVisible = detector.isPartiallyVisible(element);
const percentage = detector.getVisibilityPercentage(element);
callback({ element, isVisible, percentage });
}
});
this.isChecking = false;
});
}
observe(element, callback) {
this.elements.add(element);
this.callbacks.set(element, callback);
}
unobserve(element) {
this.elements.delete(element);
this.callbacks.delete(element);
}
destroy() {
window.removeEventListener('scroll', this.throttledCheck);
window.removeEventListener('resize', this.throttledCheck);
this.elements.clear();
this.callbacks.clear();
}
}
// 使用示例
const detector = new ViewportDetector();
const element = document.getElementById('my-element');
// 基础检测
console.log('完全可见:', detector.isInViewport(element));
console.log('部分可见:', detector.isPartiallyVisible(element));
console.log('可见度:', detector.getVisibilityPercentage(element));
// Intersection Observer 使用
const intersectionManager = new IntersectionManager({
threshold: [0, 0.5, 1]
});
intersectionManager.observe(element, (data) => {
console.log('可见性变化:', data.isIntersecting);
console.log('可见比例:', data.intersectionRatio);
});
// 懒加载使用
const lazyLoader = new LazyLoader();
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoader.observe(img);
});
面试官视角:
要点清单:
- 理解 getBoundingClientRect 的工作原理和坐标系统
- 掌握 Intersection Observer API 的使用和优势
- 了解可见性检测的性能优化策略
加分项:
- 能够实现复杂的可见度百分比计算
- 熟悉懒加载的最佳实践
- 掌握滚动性能优化技巧
常见失误:
- 频繁调用 getBoundingClientRect 导致性能问题
- 忽略边界情况和部分可见的处理
- 不考虑动态内容和响应式布局的影响
延伸阅读:
- Intersection Observer API - MDN — 交叉观察器详解
- Element.getBoundingClientRect() - MDN — 元素边界检测
- Lazy Loading Images and Video — 懒加载最佳实践
如何检测网页空闲状态(一定时间内无操作)
答案
核心概念:
网页空闲状态检测是通过监听用户交互事件来判断用户是否在一定时间内没有操作页面。核心技术包括:事件监听(mouse、keyboard、touch、scroll等)、防抖处理、Page Visibility API集成、定时器管理等。现代浏览器还提供了实验性的 Idle Detection API。
实际示例:
// 方法1:基础实现 - 监听关键用户事件
class IdleDetector {
constructor(options = {}) {
this.options = {
timeout: 15000, // 空闲超时时间(毫秒)
events: ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart'],
throttleDelay: 100, // 防抖延迟
...options
};
this.callbacks = [];
this.timer = null;
this.lastActivity = Date.now();
this.isIdle = false;
this.isVisible = !document.hidden;
this.throttledResetTimer = this.throttle(this.resetTimer.bind(this), this.options.throttleDelay);
this.init();
}
init() {
this.bindEvents();
this.startTimer();
}
bindEvents() {
// 监听用户活动事件
this.options.events.forEach(event => {
document.addEventListener(event, this.throttledResetTimer, true);
});
// 监听页面可见性变化
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
// 监听窗口焦点变化
window.addEventListener('focus', this.handleFocus.bind(this));
window.addEventListener('blur', this.handleBlur.bind(this));
}
handleVisibilityChange() {
this.isVisible = !document.hidden;
if (this.isVisible) {
// 页面变为可见时重置计时器
this.resetTimer();
} else {
// 页面隐藏时清除计时器
this.clearTimer();
}
}
handleFocus() {
this.isVisible = true;
this.resetTimer();
}
handleBlur() {
this.isVisible = false;
this.clearTimer();
}
throttle(func, wait) {
let timeout;
let lastRun = 0;
return function executedFunction(...args) {
if (!lastRun || (Date.now() - lastRun >= wait)) {
func.apply(this, args);
lastRun = Date.now();
} else {
clearTimeout(timeout);
timeout = setTimeout(() => {
if ((Date.now() - lastRun) >= wait) {
func.apply(this, args);
lastRun = Date.now();
}
}, wait - (Date.now() - lastRun));
}
};
}
resetTimer() {
if (!this.isVisible) return;
this.lastActivity = Date.now();
this.clearTimer();
if (this.isIdle) {
this.isIdle = false;
this.trigger('active');
}
this.startTimer();
}
startTimer() {
this.timer = setTimeout(() => {
this.isIdle = true;
this.trigger('idle');
}, this.options.timeout);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
onIdle(callback) {
this.callbacks.push({ type: 'idle', callback });
return this;
}
onActive(callback) {
this.callbacks.push({ type: 'active', callback });
return this;
}
trigger(type) {
this.callbacks
.filter(item => item.type === type)
.forEach(item => item.callback({
type,
isIdle: this.isIdle,
lastActivity: this.lastActivity,
idleTime: Date.now() - this.lastActivity
}));
}
destroy() {
this.clearTimer();
this.options.events.forEach(event => {
document.removeEventListener(event, this.throttledResetTimer, true);
});
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
window.removeEventListener('focus', this.handleFocus);
window.removeEventListener('blur', this.handleBlur);
this.callbacks = [];
}
}
// 方法2:高级实现 - 支持多种空闲状态
class AdvancedIdleDetector extends IdleDetector {
constructor(options = {}) {
super(options);
this.idleLevels = options.idleLevels || [
{ name: 'short', timeout: 5000 }, // 5秒短暂空闲
{ name: 'medium', timeout: 15000 }, // 15秒中等空闲
{ name: 'long', timeout: 60000 } // 60秒长期空闲
];
this.currentLevel = null;
this.levelTimers = new Map();
}
startTimer() {
// 清除所有级别的计时器
this.clearAllLevelTimers();
// 为每个空闲级别设置计时器
this.idleLevels.forEach(level => {
const timer = setTimeout(() => {
this.currentLevel = level.name;
this.trigger('idle', { level: level.name, timeout: level.timeout });
}, level.timeout);
this.levelTimers.set(level.name, timer);
});
}
clearAllLevelTimers() {
this.levelTimers.forEach(timer => clearTimeout(timer));
this.levelTimers.clear();
}
resetTimer() {
this.lastActivity = Date.now();
this.clearAllLevelTimers();
if (this.isIdle) {
this.isIdle = false;
this.currentLevel = null;
this.trigger('active');
}
this.startTimer();
}
destroy() {
this.clearAllLevelTimers();
super.destroy();
}
}
// 方法3:使用现代 Idle Detection API(实验性)
class NativeIdleDetector {
constructor(options = {}) {
this.options = {
threshold: 60000, // 60秒
...options
};
this.controller = null;
this.callbacks = [];
}
async init() {
if (!('IdleDetector' in window)) {
throw new Error('Idle Detection API not supported');
}
// 请求权限
const state = await IdleDetector.requestPermission();
if (state !== 'granted') {
throw new Error('Idle detection permission denied');
}
this.controller = new IdleDetector();
this.controller.addEventListener('change', this.handleChange.bind(this));
await this.controller.start({
threshold: this.options.threshold,
signal: new AbortController().signal
});
}
handleChange() {
const userState = this.controller.userState;
const screenState = this.controller.screenState;
this.callbacks.forEach(callback => {
callback({
userState, // 'active' | 'idle'
screenState, // 'locked' | 'unlocked'
timestamp: Date.now()
});
});
}
onChange(callback) {
this.callbacks.push(callback);
}
destroy() {
if (this.controller) {
this.controller.removeEventListener('change', this.handleChange);
this.controller = null;
}
this.callbacks = [];
}
}
// 方法4:结合用户行为分析的智能空闲检测
class SmartIdleDetector extends AdvancedIdleDetector {
constructor(options = {}) {
super(options);
this.behaviorPatterns = {
clicks: [],
scrolls: [],
keystrokes: []
};
this.learningMode = options.learningMode || false;
}
bindEvents() {
super.bindEvents();
if (this.learningMode) {
document.addEventListener('click', this.recordClick.bind(this));
document.addEventListener('scroll', this.recordScroll.bind(this));
document.addEventListener('keydown', this.recordKeystroke.bind(this));
}
}
recordClick(e) {
this.behaviorPatterns.clicks.push({
timestamp: Date.now(),
x: e.clientX,
y: e.clientY,
target: e.target.tagName
});
// 保持最近100个点击记录
if (this.behaviorPatterns.clicks.length > 100) {
this.behaviorPatterns.clicks.shift();
}
}
recordScroll() {
this.behaviorPatterns.scrolls.push({
timestamp: Date.now(),
scrollY: window.scrollY
});
if (this.behaviorPatterns.scrolls.length > 50) {
this.behaviorPatterns.scrolls.shift();
}
}
recordKeystroke() {
this.behaviorPatterns.keystrokes.push({
timestamp: Date.now()
});
if (this.behaviorPatterns.keystrokes.length > 100) {
this.behaviorPatterns.keystrokes.shift();
}
}
getActivityInsights() {
const now = Date.now();
const last5Minutes = now - 5 * 60 * 1000;
return {
recentClicks: this.behaviorPatterns.clicks.filter(c => c.timestamp > last5Minutes).length,
recentScrolls: this.behaviorPatterns.scrolls.filter(s => s.timestamp > last5Minutes).length,
recentKeystrokes: this.behaviorPatterns.keystrokes.filter(k => k.timestamp > last5Minutes).length,
isHighActivity: this.isHighActivityPeriod(),
predictedIdleTime: this.predictIdleTime()
};
}
isHighActivityPeriod() {
const insights = this.getActivityInsights();
return insights.recentClicks > 10 || insights.recentScrolls > 5 || insights.recentKeystrokes > 20;
}
predictIdleTime() {
// 基于历史模式预测可能的空闲时长
const patterns = this.behaviorPatterns;
if (patterns.clicks.length < 5) return this.options.timeout;
// 简单的预测逻辑
const avgInterval = patterns.clicks.slice(-5).reduce((acc, click, index, arr) => {
if (index === 0) return acc;
return acc + (click.timestamp - arr[index - 1].timestamp);
}, 0) / 4;
return Math.max(avgInterval * 2, this.options.timeout);
}
}
// 使用示例
const idleDetector = new AdvancedIdleDetector({
timeout: 15000,
idleLevels: [
{ name: 'short', timeout: 5000 },
{ name: 'medium', timeout: 15000 },
{ name: 'long', timeout: 60000 }
]
});
idleDetector
.onIdle((data) => {
console.log(`用户空闲 - 级别: ${data.level || 'default'}`);
// 执行空闲时的操作,如暂停视频、保存草稿等
})
.onActive(() => {
console.log('用户重新活跃');
// 执行用户重新活跃时的操作
});
// 智能检测使用
const smartDetector = new SmartIdleDetector({
learningMode: true,
timeout: 10000
});
setInterval(() => {
const insights = smartDetector.getActivityInsights();
console.log('用户活动分析:', insights);
}, 30000);
面试官视角:
要点清单:
- 理解用户交互事件的监听和防抖处理
- 掌握 Page Visibility API 在空闲检测中的应用
- 了解定时器管理和内存泄漏防护
加分项:
- 实现多级空闲状态检测
- 结合用户行为分析优化检测逻辑
- 了解现代 Idle Detection API 的使用
常见失误:
- 过于频繁的事件监听导致性能问题
- 忽略页面可见性变化的处理
- 没有正确清理事件监听器和定时器
延伸阅读:
- Page Visibility API - MDN — 页面可见性API
- Idle Detection API - W3C — 空闲检测API规范
- User Idle Detection — 空闲检测最佳实践
一次性渲染十万条数据还能保证页面不卡顿
答案
核心概念:
大量数据渲染性能优化的核心是避免阻塞主线程,主要技术方案包括:时间切片(Time Slicing)、虚拟滚动(Virtual Scrolling)、Web Worker 处理、requestAnimationFrame 调度。核心思想是将大任务分解为小任务,在浏览器空闲时间执行,确保用户交互响应性。
实际示例:
// 方案1:时间切片 + requestAnimationFrame
class TimeSlicedRenderer {
constructor(container, options = {}) {
this.container = container;
this.options = {
batchSize: 1000, // 每批次渲染数量
maxTimeSlice: 16, // 最大时间片(毫秒)
useFragment: true, // 使用文档片段优化
...options
};
this.isRendering = false;
this.renderQueue = [];
}
renderBigList(data, renderItem) {
if (this.isRendering) return;
this.isRendering = true;
this.renderQueue = [...data];
this.scheduleWork(renderItem);
}
scheduleWork(renderItem) {
const workLoop = (deadline) => {
// 在时间片内尽可能多地处理数据
while (deadline.timeRemaining() > 0 && this.renderQueue.length > 0) {
const batchData = this.renderQueue.splice(0, this.options.batchSize);
this.renderBatch(batchData, renderItem);
}
// 如果还有数据未处理,继续调度
if (this.renderQueue.length > 0) {
requestIdleCallback(workLoop);
} else {
this.isRendering = false;
this.onComplete?.();
}
};
requestIdleCallback(workLoop);
}
renderBatch(batchData, renderItem) {
const fragment = document.createDocumentFragment();
batchData.forEach((item, index) => {
const element = renderItem(item, index);
fragment.appendChild(element);
});
this.container.appendChild(fragment);
}
onComplete() {
console.log('渲染完成');
}
}
// 方案2:虚拟滚动 - 只渲染可见区域
class VirtualScrollList {
constructor(container, options = {}) {
this.container = container;
this.options = {
itemHeight: 40, // 单项高度
bufferSize: 5, // 缓冲区大小
overscan: 3, // 预渲染项数
...options
};
this.data = [];
this.viewportHeight = container.clientHeight;
this.visibleCount = Math.ceil(this.viewportHeight / this.options.itemHeight);
this.scrollTop = 0;
this.init();
}
init() {
this.setupContainer();
this.bindEvents();
}
setupContainer() {
this.container.style.position = 'relative';
this.container.style.overflow = 'auto';
// 创建滚动容器
this.scrollContainer = document.createElement('div');
this.scrollContainer.style.position = 'absolute';
this.scrollContainer.style.top = '0';
this.scrollContainer.style.left = '0';
this.scrollContainer.style.right = '0';
this.container.appendChild(this.scrollContainer);
}
bindEvents() {
let ticking = false;
this.container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
this.handleScroll();
ticking = false;
});
ticking = true;
}
});
}
setData(data) {
this.data = data;
this.totalHeight = data.length * this.options.itemHeight;
// 设置总高度以支持滚动
this.container.style.height = `${Math.min(this.totalHeight, this.viewportHeight)}px`;
this.scrollContainer.style.height = `${this.totalHeight}px`;
this.render();
}
handleScroll() {
this.scrollTop = this.container.scrollTop;
this.render();
}
render() {
const startIndex = Math.floor(this.scrollTop / this.options.itemHeight);
const endIndex = Math.min(
startIndex + this.visibleCount + this.options.overscan,
this.data.length
);
const visibleStartIndex = Math.max(0, startIndex - this.options.overscan);
// 清空现有内容
this.scrollContainer.innerHTML = '';
// 渲染可见项
for (let i = visibleStartIndex; i < endIndex; i++) {
const item = this.createItem(this.data[i], i);
item.style.position = 'absolute';
item.style.top = `${i * this.options.itemHeight}px`;
item.style.height = `${this.options.itemHeight}px`;
this.scrollContainer.appendChild(item);
}
}
createItem(data, index) {
const item = document.createElement('div');
item.className = 'virtual-list-item';
item.textContent = `Item ${index}: ${data}`;
return item;
}
}
// 方案3:Web Worker + 渲染调度
class WorkerRenderer {
constructor(container) {
this.container = container;
this.worker = this.createWorker();
this.renderQueue = [];
this.worker.onmessage = this.handleWorkerMessage.bind(this);
}
createWorker() {
const workerScript = `
self.onmessage = function(e) {
const { type, data } = e.data;
switch(type) {
case 'PROCESS_DATA':
const processedData = data.map((item, index) => ({
id: index,
content: \`处理后的项目 \${index}: \${item}\`,
processed: true
}));
// 模拟数据处理
self.postMessage({
type: 'DATA_PROCESSED',
data: processedData
});
break;
}
};
`;
const blob = new Blob([workerScript], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
}
handleWorkerMessage(e) {
const { type, data } = e.data;
switch(type) {
case 'DATA_PROCESSED':
this.renderProcessedData(data);
break;
}
}
processAndRender(rawData) {
this.worker.postMessage({
type: 'PROCESS_DATA',
data: rawData
});
}
renderProcessedData(data) {
const renderer = new TimeSlicedRenderer(this.container, {
batchSize: 500,
maxTimeSlice: 8
});
renderer.renderBigList(data, (item) => {
const div = document.createElement('div');
div.textContent = item.content;
div.style.padding = '8px';
div.style.borderBottom = '1px solid #eee';
return div;
});
}
destroy() {
this.worker.terminate();
}
}
// 方案4:智能批次渲染(动态调整批次大小)
class AdaptiveRenderer {
constructor(container) {
this.container = container;
this.performanceMetrics = {
avgRenderTime: 16,
samples: []
};
this.batchSize = 100;
}
renderLargeDataset(data, renderItem) {
let index = 0;
const renderChunk = () => {
const startTime = performance.now();
const fragment = document.createDocumentFragment();
// 渲染当前批次
const endIndex = Math.min(index + this.batchSize, data.length);
for (let i = index; i < endIndex; i++) {
const element = renderItem(data[i], i);
fragment.appendChild(element);
}
this.container.appendChild(fragment);
const endTime = performance.now();
const renderTime = endTime - startTime;
// 更新性能指标
this.updateMetrics(renderTime);
// 动态调整批次大小
this.adjustBatchSize(renderTime);
index = endIndex;
// 继续渲染下一批次
if (index < data.length) {
// 使用 MessageChannel 实现更好的调度
this.scheduleNextChunk(renderChunk);
}
};
renderChunk();
}
updateMetrics(renderTime) {
this.performanceMetrics.samples.push(renderTime);
if (this.performanceMetrics.samples.length > 10) {
this.performanceMetrics.samples.shift();
}
this.performanceMetrics.avgRenderTime =
this.performanceMetrics.samples.reduce((a, b) => a + b, 0) /
this.performanceMetrics.samples.length;
}
adjustBatchSize(renderTime) {
const targetTime = 8; // 目标渲染时间(毫秒)
if (renderTime > targetTime * 1.5) {
// 渲染时间过长,减少批次大小
this.batchSize = Math.max(10, Math.floor(this.batchSize * 0.8));
} else if (renderTime < targetTime * 0.5) {
// 渲染时间较短,增加批次大小
this.batchSize = Math.min(1000, Math.floor(this.batchSize * 1.2));
}
}
scheduleNextChunk(callback) {
if ('scheduler' in window && window.scheduler.postTask) {
// 使用现代调度API
window.scheduler.postTask(callback, { priority: 'user-blocking' });
} else if ('requestIdleCallback' in window) {
requestIdleCallback(callback);
} else {
setTimeout(callback, 0);
}
}
}
// 使用示例
const container = document.getElementById('big-list');
const data = Array.from({ length: 100000 }, (_, i) => `数据项 ${i}`);
// 时间切片渲染
const renderer = new TimeSlicedRenderer(container);
renderer.renderBigList(data, (item, index) => {
const div = document.createElement('div');
div.textContent = item;
div.style.padding = '5px';
div.style.borderBottom = '1px solid #eee';
return div;
});
// 虚拟滚动(适用于列表场景)
const virtualList = new VirtualScrollList(container, {
itemHeight: 40,
bufferSize: 10
});
virtualList.setData(data);
// Worker + 渲染调度
const workerRenderer = new WorkerRenderer(container);
workerRenderer.processAndRender(data);
面试官视角:
要点清单:
- 理解时间切片和 requestAnimationFrame 的调度原理
- 掌握虚拟滚动的实现原理和适用场景
- 了解 Web Worker 在数据处理中的应用
加分项:
- 实现自适应的批次大小调整
- 使用现代调度 API 优化任务执行
- 结合性能监控指标进行动态优化
常见失误:
- 忽略 DocumentFragment 的使用导致频繁回流
- 没有考虑用户交互响应性
- 缺乏对不同设备性能的适配
延伸阅读:
- Long Tasks API - MDN — 长任务监控
- Scheduler API - WICG — 现代任务调度
- Virtual Scrolling - web.dev — 虚拟滚动最佳实践
计算一段文本渲染之后的长度
答案
核心概念:
文本渲染长度计算是前端开发中的重要技术,主要用于文本截断、自适应布局、性能优化等场景。核心方法包括:临时DOM元素测量、Canvas measureText API、现代Intersection Observer、预计算缓存等。需要考虑字体加载、样式继承、多行文本处理等复杂情况。
实际示例:
// 方法1:临时DOM元素测量(最准确)
class TextMeasurer {
constructor() {
this.measureElement = null;
this.cache = new Map();
this.setupMeasureElement();
}
setupMeasureElement() {
this.measureElement = document.createElement('span');
this.measureElement.style.cssText = `
position: absolute;
visibility: hidden;
white-space: nowrap;
pointer-events: none;
top: -9999px;
left: -9999px;
`;
document.body.appendChild(this.measureElement);
}
measureText(text, styles = {}) {
const cacheKey = this.getCacheKey(text, styles);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// 应用样式
Object.assign(this.measureElement.style, styles);
this.measureElement.textContent = text;
const rect = this.measureElement.getBoundingClientRect();
const result = {
width: rect.width,
height: rect.height
};
this.cache.set(cacheKey, result);
return result;
}
getCacheKey(text, styles) {
return `${text}_${JSON.stringify(styles)}`;
}
destroy() {
if (this.measureElement && this.measureElement.parentNode) {
this.measureElement.parentNode.removeChild(this.measureElement);
}
this.cache.clear();
}
}
// 方法2:Canvas measureText(高性能)
class CanvasTextMeasurer {
constructor() {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.cache = new Map();
}
measureText(text, fontStyle = '16px Arial') {
const cacheKey = `${text}_${fontStyle}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
this.ctx.font = fontStyle;
const metrics = this.ctx.measureText(text);
const result = {
width: metrics.width,
height: this.getFontHeight(fontStyle)
};
this.cache.set(cacheKey, result);
return result;
}
getFontHeight(fontStyle) {
const match = fontStyle.match(/(\d+(?:\.\d+)?)px/);
return match ? parseFloat(match[1]) * 1.2 : 16;
}
}
// 方法3:智能文本折叠计算器
class TextCollapseCalculator {
constructor(container, options = {}) {
this.container = container;
this.options = {
maxLines: 3,
ellipsis: '...',
...options
};
this.measurer = new TextMeasurer();
}
calculateCollapse(text) {
const containerStyles = this.getContainerStyles();
const lineHeight = this.getLineHeight(containerStyles);
const maxWidth = this.container.clientWidth;
const maxHeight = lineHeight * this.options.maxLines;
// 检查是否需要折叠
const measurement = this.measurer.measureText(text, containerStyles);
if (measurement.height <= maxHeight) {
return {
shouldCollapse: false,
displayText: text
};
}
// 计算截断文本
const truncatedText = this.calculateTruncatedText(
text, containerStyles, maxWidth, maxHeight
);
return {
shouldCollapse: true,
displayText: truncatedText,
originalText: text
};
}
calculateTruncatedText(text, styles, maxWidth, maxHeight) {
const ellipsisWidth = this.measurer.measureText(this.options.ellipsis, styles).width;
const availableWidth = maxWidth - ellipsisWidth;
// 二分查找最合适的文本长度
let left = 0, right = text.length;
let result = '';
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const testText = text.substring(0, mid);
const measurement = this.measurer.measureText(testText, styles);
if (measurement.width <= availableWidth) {
result = testText;
left = mid + 1;
} else {
right = mid - 1;
}
}
return result + this.options.ellipsis;
}
getContainerStyles() {
const computed = getComputedStyle(this.container);
return {
fontSize: computed.fontSize,
fontFamily: computed.fontFamily,
fontWeight: computed.fontWeight
};
}
getLineHeight(styles) {
return parseFloat(styles.fontSize) * 1.2;
}
}
// 使用示例
const container = document.getElementById('text-container');
const calculator = new TextCollapseCalculator(container, { maxLines: 3 });
const longText = "这是一段很长的文本,需要根据容器宽度计算是否折叠...";
const result = calculator.calculateCollapse(longText);
if (result.shouldCollapse) {
container.innerHTML = result.displayText + '<button onclick="expand()">展开</button>';
} else {
container.textContent = result.displayText;
}
面试官视角:
要点清单:
- 掌握DOM元素和Canvas两种文本测量方法
- 理解文本折叠的计算逻辑和性能优化
- 了解字体加载对测量准确性的影响
加分项:
- 实现高性能的批量文本测量
- 考虑多行文本和复杂布局处理
- 处理字体回退和国际化场景
常见失误:
- 忽略字体加载完成前测量不准确
- 没有考虑行高和字符间距影响
- 缺乏缓存机制导致重复计算
延伸阅读:
- Canvas measureText - MDN — Canvas文本测量
- Font Loading API - MDN — 字体加载检测
- CSS Text Module - W3C — CSS文本规范