阅读视图

发现新文章,点击刷新页面。

🧱 一文搞懂盒模型box-sizing:从标准盒到怪异盒的本质区别

几乎所有前端布局问题,最终都能追溯到一个老朋友——盒模型(Box Model)

它决定了一个元素在页面中究竟“占多大空间”。
然而,当你第一次发现 width: 600px 的盒子,结果却在页面上占了 620px,是不是也曾一脸问号?🤔

别急,这篇文章带你从底层彻底搞懂:
✅ 什么是盒模型
✅ 标准盒模型 vs 怪异盒模型
box-sizing 到底改了什么
✅ 面试常考陷阱与实战建议


🧩 一、盒模型是什么?

📖 定义:盒模型是浏览器渲染元素时,用来计算元素 尺寸与位置 的规则。

每个 HTML 元素都可以看成一个盒子,它由以下几部分组成(从内到外):

当我们右键点击检查,在样式中也可以清楚的看到这个样式

image.png

  • content:内容区,width / height 实际指的就是它。
  • padding:内边距,让内容与边框“留点空隙”。
  • border:边框,占据空间。
  • margin:外边距,用来与其他元素保持距离。

📦 二、标准盒模型(content-box)

这是 CSS 默认 的盒模型。

box-sizing: content-box; /* 默认值 */

👉 特点:
widthheight 只包含 content(内容区) ,不包括 paddingborder

image.png

也就是图中框选部分

举个例子

.box {
  box-sizing: content-box;
  width: 600px;
  padding: 10px;
  border: 2px solid #000;
}

📐 实际占用空间计算:

总宽度 = content + padding + border
= 600 + (10 * 2) + (2 * 2)
= 624px

✅ 所以虽然写了 600px,在页面上其实占了 624px 的宽度

💬 这也是很多初学者常掉坑的地方。


🧱 三、怪异盒模型(border-box)

box-sizing: border-box;

👉 特点:
widthheight 包含 content + padding + border

也就是说,width: 600px 表示整个盒子(从内容到边框)的总宽度为 600px!

image.png

也就是图中框选部分

举个例子

.box {
  box-sizing: border-box;
  width: 600px;
  padding: 10px;
  border: 2px solid #000;
}

📐 实际内容宽度计算:

内容宽度 = 总宽度 - padding - border
= 600 - (10 * 2) - (2 * 2)
= 576px

✅ 页面上盒子占用的宽度仍然是 600px,只是内容被“压缩”了。


🧮 四、两种盒模型的直观对比

属性 标准盒模型 (content-box) 怪异盒模型 (border-box)
width 含义 只包含内容区(content) 包含内容 + 内边距 + 边框
实际占用宽度 width + padding + border width
修改 padding / border 会撑大盒子 不会影响盒子总宽度
默认值 ✅ 默认 ❌ 需手动设置

🧠 五、为什么有“怪异盒模型”?

“怪异”其实并不怪。
在早期 IE 浏览器中,它默认采用 border-box,导致与标准的 CSS 规范不兼容。
但开发者发现这种方式在实际布局中 更符合直觉,尤其是响应式布局时更方便。

因此,现代开发中我们常常统一设置为:

* {
  box-sizing: border-box;
}

这样无论加多少 padding 或 border,都不会让盒子“炸开”。


⚙️ 六、实际开发建议

推荐设置(现代项目通用写法)

*, *::before, *::after {
  box-sizing: border-box;
}

保持统一的布局逻辑

  • 不需要再手动计算内容 + 边框 + 内边距。
  • 与 Figma、Sketch 等设计稿尺寸一致。

在一些场景下仍可用 content-box

  • 比如文字内容容器,需要内容撑开大小时。

🎯 七、面试高频问题

❓问:box-sizing: border-box 有什么作用?

答:
它让元素的 width / height 包含内容、内边距和边框,从而让盒子大小更易于控制。
设置后,调整 padding 不会改变盒子的整体尺寸。


🧭 八、总结回顾

概念 含义
标准盒模型 width = 内容区
怪异盒模型 width = 内容 + padding + border
box-sizing 属性 用来切换两种盒模型
实战建议 全局设置 border-box,布局更可控

📚 写在最后

盒模型是 CSS 的底层逻辑之一,看似简单,却几乎出现在所有布局场景中。
理解 box-sizing,你不仅能避免常见的“页面炸宽”问题,更能写出可预测、稳定的布局。

🧠 面试问基础、实战靠直觉,理解盒模型就是成为 CSS 工程师的第一步!

🚀 一文吃透 React 性能优化三剑客:useCallback、useMemo 与 React.memo

如果你学 React 一段时间了,肯定听过这三兄弟的名字:

  • useMemo
  • useCallback
  • React.memo

但你可能也有这样的疑惑:

🤔 它们都能防止无意义渲染,到底有什么区别?
🤔 为什么我用了 React.memo 子组件还是重新渲染?
🤔 什么时候用 useMemo,什么时候用 useCallback

别急,这篇文章会用最清晰的逻辑 + 直观的代码案例,帮你从渲染底层机制彻底吃透这三者。


🧩 一、React 性能问题的根源

1.1 为什么子组件会"无辜"重渲染?

在 React 中,每当父组件状态更新时,整个函数体会重新执行一遍:

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => console.log('clicked');

  console.log('Parent 渲染');
  
  return (
    <>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </>
  );
}

function Child({ onClick }) {
  console.log('Child 渲染');
  return <button onClick={onClick}>子按钮</button>;
}

执行流程:

  1. 点击 +1 按钮
  2. count 更新,触发 Parent 重新执行
  3. handleClick 被重新创建(新的函数引用)
  4. React 对比 Child 的 props:onClick 引用变了
  5. 触发 Child 重新渲染 ❌

问题关键: 在 JavaScript 中,每次创建的函数都是新的引用:

const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false

即使函数内容完全一样,React 也会认为 props 发生了变化。


⚙️ 二、React.memo:阻止"假变化"引发的渲染

2.1 什么是 React.memo?

React.memo 是一个高阶组件(HOC),它会对组件的 props 进行浅比较

const Child = React.memo(function Child({ onClick }) {
  console.log('🎨 Child 渲染');
  return <button onClick={onClick}>子按钮</button>;
});

工作原理:

父组件重渲染
   ↓
React.memo 检查
   ↓
判断:新 props === 旧 props ?
   ├─ 是 → ✅ 跳过渲染,复用上次结果
   └─ 否 → ❌ 执行渲染

2.2 React.memo 的局限

⚠️ 注意: React.memo 只做浅比较,对于引用类型(函数、对象、数组)的变化非常敏感:

// ❌ 每次都是新引用,React.memo 失效
<Child onClick={() => {}} />
<Child data={{ name: 'Alice' }} />
<Child list={[1, 2, 3]} />

这就是为什么我们需要 useCallbackuseMemo


🔁 三、useCallback:稳定函数引用

3.1 基本用法

useCallback 返回一个记忆化的函数,只有当依赖项改变时才会更新:

const handleClick = useCallback(() => {
  console.log('clicked');
}, []); // 依赖为空,函数引用永远不变

语法:

const memoizedCallback = useCallback(
  () => {
    // 你的函数逻辑
  },
  [依赖项]
);

3.2 配合 React.memo 使用

const Child = React.memo(({ onClick }) => {
  console.log('🎨 Child 渲染');
  return <button onClick={onClick}>子按钮</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ 函数引用稳定,不会触发 Child 重渲染
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
  
  return (
    <>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </>
  );
}

执行结果:

  • 点击 +1 → Parent 渲染 ✅
  • Child 不再渲染 ✅(因为 onClick 引用未变)

3.3 带依赖的 useCallback

const [userId, setUserId] = useState(1);

const fetchUserData = useCallback(() => {
  fetch(`/api/user/${userId}`);
}, [userId]); // 只有 userId 变化时才重新创建

💡 四、useMemo:缓存计算结果

4.1 与 useCallback 的区别

对比项 useCallback useMemo
缓存对象 函数本身 函数的返回值
返回值 () => {...} 执行结果
典型场景 传递给子组件的回调 复杂计算、派生状态

记忆技巧:

useCallback(fn, deps)    ≈  useMemo(() => fn, deps)

4.2 实际应用场景

function ProductList({ products }) {
  const [filter, setFilter] = useState('');

  // ✅ 只在 products 或 filter 改变时重新计算
  const filteredProducts = useMemo(() => {
    console.log('🔍 执行过滤逻辑...');
    return products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);

  return (
    <>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      {filteredProducts.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </>
  );
}

性能对比:

场景 无 useMemo 有 useMemo
1000 个商品首次渲染 计算 1 次 计算 1 次
输入框 focus(无关状态变化) 计算 1 次 ❌ 0 次 ✅
修改 filter 计算 1 次 计算 1 次

4.3 缓存对象/数组给子组件

const Child = React.memo(({ config }) => {
  console.log('🎨 Child 渲染');
  return <div>{config.theme}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ config 引用稳定
  const config = useMemo(() => ({
    theme: 'dark',
    lang: 'zh-CN'
  }), []);
  
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child config={config} />
    </>
  );
}

🧠 五、三者关系与配合策略

5.1 配合时序图

sequenceDiagram
    participant P as 父组件
    participant CB as useCallback
    participant CM as useMemo
    participant C as 子组件
    participant M as React.memo
    
    P->>P: state 更新
    P->>CB: 检查依赖
    CB-->>P: 返回缓存函数 ✅
    
    P->>CM: 检查依赖
    CM->>CM: 依赖未变,跳过计算
    CM-->>P: 返回缓存值 ✅
    
    P->>C: 传递 props
    C->>M: 触发渲染前检查
    M->>M: Object.is(newProps, oldProps)
    
    alt props 引用相同
        M-->>C: 跳过渲染 ✅
    else props 引用不同
        M->>C: 执行渲染 ❌
        C-->>P: 返回 JSX
    end

5.2 三者对比表

名称 类型 缓存内容 返回值 是否阻止渲染 常见搭配
React.memo HOC 组件渲染结果 组件 ✅ 是 useCallback/useMemo
useCallback Hook 函数引用 函数 ❌ 否 React.memo
useMemo Hook 计算结果 任意值 ❌ 否 React.memo

🔍 六、最佳实践与常见误区

6.1 什么时候应该用?

场景 推荐方案 理由
子组件接收函数 props 频繁渲染 useCallback + React.memo 稳定引用 + 跳过渲染
大列表过滤/排序/计算 useMemo 避免重复计算
传递对象/数组给 memo 子组件 useMemo 稳定引用
组件接收的 props 不常变化 React.memo 减少渲染次数
简单组件、轻量计算 ❌ 不用 优化成本 > 收益

6.2 常见误区

❌ 误区 1:到处使用

// ❌ 过度优化,反而增加开销
const add = useCallback((a, b) => a + b, []);
const result = useMemo(() => 1 + 1, []);

正确做法: 针对性能瓶颈优化,用 React DevTools Profiler 测量。

❌ 误区 2:忘记包裹 React.memo

// ❌ 只用 useCallback 没用,子组件还会渲染
const handleClick = useCallback(() => {}, []);
<Child onClick={handleClick} /> // Child 没用 React.memo

❌ 误区 3:依赖项遗漏

// ❌ 依赖了 count 但没声明,会导致闭包陷阱
const handleClick = useCallback(() => {
  console.log(count);
}, []); // 应该是 [count]

🎯 七、综合实战案例

import React, { useState, useMemo, useCallback, memo } from "react";

// 子组件:展示计算结果
const ResultDisplay = memo(({ result }) => {
  console.log("🎨 ResultDisplay 渲染");
  return <div>计算结果:{result}</div>;
});

// 子组件:按钮
const ActionButton = memo(({ onAction, label }) => {
  console.log("🎨 ActionButton 渲染");
  return <button onClick={onAction}>{label}</button>;
});

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // ✅ useMemo 缓存复杂计算
  const expensiveResult = useMemo(() => {
    console.log("💡 执行昂贵计算...");
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += i;
    }
    return sum + count;
  }, [count]); // 只在 count 变化时重新计算

  // ✅ useCallback 缓存函数引用
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  const handleReset = useCallback(() => {
    setCount(0);
  }, []);

  console.log("🧩 App 组件渲染");

  return (
    <div>
      <h1>性能优化示例</h1>
      
      <p>计数:{count}</p>
      
      {/* 输入框变化不会触发子组件渲染 */}
      <input 
        value={text} 
        onChange={e => setText(e.target.value)} 
        placeholder="输入文字试试"
      />
      
      {/* 这两个组件只在必要时渲染 */}
      <ResultDisplay result={expensiveResult} />
      <ActionButton onAction={handleIncrement} label="+1" />
      <ActionButton onAction={handleReset} label="重置" />
    </div>
  );
}

执行效果:

  1. 修改输入框 → 只有 App 渲染,子组件不渲染 ✅
  2. 点击 +1 → AppResultDisplay 渲染,ActionButton 不渲染 ✅
  3. 点击重置 → 同上

🧠 八、记忆口诀(背下来你就是懂哥)

Hook/API 记忆口诀 核心功能 英文含义
useMemo "我记住" 缓存计算结果 memoize value
useCallback "我记住函数" 缓存函数引用 callback function
React.memo "我记住组件" 跳过重复渲染 memoize component

一句话总结:

useCallbackuseMemo 让 props 稳定,
React.memo 让稳定的 props 不触发渲染。


📊 九、性能优化决策树

开始
 │
 ├─ 组件渲染慢吗?
 │   ├─ 否 → 不用优化
 │   └─ 是 ↓
 │
 ├─ 是子组件无意义渲染吗?
 │   ├─ 是 → 用 React.memo 包裹子组件
 │   │        ↓
 │   │      检查 props 类型
 │   │        ├─ 函数 → useCallback
 │   │        ├─ 对象/数组 → useMemo
 │   │        └─ 基本类型 → 不用处理
 │   │
 │   └─ 否 ↓
 │
 └─ 有复杂计算逻辑吗?
     ├─ 是 → 用 useMemo 缓存结果
     └─ 否 → 检查其他性能问题

🏁 十、结语

React 的性能优化不只是加几个 Hook 就完事,更重要的是理解渲染流程与引用稳定性的本质。

真正的优化思维:

  1. 测量优先:用 React DevTools Profiler 找到真正的瓶颈
  2. 针对性优化:不是所有组件都需要 memo
  3. 理解原理:知道为什么要用,而不是盲目照搬
  4. 权衡取舍:优化本身也有成本(内存、代码复杂度)
❌