Skip to content
Go back

调试技术整理

Published:  at  05:17 AM

Table of contents

Open Table of contents

高级浏览器调试技术详解

由于在找程序问题的时候,发现使用 console.log 效率不高,之后还得删除。于是整理了这个有臭又长的总览

1. 开发者工具 (Dev Tools)

概述

开发者工具是现代浏览器内置的一套强大的调试和分析工具,允许开发者检查、调试和优化网页代码。

为什么使用开发者工具

适用场景

实际示例

在Chrome中按F12或右键点击页面并选择”检查”即可打开开发者工具。在Elements标签中可以检查HTML结构,在Sources标签中可以调试JavaScript代码,在Network标签中可以分析网络请求。

// 假设我们有一个复杂的用户界面出现了问题
// 1. 打开开发者工具(F12)
// 2. 选择Elements标签查看DOM结构
// 3. 使用DOM断点观察元素变化
// 4. 切换到Sources标签设置JavaScript断点
// 5. 使用Console执行测试代码

2. 断点调试 (Breakpoints)

概述

断点是在代码执行过程中的特定位置设置的暂停点,允许开发者在代码执行到该位置时检查程序状态。

为什么使用断点

适用场景

实际示例

function processPurchase(cart) {
  // 在此处设置断点
  let subtotal = 0;
  let taxRate = 0.08;
  
  // 计算商品总价
  for (let item of cart.items) {
    // 可以在循环内设置断点观察每次迭代
    subtotal += item.price * item.quantity;
  }
  
  // 应用折扣
  let discount = 0;
  if (subtotal > 100) {
    // 条件断点可以在此处检查折扣计算
    discount = subtotal * 0.1;
  }
  
  // 计算税费和总价
  const tax = (subtotal - discount) * taxRate;
  const total = subtotal - discount + tax;
  
  return {
    subtotal,
    discount,
    tax,
    total
  };
}

// 使用断点调试此函数:
// 1. 在Sources面板找到此函数
// 2. 点击第2行行号设置断点
// 3. 调用函数并观察执行流程
processPurchase({ items: [
  { name: "Laptop", price: 1200, quantity: 1 },
  { name: "Mouse", price: 25, quantity: 2 }
]});

使用断点时,可以在开发者工具中看到:

3. 步进功能 (Stepping)

3.1 步入函数 (Step Into)

概述

步入功能允许调试器在遇到函数调用时,进入该函数内部并在函数第一行暂停执行。

为什么使用步入

适用场景

实际示例

function validateEmail(email) {
  // 步入后会在这里暂停
  if (!email || typeof email !== 'string') {
    return false;
  }
  
  const trimmedEmail = email.trim();
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailPattern.test(trimmedEmail);
}

function processUserInput(formData) {
  // 在此行设置断点
  const username = formData.username;
  
  // 使用"步入"按钮会进入validateEmail函数内部
  if (!validateEmail(formData.email)) {
    return { success: false, error: "Invalid email format" };
  }
  
  // 处理其他验证...
  return { success: true };
}

// 调试流程:
// 1. 在processUserInput函数内设置断点
// 2. 执行到validateEmail调用时,点击"步入"按钮
// 3. 调试器会跳转到validateEmail函数内部的第一行
// 4. 可以观察email参数值及函数内部处理逻辑
processUserInput({
  username: "johnsmith",
  email: "invalid-email"  // 有问题的邮箱格式
});

3.2 步过函数 (Step Over)

概述

步过功能执行当前行(包括任何函数调用),然后在下一行代码处暂停,而不会进入函数内部。

为什么使用步过

适用场景

实际示例

function getFullName(user) {
  // 假设这是一个简单的辅助函数
  return `${user.firstName} ${user.lastName}`;
}

function formatUserProfile(user) {
  // 在此行设置断点
  const fullName = getFullName(user);  // 使用"步过"会执行此函数但不进入内部
  
  const formattedProfile = {
    name: fullName,
    displayName: fullName.toUpperCase(),
    email: user.email,
    joinDate: new Date(user.joinTimestamp).toLocaleDateString()
  };
  
  return formattedProfile;
}

// 调试流程:
// 1. 在formatUserProfile函数内设置断点
// 2. 执行到getFullName调用时,点击"步过"按钮
// 3. 调试器会执行getFullName函数但不进入其内部
// 4. 直接在下一行(const formattedProfile)处暂停
formatUserProfile({
  firstName: "John",
  lastName: "Smith",
  email: "john@example.com",
  joinTimestamp: 1632145600000
});

3.3 步出函数 (Step Out)

概述

步出功能执行当前函数的剩余部分,直到函数返回,然后在调用者的下一行代码处暂停。

为什么使用步出

适用场景

实际示例

function processArrayItems(items) {
  let results = [];
  
  // 一个复杂的处理循环
  for (let i = 0; i < items.length; i++) {
    // 假设我们在这里设置了断点或步入
    const item = items[i];
    const processed = item * 2 + 1;
    results.push(processed);
    // 检查了几次迭代后,想跳出这个函数
    // 此时使用"步出"按钮
  }
  
  return results; // 执行到这里然后返回到调用函数
}

function analyzeData(data) {
  // 在此处设置初始断点
  const cleanedData = data.filter(item => item > 0);
  
  // 步入此函数调用,检查几次迭代后使用"步出"
  const processedData = processArrayItems(cleanedData);
  
  // "步出"后会在这里暂停
  const average = processedData.reduce((sum, val) => sum + val, 0) / processedData.length;
  
  return {
    processed: processedData,
    average: average
  };
}

// 调试流程:
// 1. 在analyzeData函数内设置断点
// 2. 执行到processArrayItems调用时,点击"步入"按钮
// 3. 观察循环几次迭代后,点击"步出"按钮
// 4. 调试器会完成processArrayItems函数并在average计算行暂停
analyzeData([5, 10, -3, 8, 2, -1, 7]);

4. 条件断点 (Conditional Breakpoints)

概述

条件断点是一种只在满足特定条件时才会触发的断点。当代码执行到设置断点的行且条件表达式评估为true时,程序执行才会暂停。

为什么使用条件断点

适用场景

实际示例

function processOrders(orders) {
  let totalRevenue = 0;
  
  for (let i = 0; i < orders.length; i++) {
    const order = orders[i];
    
    // 在此处设置条件断点: order.total > 1000 || order.items.length > 10
    // 只有当订单金额大于1000或商品数量大于10时才会暂停
    
    // 处理订单逻辑
    const discount = calculateDiscount(order);
    const tax = calculateTax(order.total - discount);
    const finalTotal = order.total - discount + tax;
    
    // 更新总收入
    totalRevenue += finalTotal;
    
    // 其他处理...
  }
  
  return totalRevenue;
}

// 设置条件断点的步骤:
// 1. 在Sources面板找到相关代码行
// 2. 右键点击行号
// 3. 选择"添加条件断点"
// 4. 输入条件表达式: order.total > 1000 || order.items.length > 10
// 5. 执行函数,只有满足条件的订单才会触发断点

const sampleOrders = [
  { id: 1, total: 500, items: [{ /* 商品详情 */ }] },
  { id: 2, total: 1200, items: [{ /* 商品详情 */ }, { /* 商品详情 */ }] }, // 会触发断点
  { id: 3, total: 300, items: [{ /* 商品详情 */ }, { /* 商品详情 */ }, /* 更多商品... */] }, // 如果超过10个商品也会触发
  { id: 4, total: 1500, items: [{ /* 商品详情 */ }] } // 会触发断点
];

processOrders(sampleOrders);

5. 调用栈 (Call Stack)

概述

调用栈是一个展示当前执行点如何到达的函数调用链。它按照调用顺序显示所有活动的函数调用,最新的调用在顶部。

为什么使用调用栈

适用场景

实际示例

function displayProductDetails(productId) {
  // 最后被调用的函数
  const product = getProductInfo(productId);
  renderProductView(product);
}

function getProductInfo(id) {
  // 中间函数,在调用栈的中间位置
  validateId(id); // 首先验证ID
  return fetchProductFromDatabase(id);
}

function validateId(id) {
  // 调用栈中的第一个函数
  if (typeof id !== 'number' || id <= 0) {
    throw new Error('Invalid product ID');
  }
}

function fetchProductFromDatabase(id) {
  // 模拟数据库调用
  if (id === 404) {
    // 在此处设置断点
    throw new Error('Product not found');
  }
  
  return {
    id: id,
    name: `Product ${id}`,
    price: id * 10,
    description: `This is product ${id}`
  };
}

// 当在fetchProductFromDatabase中的错误处设置断点时
// 调用栈将显示(从上到下):
// - fetchProductFromDatabase (当前位置)
// - getProductInfo (调用者)
// - displayProductDetails (初始调用者)

// 执行函数,传入一个会触发错误的ID
try {
  displayProductDetails(404); // 会导致错误
} catch (error) {
  console.error('Error:', error.message);
}

在开发者工具中,当断点触发时,可以在右侧面板或单独的”Call Stack”面板查看完整调用栈。点击调用栈中的任何函数都可以跳转到该函数的执行上下文,查看当时的变量状态。

6. 监视表达式 (Watch Expressions)

概述

监视表达式允许开发者在调试过程中持续观察特定表达式的值,而无需每次都在控制台输入。表达式可以是简单变量、复杂对象属性访问、函数调用或计算。

为什么使用监视表达式

适用场景

实际示例

function calculateShoppingCartMetrics(cart) {
  // 可以添加以下监视表达式:
  // - cart.items.length (商品数量)
  // - cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0) (商品总价)
  // - cart.items.filter(item => item.discounted).length (折扣商品数量)
  
  let subtotal = 0;
  let weight = 0;
  let itemCount = 0;
  
  for (let i = 0; i < cart.items.length; i++) {
    const item = cart.items[i];
    // 在循环中可以添加以下监视:
    // - i (当前索引)
    // - item (当前商品)
    // - subtotal (累计总价)
    
    const itemPrice = item.discounted ? 
      item.price * (1 - item.discountRate) : 
      item.price;
      
    subtotal += itemPrice * item.quantity;
    weight += item.weight * item.quantity;
    itemCount += item.quantity;
  }
  
  // 计算运费
  let shipping = 0;
  if (weight > 20) {
    shipping = 15 + (weight - 20) * 0.5;
  } else if (weight > 0) {
    shipping = 10;
  }
  
  // 计算税费
  const taxRate = 0.08;
  const tax = subtotal * taxRate;
  
  // 计算总价
  const total = subtotal + shipping + tax;
  
  return {
    subtotal,
    shipping,
    tax,
    total,
    itemCount,
    weight
  };
}

// 添加监视表达式的步骤:
// 1. 在Sources面板中设置断点
// 2. 在右侧找到"Watch"部分
// 3. 点击"+"添加表达式
// 4. 输入要监视的表达式,例如:
//    - subtotal + shipping + tax
//    - itemCount > 10
//    - cart.items.some(item => item.price <= 0)

const sampleCart = {
  userId: "user123",
  items: [
    { id: 101, name: "Laptop", price: 1200, weight: 2.5, quantity: 1, discounted: false },
    { id: 102, name: "Mouse", price: 25, weight: 0.2, quantity: 2, discounted: true, discountRate: 0.1 },
    { id: 103, name: "Monitor", price: 300, weight: 5, quantity: 1, discounted: false },
    { id: 104, name: "Keyboard", price: 50, weight: 0.8, quantity: 1, discounted: true, discountRate: 0.2 }
  ]
};

calculateShoppingCartMetrics(sampleCart);

7. DOM事件监听器断点 (DOM Event Listener Breakpoints)

概述

DOM事件监听器断点允许开发者在特定类型的DOM事件触发时暂停执行,无需知道事件监听器代码的确切位置。

为什么使用DOM事件断点

适用场景

实际示例

// HTML:
// <form id="registration-form">
//   <input type="text" id="username" placeholder="Username">
//   <input type="email" id="email" placeholder="Email">
//   <button type="submit" id="submit-btn">Register</button>
// </form>

// JavaScript:
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('registration-form');
  const usernameInput = document.getElementById('username');
  const emailInput = document.getElementById('email');
  const submitButton = document.getElementById('submit-btn');
  
  // 添加事件监听器
  usernameInput.addEventListener('input', validateUsername);
  emailInput.addEventListener('input', validateEmail);
  submitButton.addEventListener('click', validateForm);
  form.addEventListener('submit', handleSubmit);
  
  function validateUsername(event) {
    const username = event.target.value.trim();
    // 验证用户名逻辑...
  }
  
  function validateEmail(event) {
    const email = event.target.value.trim();
    // 验证邮箱逻辑...
  }
  
  function validateForm(event) {
    // 表单验证逻辑...
    if (!isFormValid()) {
      event.preventDefault(); // 阻止提交
    }
  }
  
  function handleSubmit(event) {
    event.preventDefault(); // 阻止默认提交行为
    // 处理表单提交...
  }
});

// 设置DOM事件断点的步骤:
// 1. 在Chrome开发者工具中,选择Sources面板
// 2. 展开右侧的"Event Listener Breakpoints"部分
// 3. 展开相关事件类别,例如"Mouse"或"Control"
// 4. 选择感兴趣的事件类型,例如"click"或"submit"
// 5. 在网页上触发相应事件,执行会在事件处理函数开始处暂停

// 可以设置的有用事件断点:
// - Mouse > click (点击事件)
// - Control > submit (表单提交)
// - Clipboard > copy/paste (复制粘贴)
// - Keyboard > keydown/keyup (键盘输入)
// - Animation > requestAnimationFrame (动画)
// - Timer > setTimeout/setInterval (定时器)

8. DOM变更断点

8.1 节点移除断点 (Break on Node Removal)

概述

节点移除断点在指定DOM元素被从文档中移除时触发。

为什么使用节点移除断点

适用场景

实际示例

//




# 高级浏览器调试技术详解

## 8. DOM变更断点

### 8.1 节点移除断点 (Break on Node Removal)(

#### 实际示例
```javascript
// HTML:
// <div id="notification-container">
//   <div class="notification" id="notif-1">更新成功!</div>
//   <div class="notification" id="notif-2">新消息已收到</div>
// </div>

// JavaScript:
function removeNotification(id) {
  const notification = document.getElementById(id);
  if (notification) {
    // 当设置了节点移除断点时,这里会暂停执行
    notification.parentNode.removeChild(notification);
    // 或使用现代API: notification.remove();
  }
}

// 设置自动清除通知
setTimeout(() => {
  removeNotification('notif-1');
}, 3000);

// 设置节点移除断点的步骤:
// 1. 在Elements面板中找到目标元素(如id为"notif-1"的div)
// 2. 右键点击该元素
// 3. 选择"Break on..." > "node removal"
// 4. 当3秒后超时触发时,调试器会在removeNotification函数中暂停

8.2 属性修改断点 (Break on Attribute Modifications)

概述

属性修改断点在指定DOM元素的任何属性(如class、id、style等)被修改时触发。

为什么使用属性修改断点

适用场景

实际示例

// HTML:
// <button id="toggle-theme" class="btn btn-light">切换到深色模式</button>
// <div id="content" class="light-theme">
//   <p>这是一些内容文本</p>
// </div>

// JavaScript:
document.getElementById('toggle-theme').addEventListener('click', function() {
  const content = document.getElementById('content');
  const button = document.getElementById('toggle-theme');
  
  if (content.classList.contains('light-theme')) {
    // 切换到深色模式
    // 在这里会触发属性修改断点
    content.classList.remove('light-theme');
    content.classList.add('dark-theme');
    button.textContent = '切换到浅色模式';
  } else {
    // 切换到浅色模式
    // 这里也会触发属性修改断点
    content.classList.remove('dark-theme');
    content.classList.add('light-theme');
    button.textContent = '切换到深色模式';
  }
});

// 设置属性修改断点的步骤:
// 1. 在Elements面板中找到id为"content"的div元素
// 2. 右键点击该元素
// 3. 选择"Break on..." > "attribute modifications"
// 4. 点击"切换主题"按钮时,调试器会在修改classList的代码处暂停

8.3 子树修改断点 (Break on Subtree Modifications)

概述

子树修改断点在指定DOM元素的子元素结构发生变化时触发,包括添加新子元素、移除子元素或修改子元素顺序。

为什么使用子树修改断点

适用场景

实际示例

// HTML:
// <ul id="task-list">
//   <li>完成项目提案</li>
//   <li>安排团队会议</li>
// </ul>
// <input id="new-task" type="text" placeholder="添加新任务">
// <button id="add-task">添加</button>

// JavaScript:
document.getElementById('add-task').addEventListener('click', function() {
  const taskInput = document.getElementById('new-task');
  const taskText = taskInput.value.trim();
  
  if (taskText) {
    const taskList = document.getElementById('task-list');
    
    // 创建新任务项
    const newTask = document.createElement('li');
    newTask.textContent = taskText;
    
    // 添加到列表 - 这里会触发子树修改断点
    taskList.appendChild(newTask);
    
    // 清空输入框
    taskInput.value = '';
  }
});

// 设置子树修改断点的步骤:
// 1. 在Elements面板中找到id为"task-list"的ul元素
// 2. 右键点击该元素
// 3. 选择"Break on..." > "subtree modifications"
// 4. 添加新任务时,调试器会在taskList.appendChild(newTask)处暂停

9. debugger 语句

概述

debugger 是JavaScript中的一个特殊语句,当浏览器的开发者工具打开时,会在执行到该语句处自动触发断点。

为什么使用debugger语句

适用场景

实际示例

function processPayment(payment) {
  // 基本验证
  if (!payment || typeof payment !== 'object') {
    return { success: false, error: 'Invalid payment data' };
  }
  
  // 在处理大额支付时触发调试器
  if (payment.amount > 10000) {
    debugger; // 大额支付时会自动暂停执行
  }
  
  // 处理支付逻辑
  try {
    validatePaymentDetails(payment);
    const result = sendPaymentToGateway(payment);
    updateOrderStatus(payment.orderId, result.status);
    return { success: true, transactionId: result.transactionId };
  } catch (error) {
    // 在错误处理时触发调试器
    debugger; // 出现异常时会自动暂停执行
    return { success: false, error: error.message };
  }
}

// debugger语句的使用技巧:
// 1. 条件触发调试器
function complexAlgorithm(data, options) {
  let iterations = 0;
  
  while (shouldContinueProcessing(data)) {
    iterations++;
    processOneIteration(data);
    
    // 只有迭代次数过多时才触发调试器
    if (iterations > 1000) {
      debugger; // 可能存在性能问题或无限循环
    }
  }
}

// 2. 生产环境安全的调试器
function criticalFunction() {
  // 只在开发环境或调试模式下触发
  if (process.env.NODE_ENV === 'development' || window.DEBUG_MODE) {
    debugger;
  }
  
  // 执行关键逻辑...
}

10. 控制台高级技巧

开发者工具控制台提供了许多强大功能,可以配合断点调试使用或单独使用。

10.1 高级控制台命令

console.table()

概述

以表格形式显示对象或数组数据,使数据更易读和比较。

为什么使用
实际示例
// 用户数据数组
const users = [
  { id: 1, name: "张三", age: 28, role: "开发者" },
  { id: 2, name: "李四", age: 34, role: "设计师" },
  { id: 3, name: "王五", age: 24, role: "产品经理" },
  { id: 4, name: "赵六", age: 32, role: "开发者" }
];

// 普通console.log只显示折叠的对象
console.log("用户数据:", users);

// console.table以表格形式显示,更易读
console.table(users);

// 可以指定要显示的列
console.table(users, ["name", "age", "role"]);

console.trace()

概述

打印当前执行点的调用栈跟踪,显示代码执行路径。

为什么使用
实际示例
function initApp() {
  setupConfig();
}

function setupConfig() {
  loadUserPreferences();
}

function loadUserPreferences() {
  console.trace("加载用户首选项"); // 显示调用栈
  // 输出的调用栈显示:
  // loadUserPreferences
  // setupConfig
  // initApp
  // <匿名>
}

initApp();

console.time() 和 console.timeEnd()

概述

用于测量代码执行时间的计时器功能。

为什么使用
实际示例
function searchDatabase(query) {
  console.time('数据库搜索'); // 开始计时
  
  // 执行搜索逻辑
  const results = performActualSearch(query);
  
  console.timeEnd('数据库搜索'); // 结束计时并显示耗时
  return results;
}

function optimizeImages(images) {
  console.time('图片优化总时间');
  
  for (const image of images) {
    console.time(`优化图片 ${image.id}`);
    // 图片优化处理
    processImage(image);
    console.timeEnd(`优化图片 ${image.id}`);
  }
  
  console.timeEnd('图片优化总时间');
}

10.2 条件断点的控制台变体

概述

可以在条件断点中使用console.log而不实际暂停执行,实现”条件日志”功能。

为什么使用

高级浏览器调试技术详解(续)

10.2 条件断点的控制台变体(续)

为什么使用(续)

适用场景

实际示例

// 假设我们有一个处理用户交易的函数
function processTransactions(transactions) {
  for (let i = 0; i < transactions.length; i++) {
    const transaction = transactions[i];
    
    // 设置条件断点,但使用console.log而不是真正暂停:
    // console.log(transaction.amount > 1000 ? `大额交易: ${JSON.stringify(transaction)}` : ''); false
    // 这会在大额交易时输出日志,但不会暂停执行
    
    // 处理交易...
    validateTransaction(transaction);
    updateBalance(transaction);
    notifyUser(transaction);
  }
}

// 设置方法:
// 1. 在循环内的行上设置条件断点
// 2. 输入: console.log(满足条件时要显示的内容); false
// 3. "false"部分确保断点不会真正触发暂停

11. 网络请求调试技术

11.1 网络请求断点 (XHR/Fetch Breakpoints)

概述

允许在特定URL的网络请求发送前暂停执行,以便检查请求参数和调用上下文。

为什么使用网络请求断点

适用场景

实际示例

function fetchUserData(userId) {
  // 这个fetch调用会触发网络请求断点
  return fetch(`/api/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`获取用户数据失败: ${response.status}`);
      }
      return response.json();
    })
    .then(userData => {
      processUserData(userData);
      return userData;
    });
}

function saveUserProfile(profile) {
  // POST请求也会触发网络请求断点
  return fetch('/api/profiles', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(profile)
  });
}

// 设置网络请求断点的步骤:
// 1. 在Chrome开发者工具中,切换到Sources面板
// 2. 展开"XHR/fetch Breakpoints"部分
// 3. 点击"+"按钮添加URL模式
// 4. 输入URL或URL部分(例如 "api/users" 或 "api/profiles")
// 5. 当匹配的请求发送时,执行会在fetch或XMLHttpRequest调用处暂停

11.2 使用Network面板进行调试

概述

Network面板记录所有网络请求,显示详细信息如请求/响应头、载荷数据、计时和状态。

为什么使用Network面板

适用场景

实际示例

// 假设我们有一个获取天气数据的函数
async function getWeatherData(city) {
  try {
    const response = await fetch(`https://weather-api.example.com/forecast?city=${encodeURIComponent(city)}`);
    
    if (!response.ok) {
      throw new Error(`获取天气数据失败: ${response.status}`);
    }
    
    const weatherData = await response.json();
    updateWeatherUI(weatherData);
  } catch (error) {
    displayError(error.message);
  }
}

// 使用Network面板调试的步骤:
// 1. 打开Chrome开发者工具,切换到Network面板
// 2. 点击"清除"按钮清除现有记录
// 3. 调用getWeatherData("Beijing")函数
// 4. 在Network面板中找到对应请求并点击
// 5. 检查以下详细信息:
//    - Headers标签: 查看请求URL、方法、状态码
//    - Preview/Response标签: 检查响应数据
//    - Timing标签: 分析请求各阶段的耗时

12. 内存和性能调试

12.1 内存快照 (Heap Snapshots)

概述

堆快照捕获JavaScript对象的完整内存使用情况,帮助发现内存泄漏和优化内存使用。

为什么使用内存快照

适用场景

实际示例

// 可能导致内存泄漏的代码
function createMemoryLeak() {
  const leaks = window.leakyObjects || [];
  window.leakyObjects = leaks; // 存储在全局变量中
  
  // 创建一个包含大量数据的对象
  const hugeObject = {
    id: Date.now(),
    data: new Array(10000).fill('大量数据'),
    metadata: {
      createdAt: new Date(),
      type: 'leak-demo'
    }
  };
  
  // 将对象添加到全局数组,但从不清除
  leaks.push(hugeObject);
  
  return `已创建潜在内存泄漏对象,ID: ${hugeObject.id}`;
}

// 使用Heap Snapshot的步骤:
// 1. 打开Chrome开发者工具,切换到Memory面板
// 2. 选择"Heap Snapshot"选项
// 3. 点击"拍摄快照"按钮获取基准快照
// 4. 执行可能导致内存泄漏的操作(如多次调用createMemoryLeak())
// 5. 再次拍摄快照
// 6. 使用"Comparison"视图比较两次快照
// 7. 查找新分配但未释放的对象,特别关注:
//    - 对象计数的增长
//    - 内存大小的增长
//    - 对象的引用链(retaining paths)

12.2 性能分析 (Performance Profiling)

概述

性能分析记录应用执行期间的各种性能指标,包括CPU使用、事件处理、布局计算等。

为什么使用性能分析

适用场景

实际示例

// 可能导致性能问题的代码
function performHeavyCalculation() {
  console.time('复杂计算');
  
  // 强制布局回流
  document.getElementById('result-container').style.width = '100%';
  const width = document.getElementById('result-container').offsetWidth;
  
  // CPU密集型操作
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += Math.sqrt(i) * Math.sin(i);
  }
  
  // 更新DOM导致绘制
  document.getElementById('result-container').textContent = `计算结果: ${result}`;
  
  console.timeEnd('复杂计算');
}

// 使用Performance分析的步骤:
// 1. 打开Chrome开发者工具,切换到Performance面板
// 2. 点击"录制"按钮开始记录
// 3. 执行需要分析的操作(如调用performHeavyCalculation())
// 4. 点击"停止"结束录制
// 5. 分析结果,关注:
//    - Main部分: JavaScript执行、样式计算、布局计算
//    - Frames部分: 帧率和帧耗时
//    - Call Tree/Bottom-Up: 找出耗时最多的函数调用

13. 异步代码调试技术

13.1 异步调用栈 (Async Call Stack)

概述

现代浏览器开发工具能够跟踪异步操作的完整调用栈,显示异步操作源于何处。

为什么使用异步调用栈

适用场景

实际示例

// 一个包含多层异步操作的函数
function loadUserDataAsync(userId) {
  return fetchUserProfile(userId)
    .then(profile => {
      // 基于获取的配置文件获取更多数据
      return Promise.all([
        Promise.resolve(profile),
        fetchUserPreferences(profile.preferencesId),
        fetchUserPurchases(profile.purchaseHistoryId)
      ]);
    })
    .then(([profile, preferences, purchases]) => {
      // 在这里设置断点,查看异步调用栈
      return combineUserData(profile, preferences, purchases);
    })
    .catch(error => {
      console.error('加载用户数据失败:', error);
      throw error;
    });
}

function fetchUserProfile(userId) {
  return fetch(`/api/profiles/${userId}`).then(res => res.json());
}

function fetchUserPreferences(preferencesId) {
  return fetch(`/api/preferences/${preferencesId}`).then(res => res.json());
}

function fetchUserPurchases(historyId) {
  return fetch(`/api/purchases/${historyId}`).then(res => res.json());
}

// 调试异步调用栈的步骤:
// 1. 确保Chrome开发者工具中的设置已启用"Async"选项
//    (在Settings > Sources > check "Enable async stack traces")
// 2. 在Promise的then回调中设置断点
// 3. 执行异步操作
// 4. 当断点触发时,Call Stack面板会显示完整的异步调用栈
// 5. 可以看到当前回调的调用者,以及触发异步操作的原始位置

13.2. 黑盒脚本 (Blackboxing)

概述

黑盒处理允许开发者标记特定脚本,使调试器在单步执行或显示调用栈时跳过这些脚本。

为什么使用黑盒脚本

适用场景

实际示例

// 使用第三方库的代码
import _ from 'lodash';
import axios from 'axios';

async function fetchAndProcessData() {
  try {
    // 从API获取数据,axios内部代码会被黑盒处理
    const response = await axios.get('/api/data');
    
    // 使用Lodash处理数据,Lodash内部代码会被黑盒处理
    const processedData = _.chain(response.data)
      .filter(item => item.active)
      .map(item => ({
        id: item.id,
        name: item.name.toUpperCase(),
        score: item.points * 10
      }))
      .sortBy('score')
      .value();
    
    // 在这里设置断点
    return processedData;
  } catch (error) {
    console.error('处理数据失败:', error);
    throw error;
  }
}

// 设置黑盒脚本的步骤:
// 1. 在Chrome开发者工具的Sources面板中,找到需要黑盒处理的脚本
//    (如lodash.js或axios.min.js)
// 2. 右键点击脚本
// 3. 选择"Blackbox script"
// 4. 或在设置中配置黑盒模式:
//    Settings > Blackboxing > Add pattern 添加匹配模式
//    例如: "/node_modules/*" 将忽略所有node_modules中的脚本

14. 条件XHR断点

概述

条件XHR断点结合了网络请求断点和条件断点的功能,允许开发者在特定条件下的特定网络请求时暂停执行。

为什么使用条件XHR断点

适用场景

实际示例

// 用户数据服务
class UserService {
  async updateUserProfile(userId, profileData) {
    // 在这里设置条件XHR断点,条件: profileData.role === 'admin'
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(profileData)
    });
    
    if (!response.ok) {
      throw new Error(`更新用户资料失败: ${response.status}`);
    }
    
    return response.json();
  }
  
  async getAllUsers(filters = {}) {
    // 构建查询参数
    const queryParams = new URLSearchParams();
    for (const [key, value] of Object.entries(filters)) {
      queryParams.append(key, value);
    }
    
    // 在这里设置条件XHR断点,条件: filters.role !== undefined
    const url = `/api/users?${queryParams.toString()}`;
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`获取用户列表失败: ${response.status}`);
    }
    
    return response.json();
  }
}

// 设置条件XHR断点的步骤:
// 1. 在Chrome开发者工具中,切换到Sources面板
// 2. 展开"XHR/fetch Breakpoints"部分
// 3. 点击"+"按钮添加URL模式,如"/api/users"
// 4. 右键点击添加的URL断点
// 5. 选择"Edit breakpoint"
// 6. 添加条件表达式,如:
//    - 请求URL中包含特定参数: url.includes('role=admin')
//    - 请求体包含特定数据: body.includes('"role":"admin"')

15. 实用调试模式和工作流

15.1 事件驱动应用调试模式

概述

针对事件驱动型应用(如React、Vue或单页应用)的专门调试策略。

核心技术组合

实际工作流示例

// React组件示例
function UserDashboard({ userId }) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  // 加载用户数据
  useEffect(() => {
    // 可以在这里设置断点
    setLoading(true);
    
    fetchUserData(userId)
      .then(data => {
        // 在这里设置断点,观察data结构
        setUserData(data);
        setLoading(false);
      })
      .catch(err => {
        console.error('加载用户数据失败:', err);
        setError(err.message);
        setLoading(false);
      });
  }, [userId]);
  
  // 处理表单提交
  const handleSubmit = (event) => {
    event.preventDefault();
    // 设置DOM事件断点(submit)和普通断点
    const formData = new FormData(event.target);
    
    updateUserProfile(userId, {
      displayName: formData.get('displayName'),
      email: formData.get('email'),
      theme: formData.get('theme')
    });
  };
  
  // 渲染组件...
}

// 事件驱动应用的调试步骤:
// 1. 设置初始断点:
//    - 在初始数据加载处(useEffect内)
//    - 在事件处理函数起始处(handleSubmit)
// 2. 添加DOM事件监听器断点:
//    - Mouse > click (捕获按钮点击)
//    - Control > submit (捕获表单提交)
// 3. 添加DOM变更断点:
//    - 在关键UI元素上设置子树修改断点
// 4. 添加监视表达式:
//    - 组件状态: userData, loading, error
//    - 表单数据: formData.get('displayName')
// 5. 运行应用并交互,分析执行流程

15.2 性能调试模式

概述

专注于识别和解决性能问题的调试策略,结合多种工具进行全面分析。

核心技术组合

实际工作流示例

// 包含性能问题的列表渲染函数
function renderLargeList(items) {
  console.time('列表渲染');
  
  const container = document.getElementById('list-container');
  container.innerHTML = ''; // 清空容器
  
  // 低效的渲染循环
  items.forEach(item => {
    // 每次迭代都进行DOM操作,可能导致多次重绘
    const element = document.createElement('div');
    element.className = 'list-item';
    element.textContent = item.name;
    element.style.color = item.status === 'active' ? 'green' : 'gray';
    
    // 添加事件监听器
    element.addEventListener('click', () => {
      selectItem(item.id);
    });
    
    container.appendChild(element); // 每次都触发DOM更新
  });
  
  console.timeEnd('列表渲染');
}

// 性能优化版本
function renderLargeListOptimized(items) {
  console.time('优化后列表渲染');
  
  const container = document.getElementById('list-container');
  
  // 使用文档片段减少DOM操作
  const fragment = document.createDocumentFragment();
  
  // 构建所有元素
  items.forEach(item => {
    const element = document.createElement('div');
    element.className = 'list-item';
    element.textContent = item.name;
    element.style.color = item.status === 'active' ? 'green' : 'gray';
    element.dataset.itemId = item.id; // 使用数据属性存储ID
    
    fragment.appendChild(element);
  });
  
  // 一次性更新DOM
  container.innerHTML = '';
  container.appendChild(fragment);
  
  // 使用事件委托
  container.addEventListener('click', (event) => {
    const listItem = event.target.closest('.list-item');
    if (listItem) {
      selectItem(listItem.dataset.itemId);
    }
  });
  
  console.timeEnd('优化后列表渲染');
}

// 性能调试工作流:
// 1. 使用Performance面板记录渲染过程
// 2. 分析Main线程活动和帧率
// 3. 确定瓶颈(如频繁的布局重计算)
// 4. 在关键函数中添加console.time/timeEnd
// 5. 设置断点分析具体执行细节
// 6. 实施优化并比较前后性能差异

幸亏有目录



Previous Post
Git提交规范