跳到主要内容

HTML 编程实现✅

本主题涵盖 HTML/DOM 开发中常见的编程实现问题,包括 DOM 操作、事件处理、数据解析、UI 交互等实用工具函数的实现方法。

取出一个 html 树,并返回标签类型和各标签出现次数?

答案
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HTML 标签统计</title>
  </head>
  <body>
    <div id="app">
      <button onclick="domCount()">点击统计</button>
      <h1>HTML 标签统计</h1>
      <p>这是一个段落。</p>
      <ul>
        <li>列表项 1</li>
        <li>列表项 2</li>
      </ul>
      <iframe srcdoc="<h1>hello world</h1>"></iframe>
    </div>

    <script>
      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;

            if (tagName === "iframe") {
              try {
                // Access contentDocument for same-origin iframes
                const iframeDocument =
                  element.contentDocument || element.contentWindow.document;
                if (iframeDocument) {
                  // iframeDocument.body is the root element of the iframe
                  const iframeTagCounts = countTags(iframeDocument.body);

                  // Merge iframe tag counts into the main counts
                  for (const tag in iframeTagCounts) {
                    if (iframeTagCounts.hasOwnProperty(tag)) {
                      tagCounts[tag] =
                        (tagCounts[tag] || 0) + iframeTagCounts[tag];
                    }
                  }
                }
              } catch (e) {
                console.error("Cannot access cross-origin iframe content.");
              }
            } else {
              for (let i = element.children.length - 1; i >= 0; i--) {
                stack.push(element.children[i]);
              }
            }
          }
        }

        return tagCounts;
      }

      function domCount() {
         debugger
        const root = document.documentElement
        if (!root) {
          console.error("Could not find element with id 'app'");
        } else {
          const counts = countTags(root);
          console.log(counts);

          const resultElement = document.createElement("pre");
          resultElement.textContent = JSON.stringify(counts, null, 2);
          document.body.appendChild(resultElement);
        }
      }
    </script>

    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 20px;
      }

      h1 {
        color: #333;
      }

      button {
        margin-bottom: 20px;
        padding: 10px 15px;
        background-color: #007bff;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
      }

      button:hover {
        background-color: #0056b3;
      }

      pre {
        background-color: #f8f9fa;
        padding: 10px;
        border-radius: 5px;
      }
    </style>
  </body>
</html>

查找页面出现次数最多的 HTML 标签

答案

核心概念:

DOM 遍历统计是前端开发中的基础操作,通过遍历页面所有元素并统计标签类型,可以分析页面结构。核心思路是使用 document.getElementsByTagName('*') 获取所有元素,然后用对象记录每种标签的出现次数,最后找出频次最高的标签。

实际示例:

<!DOCTYPE html>
<html lang="zh-CN">
<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-area {
            border: 2px solid #007bff;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
            background-color: #f8f9fa;
        }
        button {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin: 10px 5px;
        }
        button:hover {
            background-color: #0056b3;
        }
        .results {
            background-color: #d4edda;
            border: 1px solid #c3e6cb;
            padding: 15px;
            border-radius: 5px;
            margin-top: 15px;
        }
        .test-content {
            margin: 15px 0;
        }
        pre {
            background-color: #f8f9fa;
            padding: 10px;
            border-radius: 5px;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <h1>查找页面出现次数最多的HTML标签</h1>
    
    <div class="demo-area">
        <h2>测试内容区域</h2>
        <div class="test-content">
            <p>这是一个段落1</p>
            <p>这是一个段落2</p>
            <p>这是一个段落3</p>
            <div>
                <span>嵌套span1</span>
                <span>嵌套span2</span>
                <ul>
                    <li>列表项1</li>
                    <li>列表项2</li>
                    <li>列表项3</li>
                </ul>
            </div>
            <table>
                <tr><td>表格单元格1</td><td>表格单元格2</td></tr>
                <tr><td>表格单元格3</td><td>表格单元格4</td></tr>
            </table>
        </div>
        
        <button onclick="findMostFrequentTag()">统计页面标签</button>
        <button onclick="showDetailedStats()">显示详细统计</button>
        <button onclick="clearResults()">清除结果</button>
    </div>
    
    <div id="results"></div>

    <script>
        // 基础实现:查找出现次数最多的HTML标签
        function findMostFrequentTag() {
            const allElements = document.getElementsByTagName('*');
            const tagCount = {};
            
            // 统计每种标签的数量
            for (let i = 0; i < allElements.length; i++) {
                const tagName = allElements[i].tagName.toLowerCase();
                tagCount[tagName] = (tagCount[tagName] || 0) + 1;
            }
            
            // 找出出现次数最多的标签
            let mostFrequentTag = null;
            let maxCount = 0;
            
            for (const tag in tagCount) {
                if (tagCount[tag] > maxCount) {
                    mostFrequentTag = tag;
                    maxCount = tagCount[tag];
                }
            }
            
            displayResult({
                tag: mostFrequentTag,
                count: maxCount,
                total: allElements.length,
                types: Object.keys(tagCount).length
            });
        }
        
        // 现代化实现:使用现代API和更详细的统计
        function showDetailedStats() {
            const allElements = [...document.querySelectorAll('*')];
            
            // 使用 Map 进行统计
            const tagStats = allElements.reduce((stats, element) => {
                const tagName = element.tagName.toLowerCase();
                if (!stats.has(tagName)) {
                    stats.set(tagName, {
                        count: 0,
                        examples: []
                    });
                }
                
                const tagInfo = stats.get(tagName);
                tagInfo.count++;
                
                // 保存前3个示例
                if (tagInfo.examples.length < 3) {
                    tagInfo.examples.push({
                        id: element.id || '(无id)',
                        className: element.className || '(无class)',
                        text: element.textContent?.slice(0, 30) || ''
                    });
                }
                
                return stats;
            }, new Map());
            
            // 找出最频繁的标签
            const [mostFrequentTag, maxInfo] = [...tagStats.entries()]
                .reduce((max, current) => 
                    current[1].count > max[1].count ? current : max
                );
            
            displayDetailedResult({
                mostFrequent: { tag: mostFrequentTag, ...maxInfo },
                allStats: Object.fromEntries(tagStats),
                summary: {
                    totalElements: allElements.length,
                    uniqueTags: tagStats.size
                }
            });
        }
        
        // 显示基础结果
        function displayResult(result) {
            const resultsDiv = document.getElementById('results');
            resultsDiv.innerHTML = `
                <div class="results">
                    <h3>统计结果</h3>
                    <p><strong>最常见的标签:</strong> &lt;${result.tag}&gt;</p>
                    <p><strong>出现次数:</strong> ${result.count} 次</p>
                    <p><strong>页面总元素数:</strong> ${result.total} 个</p>
                    <p><strong>不同标签类型:</strong> ${result.types} 种</p>
                </div>
            `;
        }
        
        // 显示详细结果
        function displayDetailedResult(result) {
            const resultsDiv = document.getElementById('results');
            
            const statsHtml = Object.entries(result.allStats)
                .sort(([,a], [,b]) => b.count - a.count)
                .slice(0, 10) // 只显示前10种
                .map(([tag, info]) => 
                    `<tr><td>&lt;${tag}&gt;</td><td>${info.count}</td></tr>`
                ).join('');
            
            resultsDiv.innerHTML = `
                <div class="results">
                    <h3>详细统计结果</h3>
                    <p><strong>最常见标签:</strong> &lt;${result.mostFrequent.tag}&gt; (${result.mostFrequent.count} 次)</p>
                    
                    <h4>Top 10 标签统计:</h4>
                    <table style="width: 100%; border-collapse: collapse;">
                        <thead>
                            <tr style="background-color: #e9ecef;">
                                <th style="border: 1px solid #ddd; padding: 8px;">标签</th>
                                <th style="border: 1px solid #ddd; padding: 8px;">数量</th>
                            </tr>
                        </thead>
                        <tbody>
                            ${statsHtml}
                        </tbody>
                    </table>
                    
                    <p style="margin-top: 15px;">
                        <strong>总计:</strong> ${result.summary.totalElements} 个元素,
                        ${result.summary.uniqueTags} 种不同标签
                    </p>
                </div>
            `;
        }
        
        // 清除结果
        function clearResults() {
            document.getElementById('results').innerHTML = '';
        }
        
        // 页面加载完成提示
        console.log('页面加载完成,可以开始统计HTML标签');
        console.log('尝试运行 findMostFrequentTag() 函数');
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 掌握DOM遍历的基本方法
  • 理解不同遍历方式的性能差异
  • 能够处理边界情况和异常场景

加分项:

  • 提及现代化的实现方式(Set、Map、解构等)
  • 考虑性能优化和内存管理
  • 支持自定义过滤条件和详细分析

常见失误:

  • 忘记处理大小写统一问题
  • 不考虑隐藏元素的处理
  • 缺少错误处理和边界检查

延伸阅读:

前端如何快速获取页面 URL query 参数

答案

核心概念:

URL 查询参数解析是前端开发的常见需求,主要有三种实现方式:现代 URLSearchParams API、手动字符串解析、第三方库。URLSearchParams 是最推荐的方式,提供了完整的查询参数操作接口。对于复杂场景如嵌套对象、数组等,需要自定义解析逻辑。

实际示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>URL查询参数解析</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            line-height: 1.6;
        }
        .demo-section {
            border: 2px solid #28a745;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
            background-color: #f8fff9;
        }
        .input-group {
            margin: 15px 0;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        input[type="text"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        button {
            background-color: #28a745;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin: 5px;
        }
        button:hover {
            background-color: #218838;
        }
        .method-tabs {
            display: flex;
            margin-bottom: 20px;
        }
        .tab {
            padding: 10px 20px;
            background-color: #e9ecef;
            border: 1px solid #ddd;
            cursor: pointer;
            border-radius: 5px 5px 0 0;
            margin-right: 5px;
        }
        .tab.active {
            background-color: #007bff;
            color: white;
        }
        .results {
            background-color: #d1ecf1;
            border: 1px solid #bee5eb;
            padding: 15px;
            border-radius: 5px;
            margin-top: 15px;
        }
        pre {
            background-color: #f8f9fa;
            padding: 10px;
            border-radius: 5px;
            overflow-x: auto;
            border: 1px solid #e9ecef;
        }
        .current-url {
            background-color: #fff3cd;
            border: 1px solid #ffeaa7;
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 15px;
        }
    </style>
</head>
<body>
    <h1>前端URL查询参数解析</h1>
    
    <div class="current-url">
        <strong>当前页面URL:</strong> <span id="currentUrl"></span>
    </div>
    
    <div class="demo-section">
        <h2>测试URL输入</h2>
        <div class="input-group">
            <label for="testUrl">输入测试URL:</label>
            <input type="text" id="testUrl" 
                   value="https://example.com?name=张三&age=25&city=北京&tags[]=javascript&tags[]=react&user.id=123&user.name=John&data={'theme':'dark'}">
        </div>
        
        <div class="method-tabs">
            <div class="tab active" onclick="switchMethod('urlsearchparams')">URLSearchParams API</div>
            <div class="tab" onclick="switchMethod('manual')">手动解析</div>
            <div class="tab" onclick="switchMethod('advanced')">高级解析</div>
        </div>
        
        <button onclick="parseCurrentMethod()">解析参数</button>
        <button onclick="updateCurrentUrl()">使用当前页面URL</button>
        <button onclick="setComplexUrl()">设置复杂URL</button>
        <button onclick="clearResults()">清除结果</button>
    </div>
    
    <div id="results"></div>

    <script>
        let currentMethod = 'urlsearchparams';
        
        // 初始化页面
        document.addEventListener('DOMContentLoaded', () => {
            document.getElementById('currentUrl').textContent = window.location.href;
        });
        
        // 切换解析方法
        function switchMethod(method) {
            currentMethod = method;
            document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
            event.target.classList.add('active');
        }
        
        // 1. URLSearchParams API 方式(推荐)
        function parseWithURLSearchParams(url) {
            try {
                const urlObj = new URL(url);
                const params = new URLSearchParams(urlObj.search);
                const result = {};
                
                // 处理同名参数(转为数组)
                for (const [key, value] of params) {
                    if (result[key]) {
                        if (Array.isArray(result[key])) {
                            result[key].push(value);
                        } else {
                            result[key] = [result[key], value];
                        }
                    } else {
                        result[key] = value;
                    }
                }
                
                return {
                    method: 'URLSearchParams API',
                    success: true,
                    data: result,
                    details: {
                        searchString: urlObj.search,
                        paramCount: params.size
                    }
                };
            } catch (error) {
                return { method: 'URLSearchParams API', success: false, error: error.message };
            }
        }
        
        // 2. 手动解析方式
        function parseManually(url) {
            try {
                const params = {};
                const queryString = url.split('?')[1];
                
                if (!queryString) {
                    return { method: '手动解析', success: true, data: {}, details: { queryString: '' } };
                }
                
                const pairs = queryString.split('&');
                pairs.forEach(pair => {
                    const [key, value = ''] = pair.split('=');
                    if (key) {
                        const decodedKey = decodeURIComponent(key);
                        const decodedValue = decodeURIComponent(value);
                        
                        // 处理同名参数
                        if (params[decodedKey]) {
                            if (Array.isArray(params[decodedKey])) {
                                params[decodedKey].push(decodedValue);
                            } else {
                                params[decodedKey] = [params[decodedKey], decodedValue];
                            }
                        } else {
                            params[decodedKey] = decodedValue;
                        }
                    }
                });
                
                return {
                    method: '手动解析',
                    success: true,
                    data: params,
                    details: {
                        queryString,
                        pairCount: pairs.length
                    }
                };
            } catch (error) {
                return { method: '手动解析', success: false, error: error.message };
            }
        }
        
        // 3. 高级解析(支持嵌套对象和数组)
        function parseAdvanced(url) {
            try {
                const params = {};
                const queryString = url.split('?')[1];
                
                if (!queryString) {
                    return { method: '高级解析', success: true, data: {}, details: { queryString: '' } };
                }
                
                const pairs = queryString.split('&');
                pairs.forEach(pair => {
                    const [key, value = ''] = pair.split('=');
                    if (key) {
                        setNestedValue(params, key, decodeURIComponent(value));
                    }
                });
                
                return {
                    method: '高级解析',
                    success: true,
                    data: params,
                    details: {
                        queryString,
                        supportFeatures: ['嵌套对象', '数组', 'JSON解析']
                    }
                };
            } catch (error) {
                return { method: '高级解析', success: false, error: error.message };
            }
        }
        
        // 设置嵌套值(支持 user.name 和 tags[] 语法)
        function setNestedValue(obj, key, value) {
            // 处理数组语法: tags[]=value1&tags[]=value2
            if (key.endsWith('[]')) {
                const arrayKey = key.slice(0, -2);
                if (!obj[arrayKey]) obj[arrayKey] = [];
                obj[arrayKey].push(parseValue(value));
                return;
            }
            
            // 处理嵌套对象语法: user.name=John&user.age=30
            if (key.includes('.')) {
                const keys = key.split('.');
                let current = obj;
                
                for (let i = 0; i < keys.length - 1; i++) {
                    const k = keys[i];
                    if (!current[k] || typeof current[k] !== 'object') {
                        current[k] = {};
                    }
                    current = current[k];
                }
                
                const lastKey = keys[keys.length - 1];
                current[lastKey] = parseValue(value);
            } else {
                // 普通参数
                if (obj[key]) {
                    // 转换为数组
                    if (Array.isArray(obj[key])) {
                        obj[key].push(parseValue(value));
                    } else {
                        obj[key] = [obj[key], parseValue(value)];
                    }
                } else {
                    obj[key] = parseValue(value);
                }
            }
        }
        
        // 尝试解析值(JSON、数字、布尔值)
        function parseValue(value) {
            // 尝试解析JSON
            if (value.startsWith('{') || value.startsWith('[')) {
                try {
                    return JSON.parse(value);
                } catch (e) {
                    return value;
                }
            }
            
            // 尝试解析数字
            if (/^\d+$/.test(value)) {
                return parseInt(value, 10);
            }
            
            if (/^\d*\.\d+$/.test(value)) {
                return parseFloat(value);
            }
            
            // 解析布尔值
            if (value === 'true') return true;
            if (value === 'false') return false;
            
            return value;
        }
        
        // 执行当前选择的解析方法
        function parseCurrentMethod() {
            const url = document.getElementById('testUrl').value;
            if (!url) {
                alert('请输入URL');
                return;
            }
            
            let result;
            switch (currentMethod) {
                case 'urlsearchparams':
                    result = parseWithURLSearchParams(url);
                    break;
                case 'manual':
                    result = parseManually(url);
                    break;
                case 'advanced':
                    result = parseAdvanced(url);
                    break;
            }
            
            displayResult(result, url);
        }
        
        // 显示解析结果
        function displayResult(result, originalUrl) {
            const resultsDiv = document.getElementById('results');
            
            if (!result.success) {
                resultsDiv.innerHTML = `
                    <div class="results">
                        <h3>解析失败</h3>
                        <p><strong>方法:</strong> ${result.method}</p>
                        <p><strong>错误:</strong> ${result.error}</p>
                    </div>
                `;
                return;
            }
            
            const dataJson = JSON.stringify(result.data, null, 2);
            const detailsJson = JSON.stringify(result.details, null, 2);
            
            resultsDiv.innerHTML = `
                <div class="results">
                    <h3>解析结果 - ${result.method}</h3>
                    
                    <h4>原始URL:</h4>
                    <pre>${originalUrl}</pre>
                    
                    <h4>解析后的参数:</h4>
                    <pre>${dataJson}</pre>
                    
                    <h4>解析详情:</h4>
                    <pre>${detailsJson}</pre>
                    
                    <h4>使用示例:</h4>
                    <pre>// 获取特定参数
const name = result.data.name; // "${result.data.name || '未找到'}"
const age = result.data.age;   // ${result.data.age || '未找到'}

// 遍历所有参数
Object.entries(result.data).forEach(([key, value]) => {
    console.log(\`\${key}: \${value}\`);
});</pre>
                </div>
            `;
        }
        
        // 使用当前页面URL
        function updateCurrentUrl() {
            document.getElementById('testUrl').value = window.location.href;
        }
        
        // 设置复杂URL示例
        function setComplexUrl() {
            const complexUrl = 'https://shop.example.com/search?q=手机&category=electronics&price.min=1000&price.max=5000&brands[]=apple&brands[]=samsung&sort=price&filter.color=black&filter.storage=128GB&config={"theme":"dark","lang":"zh"}';
            document.getElementById('testUrl').value = complexUrl;
        }
        
        // 清除结果
        function clearResults() {
            document.getElementById('results').innerHTML = '';
        }
        
        // 工具函数:快速解析当前页面参数(在控制台中使用)
        window.getPageParams = function() {
            return parseWithURLSearchParams(window.location.href).data;
        };
        
        console.log('URL参数解析工具已加载');
        console.log('在控制台中使用 getPageParams() 快速获取当前页面参数');
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 了解URLSearchParams API的基本用法
  • 掌握手动解析查询字符串的方法
  • 理解URL编码和解码的重要性

加分项:

  • 支持复杂数据结构(嵌套对象、数组)
  • 考虑类型转换和JSON解析
  • 提供完整的URL操作工具集

常见失误:

  • 忘记URL解码导致特殊字符问题
  • 不处理同名参数的多值情况
  • 缺少错误处理和边界检查

延伸阅读:

实现文本溢出 popover 效果

答案

核心概念:

文本溢出悬浮提示是一种常见的UI交互模式,当文本内容超出容器宽度时,通过悬浮层展示完整内容。核心技术包括溢出检测(比较scrollWidth和offsetWidth)、动态创建弹层、定位计算、事件管理等。现代实现可以利用HTML5的popover API或自定义实现,需要考虑响应式、无障碍性和性能优化。

实际示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文本溢出Popover效果</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            line-height: 1.6;
        }
        
        .demo-section {
            border: 2px solid #6f42c1;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
            background-color: #f8f6ff;
        }
        
        /* 文本截断样式 */
        .text-truncate {
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            cursor: help;
            padding: 8px 12px;
            background-color: #e9ecef;
            border: 1px solid #ced4da;
            border-radius: 4px;
            margin: 10px 0;
            display: block;
        }
        
        /* 不同宽度的容器 */
        .container-small { width: 200px; }
        .container-medium { width: 300px; }
        .container-large { width: 400px; }
        
        /* Popover样式 */
        .text-popover {
            position: absolute;
            z-index: 1000;
            background-color: #333;
            color: #fff;
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 14px;
            max-width: 300px;
            word-wrap: break-word;
            opacity: 0;
            transform: scale(0.95);
            transition: all 0.2s ease;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            pointer-events: none;
        }
        
        .text-popover.visible {
            opacity: 1;
            transform: scale(1);
            pointer-events: auto;
        }
        
        /* 现代 Popover API 样式 */
        .native-popover {
            padding: 8px 12px;
            background: #333;
            color: #fff;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            max-width: 300px;
        }
        
        /* 控制按钮 */
        .controls {
            margin: 20px 0;
            padding: 15px;
            background-color: #fff;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        
        button {
            background-color: #6f42c1;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            margin: 5px;
            font-size: 14px;
        }
        
        button:hover {
            background-color: #5a32a3;
        }
        
        .trigger-mode {
            margin: 10px 0;
        }
        
        .trigger-mode label {
            margin-left: 5px;
        }
        
        .status {
            background-color: #d1ecf1;
            border: 1px solid #bee5eb;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <h1>文本溢出 Popover 效果演示</h1>
    
    <div class="controls">
        <h3>设置</h3>
        <div class="trigger-mode">
            <strong>触发方式:</strong>
            <label><input type="radio" name="trigger" value="hover" checked> 鼠标悬停</label>
            <label><input type="radio" name="trigger" value="click"> 点击</label>
        </div>
        <button onclick="addTestContent()">添加测试内容</button>
        <button onclick="toggleNativePopover()">切换原生Popover API</button>
        <button onclick="clearContent()">清除内容</button>
    </div>
    
    <div class="status">
        <span id="status">自定义Popover已启用 | 当前触发方式: 悬停</span>
    </div>
    
    <div class="demo-section">
        <h2>基础演示</h2>
        <p>将鼠标悬停在下面的文本上,或根据设置点击来查看完整内容:</p>
        
        <div class="container-small">
            <span class="text-truncate" data-full-text="这是一段很长很长很长很长很长很长很长很长的文本内容,用来演示文本溢出时的popover效果">
                这是一段很长很长很长很长很长很长很长很长的文本内容,用来演示文本溢出时的popover效果
            </span>
        </div>
        
        <div class="container-medium">
            <span class="text-truncate" data-full-text="JavaScript是一种高级的、解释型的编程语言,具有函数优先的特点">
                JavaScript是一种高级的、解释型的编程语言,具有函数优先的特点
            </span>
        </div>
        
        <div class="container-large">
            <span class="text-truncate" data-full-text="前端开发涉及HTML、CSS、JavaScript等多种技术栈,需要综合运用各种技能来构建用户界面和用户体验">
                前端开发涉及HTML、CSS、JavaScript等多种技术栈,需要综合运用各种技能来构建用户界面和用户体验
            </span>
        </div>
    </div>
    
    <div id="dynamicContent" class="demo-section">
        <h2>动态内容测试</h2>
        <p>点击"添加测试内容"按钮来测试动态生成的内容:</p>
    </div>

    <script>
        // 自定义 Popover 实现
        class TextOverflowPopover {
            constructor(options = {}) {
                this.options = {
                    trigger: 'hover',
                    className: 'text-popover',
                    showDelay: 300,
                    hideDelay: 100,
                    ...options
                };
                this.activePopover = null;
                this.showTimer = null;
                this.hideTimer = null;
                this.elements = new Set();
            }
            
            init(selector = '.text-truncate') {
                // 清理之前的监听器
                this.cleanup();
                
                const elements = document.querySelectorAll(selector);
                elements.forEach(element => this.setupElement(element));
            }
            
            setupElement(element) {
                // 检查是否溢出
                if (!this.isOverflowing(element)) {
                    element.style.cursor = 'default';
                    return;
                }
                
                element.style.cursor = 'help';
                this.elements.add(element);
                
                // 绑定事件
                if (this.options.trigger === 'hover') {
                    element.addEventListener('mouseenter', (e) => this.showPopover(e.target));
                    element.addEventListener('mouseleave', () => this.hidePopover());
                } else {
                    element.addEventListener('click', (e) => this.showPopover(e.target));
                }
            }
            
            isOverflowing(element) {
                return element.scrollWidth > element.offsetWidth;
            }
            
            showPopover(element) {
                clearTimeout(this.hideTimer);
                
                this.showTimer = setTimeout(() => {
                    this.hidePopover(true);
                    
                    const popover = this.createPopover(element);
                    document.body.appendChild(popover);
                    this.positionPopover(popover, element);
                    
                    // 显示动画
                    requestAnimationFrame(() => {
                        popover.classList.add('visible');
                    });
                    
                    this.activePopover = popover;
                    
                    // 添加全局点击关闭
                    this.addGlobalListeners();
                }, this.options.showDelay);
            }
            
            createPopover(element) {
                const popover = document.createElement('div');
                popover.className = this.options.className;
                
                const fullText = element.dataset.fullText || element.textContent;
                popover.textContent = fullText;
                
                return popover;
            }
            
            positionPopover(popover, target) {
                const targetRect = target.getBoundingClientRect();
                const popoverRect = popover.getBoundingClientRect();
                
                let left = targetRect.left + (targetRect.width - popoverRect.width) / 2;
                let top = targetRect.top - popoverRect.height - 8;
                
                // 边界检查
                if (top < 0) {
                    top = targetRect.bottom + 8; // 放到下方
                }
                
                left = Math.max(8, Math.min(left, window.innerWidth - popoverRect.width - 8));
                
                popover.style.left = `${left + window.scrollX}px`;
                popover.style.top = `${top + window.scrollY}px`;
            }
            
            hidePopover(immediate = false) {
                clearTimeout(this.showTimer);
                
                if (!this.activePopover) return;
                
                if (immediate) {
                    this.activePopover.remove();
                    this.activePopover = null;
                } else {
                    this.hideTimer = setTimeout(() => {
                        if (this.activePopover) {
                            this.activePopover.remove();
                            this.activePopover = null;
                        }
                    }, this.options.hideDelay);
                }
            }
            
            addGlobalListeners() {
                const handleClickOutside = (e) => {
                    if (this.activePopover && !this.activePopover.contains(e.target)) {
                        this.hidePopover(true);
                        document.removeEventListener('click', handleClickOutside);
                    }
                };
                
                const handleKeyDown = (e) => {
                    if (e.key === 'Escape') {
                        this.hidePopover(true);
                        document.removeEventListener('keydown', handleKeyDown);
                    }
                };
                
                document.addEventListener('click', handleClickOutside);
                document.addEventListener('keydown', handleKeyDown);
            }
            
            cleanup() {
                this.hidePopover(true);
                this.elements.clear();
            }
            
            updateTrigger(newTrigger) {
                this.options.trigger = newTrigger;
                this.init();
            }
        }
        
        // 原生 Popover API 支持
        class NativePopoverManager {
            init() {
                if (!('showPopover' in HTMLElement.prototype)) {
                    return false;
                }
                
                const elements = document.querySelectorAll('.text-truncate');
                elements.forEach(element => this.setupNativePopover(element));
                return true;
            }
            
            setupNativePopover(element) {
                if (!this.isOverflowing(element)) return;
                
                // 清理可能存在的旧popover
                const oldPopoverId = element.getAttribute('popovertarget');
                if (oldPopoverId) {
                    const oldPopover = document.getElementById(oldPopoverId);
                    oldPopover?.remove();
                }
                
                const popoverId = `popover-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
                const popover = document.createElement('div');
                
                popover.id = popoverId;
                popover.popover = 'auto';
                popover.className = 'native-popover';
                
                const fullText = element.dataset.fullText || element.textContent;
                popover.textContent = fullText;
                
                document.body.appendChild(popover);
                element.setAttribute('popovertarget', popoverId);
                element.style.cursor = 'help';
            }
            
            isOverflowing(element) {
                return element.scrollWidth > element.offsetWidth;
            }
            
            cleanup() {
                // 清理所有原生popover
                document.querySelectorAll('[popover]').forEach(popover => {
                    if (popover.id.startsWith('popover-')) {
                        popover.remove();
                    }
                });
                
                document.querySelectorAll('[popovertarget]').forEach(element => {
                    element.removeAttribute('popovertarget');
                });
            }
        }
        
        // 管理器实例
        let popoverManager = new TextOverflowPopover();
        let nativeManager = new NativePopoverManager();
        let isUsingNative = false;
        
        // 初始化
        document.addEventListener('DOMContentLoaded', () => {
            popoverManager.init();
            updateStatus();
        });
        
        // 触发方式切换
        document.addEventListener('change', (e) => {
            if (e.target.name === 'trigger') {
                const trigger = e.target.value;
                if (!isUsingNative) {
                    popoverManager.updateTrigger(trigger);
                }
                updateStatus();
            }
        });
        
        // 添加测试内容
        function addTestContent() {
            const container = document.getElementById('dynamicContent');
            const testTexts = [
                '这是动态添加的超长文本内容,用来测试 Popover 在动态内容上的工作效果',
                'React是一个用于构建用户界面的JavaScript库,由Facebook开发并维护',
                'Vue.js是一套用于构建用户界面的渐进式JavaScript框架,易学易用',
                'Node.js是一个基于Chrome V8引擎的JavaScript运行时环境'
            ];
            
            const randomText = testTexts[Math.floor(Math.random() * testTexts.length)];
            const div = document.createElement('div');
            div.className = 'container-medium';
            div.style.marginTop = '10px';
            
            div.innerHTML = `
                <span class="text-truncate" data-full-text="${randomText}">
                    ${randomText}
                </span>
            `;
            
            container.appendChild(div);
            
            // 重新初始化
            if (isUsingNative) {
                nativeManager.init();
            } else {
                popoverManager.init();
            }
        }
        
        // 切换原生/自定义 Popover
        function toggleNativePopover() {
            if (isUsingNative) {
                // 切换到自定义
                nativeManager.cleanup();
                popoverManager.init();
                isUsingNative = false;
            } else {
                // 切换到原生
                if (nativeManager.init()) {
                    popoverManager.cleanup();
                    isUsingNative = true;
                } else {
                    alert('您的浏览器不支持原生 Popover API');
                    return;
                }
            }
            updateStatus();
        }
        
        // 清除动态内容
        function clearContent() {
            const container = document.getElementById('dynamicContent');
            const children = container.children;
            for (let i = children.length - 1; i >= 0; i--) {
                if (children[i].className === 'container-medium') {
                    children[i].remove();
                }
            }
        }
        
        // 更新状态显示
        function updateStatus() {
            const status = document.getElementById('status');
            const trigger = document.querySelector('input[name="trigger"]:checked').value;
            const triggerText = trigger === 'hover' ? '悬停' : '点击';
            const modeText = isUsingNative ? '原生Popover API' : '自定义Popover';
            
            status.textContent = `${modeText}已启用 | 当前触发方式: ${triggerText}`;
        }
        
        // 工具函数:检查浏览器支持
        window.checkPopoverSupport = function() {
            const hasNativeSupport = 'showPopover' in HTMLElement.prototype;
            console.log('原生 Popover API 支持:', hasNativeSupport);
            return hasNativeSupport;
        };
        
        console.log('文本溢出 Popover 演示已加载');
        console.log('使用 checkPopoverSupport() 检查浏览器支持情况');
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 掌握文本溢出检测的基本方法(scrollWidth vs offsetWidth)
  • 了解Popover的定位计算和边界处理
  • 理解事件管理和内存泄漏预防

加分项:

  • 使用现代Popover API提升用户体验
  • 实现自动化检测和响应式处理
  • 考虑无障碍性和键盘操作支持

常见失误:

  • 忽略边界检测导致popover超出视口
  • 没有正确处理事件清理造成内存泄漏
  • 缺乏响应式处理和动态内容适配

延伸阅读:

不使用 setTimeout 来实现 setInterval

答案

核心概念:

实现自定义的 setInterval 主要有两种方案:使用 requestAnimationFrame 或递归调用 setTimeout。requestAnimationFrame 方案可以与浏览器的刷新率同步,提供更流畅的动画效果,但精度相对较低;setTimeout 递归方案可以提供更精确的时间间隔控制。两种方案都需要处理定时器的启停管理和错误处理。

实际示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>自定义setInterval实现</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            line-height: 1.6;
        }
        
        .demo-section {
            border: 2px solid #fd7e14;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
            background-color: #fff8f0;
        }
        
        .controls {
            background-color: #f8f9fa;
            padding: 15px;
            border: 1px solid #e9ecef;
            border-radius: 5px;
            margin: 15px 0;
        }
        
        .timer-display {
            background-color: #d1ecf1;
            border: 1px solid #bee5eb;
            padding: 15px;
            border-radius: 5px;
            margin: 15px 0;
            font-family: 'Courier New', monospace;
            font-size: 16px;
        }
        
        button {
            background-color: #fd7e14;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            margin: 5px;
        }
        
        button:hover {
            background-color: #e86a0f;
        }
        
        button:disabled {
            background-color: #6c757d;
            cursor: not-allowed;
        }
        
        .method-tabs {
            display: flex;
            margin: 15px 0;
        }
        
        .tab {
            padding: 10px 20px;
            background-color: #e9ecef;
            border: 1px solid #ddd;
            cursor: pointer;
            border-radius: 5px 5px 0 0;
            margin-right: 5px;
        }
        
        .tab.active {
            background-color: #fd7e14;
            color: white;
        }
        
        .logs {
            background-color: #f8f9fa;
            border: 1px solid #e9ecef;
            padding: 15px;
            border-radius: 5px;
            height: 200px;
            overflow-y: auto;
            font-family: 'Courier New', monospace;
            font-size: 14px;
        }
        
        .performance-stats {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 10px;
            margin: 15px 0;
        }
        
        .stat-item {
            background-color: #fff;
            border: 1px solid #dee2e6;
            padding: 10px;
            border-radius: 5px;
            text-align: center;
        }
        
        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: #fd7e14;
        }
        
        .stat-label {
            font-size: 12px;
            color: #6c757d;
        }
        
        input[type="number"] {
            width: 80px;
            padding: 5px;
            border: 1px solid #ced4da;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <h1>不使用setTimeout实现setInterval</h1>
    
    <div class="demo-section">
        <h2>实现方式对比</h2>
        
        <div class="method-tabs">
            <div class="tab active" onclick="switchMethod('raf')">requestAnimationFrame</div>
            <div class="tab" onclick="switchMethod('timeout')">setTimeout递归</div>
            <div class="tab" onclick="switchMethod('worker')">Web Worker</div>
        </div>
        
        <div class="controls">
            <label>间隔时间 (ms): <input type="number" id="interval" value="1000" min="100" max="5000"></label>
            <label>最大执行次数: <input type="number" id="maxCount" value="10" min="1" max="100"></label>
            <button id="startBtn" onclick="startTimer()">开始</button>
            <button id="stopBtn" onclick="stopTimer()" disabled>停止</button>
            <button onclick="clearLogs()">清除日志</button>
        </div>
        
        <div class="timer-display">
            <div>当前方法: <span id="currentMethod">requestAnimationFrame</span></div>
            <div>状态: <span id="status">停止</span></div>
            <div>已执行: <span id="executed">0</span></div>
            <div>平均间隔: <span id="avgInterval">0</span> ms</div>
        </div>
        
        <div class="performance-stats">
            <div class="stat-item">
                <div class="stat-value" id="totalTime">0</div>
                <div class="stat-label">总运行时间 (ms)</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="precision">0</div>
                <div class="stat-label">精度偏差 (ms)</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="performance">0</div>
                <div class="stat-label">性能评分</div>
            </div>
        </div>
        
        <div class="logs" id="logs"></div>
    </div>

    <script>
        // 全局状态
        let currentMethod = 'raf';
        let activeTimer = null;
        let executionCount = 0;
        let startTime = 0;
        let lastExecutionTime = 0;
        let intervalSum = 0;
        let workerInstance = null;
        
        // 1. RequestAnimationFrame 实现
        class RAFInterval {
            constructor() {
                this.timers = new Map();
                this.timerId = 0;
            }
            
            setInterval(callback, interval) {
                const id = ++this.timerId;
                let startTime = performance.now();
                let lastExecution = startTime;
                
                const loop = (currentTime) => {
                    const timer = this.timers.get(id);
                    if (!timer) return;
                    
                    const elapsed = currentTime - lastExecution;
                    
                    if (elapsed >= interval) {
                        try {
                            callback();
                            lastExecution = currentTime;
                        } catch (error) {
                            console.error('RAF Interval error:', error);
                            this.clearInterval(id);
                            return;
                        }
                    }
                    
                    timer.rafId = requestAnimationFrame(loop);
                    this.timers.set(id, timer);
                };
                
                const rafId = requestAnimationFrame(loop);
                this.timers.set(id, { rafId, callback, interval });
                
                return id;
            }
            
            clearInterval(id) {
                const timer = this.timers.get(id);
                if (timer) {
                    cancelAnimationFrame(timer.rafId);
                    this.timers.delete(id);
                }
            }
        }
        
        // 2. setTimeout 递归实现
        class TimeoutInterval {
            constructor() {
                this.timers = new Map();
                this.timerId = 0;
            }
            
            setInterval(callback, interval) {
                const id = ++this.timerId;
                let executionCount = 0;
                let startTime = Date.now();
                
                const execute = () => {
                    try {
                        callback();
                        executionCount++;
                    } catch (error) {
                        console.error('Timeout Interval error:', error);
                        this.clearInterval(id);
                        return;
                    }
                    
                    // 时间补偿机制
                    const expectedNext = startTime + (executionCount + 1) * interval;
                    const currentTime = Date.now();
                    const delay = Math.max(0, expectedNext - currentTime);
                    
                    const timeoutId = setTimeout(execute, delay);
                    this.timers.set(id, { timeoutId, callback, interval });
                };
                
                const timeoutId = setTimeout(execute, interval);
                this.timers.set(id, { timeoutId, callback, interval });
                
                return id;
            }
            
            clearInterval(id) {
                const timer = this.timers.get(id);
                if (timer) {
                    clearTimeout(timer.timeoutId);
                    this.timers.delete(id);
                }
            }
        }
        
        // 3. Web Worker 实现
        class WorkerInterval {
            constructor() {
                this.callbacks = new Map();
                this.timerId = 0;
                this.worker = null;
                this.initWorker();
            }
            
            initWorker() {
                const workerScript = `
                    let timers = new Map();
                    
                    self.onmessage = function(e) {
                        const { type, id, interval } = e.data;
                        
                        switch(type) {
                            case 'start':
                                const timer = setInterval(() => {
                                    self.postMessage({ type: 'tick', id });
                                }, interval);
                                timers.set(id, timer);
                                break;
                                
                            case 'stop':
                                const existingTimer = timers.get(id);
                                if (existingTimer) {
                                    clearInterval(existingTimer);
                                    timers.delete(id);
                                }
                                break;
                        }
                    };
                `;
                
                const blob = new Blob([workerScript], { type: 'application/javascript' });
                this.worker = new Worker(URL.createObjectURL(blob));
                
                this.worker.onmessage = (e) => {
                    const { type, id } = e.data;
                    if (type === 'tick') {
                        const callback = this.callbacks.get(id);
                        if (callback) {
                            try {
                                callback();
                            } catch (error) {
                                console.error('Worker interval error:', error);
                            }
                        }
                    }
                };
            }
            
            setInterval(callback, interval) {
                const id = ++this.timerId;
                this.callbacks.set(id, callback);
                this.worker.postMessage({ type: 'start', id, interval });
                return id;
            }
            
            clearInterval(id) {
                this.callbacks.delete(id);
                this.worker.postMessage({ type: 'stop', id });
            }
            
            destroy() {
                if (this.worker) {
                    this.worker.terminate();
                    this.worker = null;
                }
                this.callbacks.clear();
            }
        }
        
        // 管理器实例
        const rafManager = new RAFInterval();
        const timeoutManager = new TimeoutInterval();
        let workerManager = null;
        
        // 初始化 Worker (延迟加载)
        function initWorkerManager() {
            if (!workerManager) {
                workerManager = new WorkerInterval();
                workerInstance = workerManager;
            }
            return workerManager;
        }
        
        // 切换实现方法
        function switchMethod(method) {
            if (activeTimer) {
                alert('请先停止当前定时器');
                return;
            }
            
            currentMethod = method;
            document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
            event.target.classList.add('active');
            
            const methodNames = {
                'raf': 'requestAnimationFrame',
                'timeout': 'setTimeout递归',
                'worker': 'Web Worker'
            };
            
            document.getElementById('currentMethod').textContent = methodNames[method];
            log(`切换到 ${methodNames[method]} 实现`);
        }
        
        // 开始定时器
        function startTimer() {
            if (activeTimer) return;
            
            const interval = parseInt(document.getElementById('interval').value);
            const maxCount = parseInt(document.getElementById('maxCount').value);
            
            if (interval < 100 || maxCount < 1) {
                alert('参数设置错误');
                return;
            }
            
            // 重置统计
            executionCount = 0;
            startTime = performance.now();
            lastExecutionTime = startTime;
            intervalSum = 0;
            
            // 更新UI
            document.getElementById('startBtn').disabled = true;
            document.getElementById('stopBtn').disabled = false;
            document.getElementById('status').textContent = '运行中';
            
            log(`开始执行 - 间隔: ${interval}ms, 最大次数: ${maxCount}`);
            
            // 执行回调函数
            const callback = () => {
                const currentTime = performance.now();
                const actualInterval = currentTime - lastExecutionTime;
                
                executionCount++;
                intervalSum += actualInterval;
                
                log(`第 ${executionCount} 次执行 - 实际间隔: ${actualInterval.toFixed(2)}ms`);
                
                // 更新统计
                updateStats(interval, actualInterval);
                
                lastExecutionTime = currentTime;
                
                // 检查是否达到最大次数
                if (executionCount >= maxCount) {
                    stopTimer();
                }
            };
            
            // 根据选择的方法启动定时器
            switch (currentMethod) {
                case 'raf':
                    activeTimer = { id: rafManager.setInterval(callback, interval), manager: rafManager };
                    break;
                case 'timeout':
                    activeTimer = { id: timeoutManager.setInterval(callback, interval), manager: timeoutManager };
                    break;
                case 'worker':
                    const manager = initWorkerManager();
                    activeTimer = { id: manager.setInterval(callback, interval), manager };
                    break;
            }
        }
        
        // 停止定时器
        function stopTimer() {
            if (!activeTimer) return;
            
            activeTimer.manager.clearInterval(activeTimer.id);
            activeTimer = null;
            
            // 更新UI
            document.getElementById('startBtn').disabled = false;
            document.getElementById('stopBtn').disabled = true;
            document.getElementById('status').textContent = '停止';
            
            const totalTime = performance.now() - startTime;
            log(`定时器已停止 - 总运行时间: ${totalTime.toFixed(2)}ms`);
            
            // 最终统计
            updateFinalStats(totalTime);
        }
        
        // 更新统计信息
        function updateStats(expectedInterval, actualInterval) {
            document.getElementById('executed').textContent = executionCount;
            
            if (executionCount > 1) {
                const avgInterval = intervalSum / executionCount;
                document.getElementById('avgInterval').textContent = avgInterval.toFixed(2);
                
                const precision = Math.abs(avgInterval - expectedInterval);
                document.getElementById('precision').textContent = precision.toFixed(2);
            }
        }
        
        // 更新最终统计
        function updateFinalStats(totalTime) {
            document.getElementById('totalTime').textContent = totalTime.toFixed(0);
            
            if (executionCount > 1) {
                const avgInterval = intervalSum / executionCount;
                const expectedInterval = parseInt(document.getElementById('interval').value);
                const precision = Math.abs(avgInterval - expectedInterval);
                const performance = Math.max(0, 100 - (precision / expectedInterval) * 100);
                
                document.getElementById('performance').textContent = performance.toFixed(0);
            }
        }
        
        // 日志记录
        function log(message) {
            const logs = document.getElementById('logs');
            const time = new Date().toLocaleTimeString();
            logs.innerHTML += `<div>[${time}] ${message}</div>`;
            logs.scrollTop = logs.scrollHeight;
        }
        
        // 清除日志
        function clearLogs() {
            document.getElementById('logs').innerHTML = '';
        }
        
        // 页面卸载时清理
        window.addEventListener('beforeunload', () => {
            if (activeTimer) {
                stopTimer();
            }
            if (workerInstance) {
                workerInstance.destroy();
            }
        });
        
        // 工具函数:性能测试
        window.performanceTest = async function(method, interval, count) {
            console.log(`开始性能测试: ${method}, 间隔: ${interval}ms, 次数: ${count}`);
            
            return new Promise((resolve) => {
                let execCount = 0;
                const intervals = [];
                let lastTime = performance.now();
                
                const callback = () => {
                    const currentTime = performance.now();
                    if (execCount > 0) {
                        intervals.push(currentTime - lastTime);
                    }
                    lastTime = currentTime;
                    execCount++;
                    
                    if (execCount >= count) {
                        // 计算统计数据
                        const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
                        const variance = intervals.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / intervals.length;
                        const stdDev = Math.sqrt(variance);
                        
                        resolve({
                            method,
                            average: avg,
                            stdDev,
                            precision: Math.abs(avg - interval),
                            intervals
                        });
                    }
                };
                
                // 启动对应的定时器
                let timerId;
                switch (method) {
                    case 'raf':
                        timerId = rafManager.setInterval(callback, interval);
                        break;
                    case 'timeout':
                        timerId = timeoutManager.setInterval(callback, interval);
                        break;
                }
            });
        };
        
        console.log('自定义 setInterval 实现演示已加载');
        console.log('使用 performanceTest("raf", 1000, 10) 进行性能测试');
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 理解不同实现方案的优缺点和适用场景
  • 掌握 requestAnimationFrame 和 setTimeout 的使用
  • 了解定时器的精度问题和时间补偿机制

加分项:

  • 考虑错误处理和资源清理
  • 实现暂停/恢复等高级功能
  • 提及 Web Worker 在定时任务中的优势

常见失误:

  • 不考虑时间漂移问题
  • 忽略错误处理和内存泄漏
  • 混淆动画场景和精确计时场景的需求

延伸阅读: