跳到主要内容

BOM 和 DOM✅

DOM0 到 DOM3 区别?

答案
级别标准化主要内容与特性事件处理方式事件类型扩展
DOM0早期浏览器私有API,未标准化,仅支持基本DOM操作直接赋值事件属性(如onclick)少量,类型有限
DOM1W3C首个标准,定义DOM核心(XML结构)和DOM HTML模块,支持基本节点操作同DOM0基本事件
DOM2扩展节点API,新增视图、事件、遍历、样式等模块,首次引入标准事件模型addEventListener/removeEventListener,支持捕获/冒泡类型增多,支持多监听
DOM3增加XPath、加载/保存、验证等模块,事件类型更丰富,支持自定义事件同DOM2UI、键盘、合成等更多

事件模型与事件流

  • 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 统计和遍历示例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>HTML 标签统计示例</title>
  <style>
      body {
          font-family: Arial, sans-serif;
          margin: 20px;
          line-height: 1.6;
      }
      .demo-section {
          border: 2px solid #007bff;
          padding: 20px;
          margin: 20px 0;
          border-radius: 8px;
          background-color: #f8f9fa;
      }
      button {
          margin-bottom: 20px;
          padding: 10px 15px;
          background-color: #007bff;
          color: white;
          border: none;
          border-radius: 5px;
          cursor: pointer;
          font-size: 16px;
      }
      button:hover {
          background-color: #0056b3;
      }
      .results {
          background-color: #d4edda;
          padding: 15px;
          border-radius: 5px;
          margin-top: 15px;
      }
      pre {
          background-color: #f8f9fa;
          padding: 10px;
          border-radius: 5px;
          overflow-x: auto;
          max-height: 300px;
      }
      .demo-content {
          margin: 10px 0;
      }
      iframe {
          border: 1px solid #ccc;
          border-radius: 4px;
      }
  </style>
</head>
<body>
  <h1>HTML 标签统计与DOM遍历</h1>

  <button onclick="domCount()">统计页面所有标签</button>
  
  <div class="demo-section">
      <h2>测试内容区域</h2>
      <div class="demo-content">
          <p>这是一个段落。</p>
          <ul>
              <li>列表项 1</li>
              <li>列表项 2</li>
              <li>
                  <span>嵌套内容</span>
                  <a href="#">链接</a>
              </li>
          </ul>
          <table>
              <tr><td>表格单元格</td></tr>
          </table>
          <iframe srcdoc="<h1>iframe内容</h1><p>iframe段落</p>"></iframe>
      </div>
  </div>
  
  <div id="results"></div>

  <script>
      // 统计DOM标签的函数(支持iframe)
      function countTags(rootElement) {
          const tagCounts = {};

          if (!rootElement) {
              console.error("Root element is null or undefined.");
              return tagCounts;
          }

          // 使用栈进行深度优先遍历
          const stack = [rootElement];

          while (stack.length > 0) {
              const element = stack.pop();

              if (element.nodeType === Node.ELEMENT_NODE) {
                  const tagName = element.tagName.toLowerCase();
                  tagCounts[tagName] = (tagCounts[tagName] || 0) + 1;

                  // 特殊处理iframe
                  if (tagName === "iframe") {
                      try {
                          // 尝试访问同源iframe的内容
                          const iframeDocument = element.contentDocument || 
                                               element.contentWindow.document;
                          if (iframeDocument && iframeDocument.body) {
                              // 递归统计iframe内容
                              const iframeTagCounts = countTags(iframeDocument.body);
                              
                              // 合并iframe标签统计
                              for (const tag in iframeTagCounts) {
                                  if (iframeTagCounts.hasOwnProperty(tag)) {
                                      tagCounts[tag] = (tagCounts[tag] || 0) + 
                                                     iframeTagCounts[tag];
                                  }
                              }
                          }
                      } catch (e) {
                          console.warn("无法访问跨源iframe内容:", e.message);
                      }
                  } else {
                      // 将子元素添加到栈中(倒序添加保持原始顺序)
                      for (let i = element.children.length - 1; i >= 0; i--) {
                          stack.push(element.children[i]);
                      }
                  }
              }
          }

          return tagCounts;
      }

      // 主统计函数
      function domCount() {
          console.log('开始统计DOM标签...');
          
          const root = document.documentElement; // 从根元素开始
          if (!root) {
              console.error("无法找到根元素");
              return;
          }

          const startTime = performance.now();
          const counts = countTags(root);
          const endTime = performance.now();
          
          console.log('统计结果:', counts);
          
          // 在页面上显示结果
          displayResults(counts, endTime - startTime);
      }

      // 显示统计结果
      function displayResults(counts, duration) {
          const resultsElement = document.getElementById('results');
          
          // 按标签名排序
          const sortedCounts = Object.entries(counts)
              .sort(([a], [b]) => a.localeCompare(b))
              .reduce((acc, [key, value]) => {
                  acc[key] = value;
                  return acc;
              }, {});

          let html = '<div class="results">';
          html += '<h3>DOM标签统计结果</h3>';
          html += `<p><strong>统计耗时:</strong> ${duration.toFixed(2)}ms</p>`;
          html += `<p><strong>标签种类:</strong> ${Object.keys(counts).length} 种</p>`;
          html += `<p><strong>标签总数:</strong> ${Object.values(counts).reduce((a, b) => a + b, 0)} 个</p>`;
          
          html += '<h4>详细统计:</h4>';
          html += '<pre>' + JSON.stringify(sortedCounts, null, 2) + '</pre>';
          html += '</div>';
          
          resultsElement.innerHTML = html;
      }

      // 页面加载完成后的提示
      document.addEventListener('DOMContentLoaded', () => {
          console.log('页面加载完成,可以开始统计DOM标签');
      });
  </script>
</body>
</html>

复杂DOM交互示例

<!DOCTYPE html>
<html>
<head>
  <title>可拖拽节点连接器</title>
  <style>
      body {
          margin: 0;
          padding: 20px;
          font-family: Arial, sans-serif;
          background-color: #f5f5f5;
      }

      h1 {
          color: #333;
          text-align: center;
      }
      
      .instructions {
          background-color: #e7f3ff;
          padding: 15px;
          border-radius: 5px;
          margin-bottom: 20px;
          border-left: 4px solid #007bff;
      }
      
      .workspace {
          position: relative;
          width: 100%;
          height: 500px;
          background-color: white;
          border: 2px solid #ddd;
          border-radius: 8px;
          overflow: hidden;
      }
      
      .node {
          width: 100px;
          height: 100px;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          border: 2px solid #fff;
          position: absolute;
          cursor: move;
          display: flex;
          align-items: center;
          justify-content: center;
          user-select: none;
          border-radius: 10px;
          color: white;
          font-weight: bold;
          box-shadow: 0 4px 8px rgba(0,0,0,0.1);
          transition: transform 0.2s ease;
      }
      
      .node:hover {
          transform: scale(1.05);
          box-shadow: 0 6px 12px rgba(0,0,0,0.15);
      }
      
      #node1 { 
          left: 100px; 
          top: 100px; 
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      }
      
      #node2 { 
          left: 350px; 
          top: 300px; 
          background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
      }
      
      .connector {
          width: 12px;
          height: 12px;
          background: #fff;
          position: absolute;
          border-radius: 50%;
          cursor: pointer;
          z-index: 10;
          border: 2px solid #007bff;
          transition: all 0.2s ease;
      }
      
      .connector:hover {
          background: #007bff;
          transform: scale(1.3);
          box-shadow: 0 0 8px rgba(0,123,255,0.4);
      }
      
      .line {
          position: absolute;
          pointer-events: none;
          z-index: 1;
      }
      
      /* 连接点位置 */
      .top { top: -6px; left: 44px; }
      .right { top: 44px; right: -6px; }
      .bottom { bottom: -6px; left: 44px; }
      .left { top: 44px; left: -6px; }
      
      .connection-count {
          position: absolute;
          top: 10px;
          right: 10px;
          background: rgba(0,0,0,0.7);
          color: white;
          padding: 8px 12px;
          border-radius: 5px;
          font-size: 14px;
      }
  </style>
</head>
<body>
  <h1>可拖拽节点连接器</h1>

  <div class="instructions">
      <strong>操作说明:</strong>
      <ul>
          <li>拖拽节点可以移动位置</li>
          <li>点击节点边缘的小圆点开始连线</li>
          <li>拖拽到另一个节点的连接点完成连线</li>
          <li>连线会自动跟随节点移动</li>
      </ul>
  </div>
  
  <div class="workspace">
      <svg id="lines" style="position: absolute; width: 100%; height: 100%; pointer-events: none;"></svg>
      <div class="connection-count" id="connectionCount">连接数: 0</div>
      
      <div id="node1" class="node">节点 1</div>
      <div id="node2" class="node">节点 2</div>
  </div>
  
  <script>
      const svg = document.getElementById('lines');
      const connectionCount = document.getElementById('connectionCount');
      let isDragging = false;
      let selectedConnector = null;
      let connections = [];
      let tempLine = null;
      
      // 等待 DOM 加载完成后初始化
      document.addEventListener('DOMContentLoaded', () => {
          const nodes = document.querySelectorAll('.node');
          nodes.forEach(node => {
              addConnectors(node);
              enableDrag(node);
          });
          updateConnectionCount();
      });
      
      // 为节点添加连接点
      function addConnectors(node) {
          ['top', 'right', 'bottom', 'left'].forEach(position => {
              const connector = document.createElement('div');
              connector.className = `connector ${position}`;
              connector.dataset.position = position;
              connector.dataset.nodeId = node.id;
              
              connector.addEventListener('mousedown', (e) => {
                  e.stopPropagation();
                  startConnection(e);
              });
              
              node.appendChild(connector);
          });
      }
      
      // 开始连接操作
      function startConnection(e) {
          e.stopPropagation();
          selectedConnector = e.target;
          selectedConnector.style.background = '#28a745';
          
          document.addEventListener('mousemove', drawTempLine);
          document.addEventListener('mouseup', endConnection);
      }
      
      // 结束连接操作
      function endConnection(e) {
          e.stopPropagation();
          
          if (selectedConnector && e.target.classList.contains('connector')) {
              const start = selectedConnector;
              const end = e.target;
              
              // 检查连接有效性
              if (isValidConnection(start, end)) {
                  createConnection(start, end);
              }
          }
          
          // 重置连接点颜色
          if (selectedConnector) {
              selectedConnector.style.background = '#fff';
          }
          
          cleanupTempLine();
          document.removeEventListener('mousemove', drawTempLine);
          document.removeEventListener('mouseup', endConnection);
      }
      
      // 检查连接是否有效
      function isValidConnection(start, end) {
          // 不能自连接
          if (start.dataset.nodeId === end.dataset.nodeId) {
              console.log('不能连接到同一个节点');
              return false;
          }
          
          // 不能重复连接
          const existingConnection = connections.some(conn => 
              (conn.start === start && conn.end === end) ||
              (conn.start === end && conn.end === start)
          );
          
          if (existingConnection) {
              console.log('连接已存在');
              return false;
          }
          
          return true;
      }
      
      // 创建连接
      function createConnection(start, end) {
          const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
          path.setAttribute('stroke', '#007bff');
          path.setAttribute('stroke-width', '3');
          path.setAttribute('fill', 'none');
          path.setAttribute('stroke-linecap', 'round');
          
          // 添加连接动画
          path.style.strokeDasharray = '5,5';
          path.style.animation = 'dash 1s linear infinite';
          
          const connection = { start, end, path };
          connections.push(connection);
          svg.appendChild(path);
          
          updateConnection(connection);
          updateConnectionCount();
          
          console.log(`创建连接: ${start.dataset.nodeId} -> ${end.dataset.nodeId}`);
      }
      
      // 获取连接点位置
      function getConnectorPosition(connector) {
          const rect = connector.getBoundingClientRect();
          const workspaceRect = document.querySelector('.workspace').getBoundingClientRect();
          
          return {
              x: rect.left + rect.width / 2 - workspaceRect.left,
              y: rect.top + rect.height / 2 - workspaceRect.top,
              position: connector.dataset.position
          };
      }
      
      // 计算贝塞尔曲线控制点
      function calculateControlPoints(start, end) {
          const dx = end.x - start.x;
          const dy = end.y - start.y;
          const distance = Math.sqrt(dx * dx + dy * dy);
          const offset = Math.min(distance * 0.4, 100);
          
          let c1x, c1y, c2x, c2y;
          
          // 根据连接点位置计算控制点
          switch(start.position) {
              case 'right':
                  c1x = start.x + offset;
                  c1y = start.y;
                  break;
              case 'left':
                  c1x = start.x - offset;
                  c1y = start.y;
                  break;
              case 'top':
                  c1x = start.x;
                  c1y = start.y - offset;
                  break;
              case 'bottom':
                  c1x = start.x;
                  c1y = start.y + offset;
                  break;
          }
          
          switch(end.position) {
              case 'right':
                  c2x = end.x + offset;
                  c2y = end.y;
                  break;
              case 'left':
                  c2x = end.x - offset;
                  c2y = end.y;
                  break;
              case 'top':
                  c2x = end.x;
                  c2y = end.y - offset;
                  break;
              case 'bottom':
                  c2x = end.x;
                  c2y = end.y + offset;
                  break;
          }
          
          return { c1x, c1y, c2x, c2y };
      }
      
      // 更新连接线
      function updateConnection(connection) {
          const startPos = getConnectorPosition(connection.start);
          const endPos = getConnectorPosition(connection.end);
          const { c1x, c1y, c2x, c2y } = calculateControlPoints(startPos, endPos);
          
          const d = `M ${startPos.x} ${startPos.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${endPos.x} ${endPos.y}`;
          connection.path.setAttribute('d', d);
      }
      
      // 启用拖拽功能
      function enableDrag(element) {
          let currentX, currentY, initialX, initialY;
          
          element.addEventListener('mousedown', (e) => {
              if (e.target === element) {
                  startDragging(e);
              }
          });
          
          function startDragging(e) {
              isDragging = true;
              element.style.zIndex = '1000';
              
              initialX = e.clientX - element.offsetLeft;
              initialY = e.clientY - element.offsetTop;
              
              document.addEventListener('mousemove', drag);
              document.addEventListener('mouseup', stopDragging);
          }
          
          function drag(e) {
              if (isDragging) {
                  e.preventDefault();
                  currentX = e.clientX - initialX;
                  currentY = e.clientY - initialY;
                  
                  // 边界检查
                  const workspace = document.querySelector('.workspace');
                  const maxX = workspace.clientWidth - element.offsetWidth;
                  const maxY = workspace.clientHeight - element.offsetHeight;
                  
                  currentX = Math.max(0, Math.min(currentX, maxX));
                  currentY = Math.max(0, Math.min(currentY, maxY));
                  
                  element.style.left = currentX + 'px';
                  element.style.top = currentY + 'px';
                  
                  // 更新所有相关连接
                  connections.forEach(conn => {
                      if (conn.start.closest('.node') === element || 
                          conn.end.closest('.node') === element) {
                          updateConnection(conn);
                      }
                  });
              }
          }
          
          function stopDragging() {
              isDragging = false;
              element.style.zIndex = '';
              document.removeEventListener('mousemove', drag);
              document.removeEventListener('mouseup', stopDragging);
          }
      }
      
      // 绘制临时连接线
      function drawTempLine(e) {
          if (!selectedConnector) return;
          
          if (!tempLine) {
              tempLine = document.createElementNS('http://www.w3.org/2000/svg', 'path');
              tempLine.setAttribute('stroke', '#6c757d');
              tempLine.setAttribute('stroke-width', '2');
              tempLine.setAttribute('stroke-dasharray', '5,5');
              tempLine.setAttribute('fill', 'none');
              svg.appendChild(tempLine);
          }
          
          const startPos = getConnectorPosition(selectedConnector);
          const workspaceRect = document.querySelector('.workspace').getBoundingClientRect();
          const endPos = { 
              x: e.clientX - workspaceRect.left, 
              y: e.clientY - workspaceRect.top,
              position: 'temp' 
          };
          
          const { c1x, c1y, c2x, c2y } = calculateControlPoints(startPos, endPos);
          const d = `M ${startPos.x} ${startPos.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${endPos.x} ${endPos.y}`;
          tempLine.setAttribute('d', d);
      }
      
      // 清理临时连接线
      function cleanupTempLine() {
          if (tempLine) {
              tempLine.remove();
              tempLine = null;
          }
          selectedConnector = null;
      }
      
      // 更新连接计数
      function updateConnectionCount() {
          connectionCount.textContent = `连接数: ${connections.length}`;
      }
      
      // 点击空白处取消连接操作
      document.addEventListener('mouseup', (e) => {
          if (!e.target.classList.contains('connector') && selectedConnector) {
              if (selectedConnector) {
                  selectedConnector.style.background = '#fff';
              }
              cleanupTempLine();
          }
      });
      
      // 添加CSS动画
      const style = document.createElement('style');
      style.textContent = `
          @keyframes dash {
              to {
                  stroke-dashoffset: -10;
              }
          }
      `;
      document.head.appendChild(style);
      
      console.log('可拖拽节点连接器初始化完成');
  </script>
</body>
</html>

延伸阅读

offsetHeightclientHeightscrollHeight 有什么区别

答案
  • 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查询和修改
  • 混合读写操作导致强制同步布局
  • 忽视事件监听器的性能影响

延伸阅读:

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()增强无障碍体验

常见失误:

  • 忽略平滑滚动可能被用户设置覆盖
  • 不考虑固定导航栏对滚动位置的影响
  • 过度使用可能造成用户困扰

延伸阅读:

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会被自动清空
  • 过度使用导致代码复杂化

延伸阅读:

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 读所有最终样式。

延伸阅读

History API ?

答案

核心概念

History API 是 HTML5 提供的浏览器历史记录管理接口,允许开发者通过 JavaScript 动态修改 URL 和历史记录,而无需页面刷新。常用方法包括 pushStatereplaceStategobackforward,配合 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.statehistory.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片段/参数值所有非字母数字字符%20URL参数、片段编码

实际示例:

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函数

延伸阅读:

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()就执行长时间任务
  • 混淆requestIdleCallbackrequestAnimationFrame的使用场景

延伸阅读:

如何判断页签是否为活跃状态

答案

判断当前浏览器页签(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('页面已隐藏')
}
})

延伸阅读

移动端如何实现上拉加载,下拉刷新?

答案

核心概念:

移动端上拉加载和下拉刷新是常见的交互模式,核心实现原理是通过监听触摸事件和滚动事件来检测用户手势。主要技术点包括:触摸事件处理(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 优化性能
  • 考虑原生下拉刷新的兼容性处理

常见失误:

  • 没有处理滚动边界情况
  • 忽视手势冲突和事件防止默认行为
  • 状态管理混乱导致重复触发

延伸阅读:

如何判断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 导致性能问题
  • 忽略边界情况和部分可见的处理
  • 不考虑动态内容和响应式布局的影响

延伸阅读:

如何检测网页空闲状态(一定时间内无操作)

答案

核心概念:

网页空闲状态检测是通过监听用户交互事件来判断用户是否在一定时间内没有操作页面。核心技术包括:事件监听(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 的使用

常见失误:

  • 过于频繁的事件监听导致性能问题
  • 忽略页面可见性变化的处理
  • 没有正确清理事件监听器和定时器

延伸阅读:

一次性渲染十万条数据还能保证页面不卡顿

答案

核心概念:

大量数据渲染性能优化的核心是避免阻塞主线程,主要技术方案包括:时间切片(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 的使用导致频繁回流
  • 没有考虑用户交互响应性
  • 缺乏对不同设备性能的适配

延伸阅读:

计算一段文本渲染之后的长度

答案

核心概念:

文本渲染长度计算是前端开发中的重要技术,主要用于文本截断、自适应布局、性能优化等场景。核心方法包括:临时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两种文本测量方法
  • 理解文本折叠的计算逻辑和性能优化
  • 了解字体加载对测量准确性的影响

加分项:

  • 实现高性能的批量文本测量
  • 考虑多行文本和复杂布局处理
  • 处理字体回退和国际化场景

常见失误:

  • 忽略字体加载完成前测量不准确
  • 没有考虑行高和字符间距影响
  • 缺乏缓存机制导致重复计算

延伸阅读: