【VDOM,Diff算法,生命周期,并发请求】

防抖:竟态条件处理(搜索框取消请求方法):

1.标志位法:
javascript 复制代码
let requestId = 0;

useEffect(() => {
  const currentId = ++requestId; 

  fetch(url).then((data) => {
    // 重点:这个 function 带走了 currentId
    console.log(currentId); 
       
     if(currentID===requestID){
        setResults(data)
       }
  });
}, [value]);

为什么请求发出去很久之后,currentId 还能记住当时的数字? 比如第 1 次请求 currentId=1,过了 3 秒请求才回来,为什么它还知道自己是 1,而不是变成最新的 3?

答案就是:**闭包 = 函数带走了自己的 "专属小背包"**我用最简单的方式讲:

闭包核心:函数在定义时,会把它周围的变量 "打包带走",永远跟着它,永远不变。 不管过多久,不管外面变量怎么变,函数带走的那个值,永远是当时定义时的值。

关键:每次 useEffect 运行,都会创造一个全新的独立环境!

模拟 3 次快速触发(最直观):

你连续快速触发 3 次,等于创建了 3 个独立的函数 + 3 个独立的 currentId

2. 什么都不做:

javascript 复制代码
// 让所有请求都完成,最后一个覆盖前面的
fetch(url).then(data => setResults(data));

缺点: 竞态条件 - 如果慢请求后返回,会覆盖快请求的正确结果。

3.AbortController:

javascript 复制代码
const controllerRef=useRef(null)
useEffect(() => {
  if (!value) return; // 空值直接返回
  
  // 1️⃣ 设置定时器(防抖)
  const timer = setTimeout(() => {
    
    // 2️⃣ 取消上一次请求
    if (controllerRef.current) {
      controllerRef.current.abort(); // 中断旧请求
    }
    
    // 3️⃣ 创建新的控制器
    const controller = new AbortController();
    controllerRef.current = controller;
    
    // 4️⃣ 发起新请求
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(data => setResults(data))
      .catch(err => {
        // 5️⃣ 过滤 AbortError(正常取消不算错误)
        if (err.name !== 'AbortError') {
          console.log(err);
        }
      });
  }, 300);
  
  // 6️⃣ 清理函数:组件卸载或 value 变化时执行
  return () => {
    clearTimeout(timer);
    if (controllerRef.current) {
      controllerRef.current.abort();
    }
  };
}, [value]);

1. Virtual DOM:为什么需要它?

核心问题:直接操作 DOM 太慢太贵。 真实 DOM 是浏览器内核构造的巨型对象。每操作一次 DOM,浏览器都要:重新计算样式(Recalculate Style),重新布局(Layout / Reflow),重新绘制(Paint),重新合成(Composite)。

频繁操作 = 频繁重排重绘 = 页面卡顿、掉帧。

VDOM: Virtual DOM = 用轻量 JS 对象,模拟真实 DOM 树。

javascript 复制代码
// ❌ 传统方式:每次都操作真实 DOM
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  document.body.appendChild(div); // 触发 1000 次重排/重绘
}

// ✅ React 方式:批量更新
const items = Array.from({ length: 1000 }, (_, i) => <div key={i}>{i}</div>);
// React 会计算最小变更,一次性更新 DOM

Virtual DOM 工作流程:

javascript 复制代码
1. 状态变化 → 2. 生成新 VDOM树 → 3. Diff 算法对比 → 4. 最小化 DOM 操作

实际例子:

javascript 复制代码
// 旧 VNode
const oldVNode = {
  type: 'ul',
  children: [
    { type: 'li', children: 'A' },
    { type: 'li', children: 'B' },
  ]
};

// 新 VNode
const newVNode = {
  type: 'ul',
  children: [
    { type: 'li', children: 'A' }, // 不变
    { type: 'li', children: 'C' }, // 改变
    { type: 'li', children: 'D' }, // 新增
  ]
};

// Diff 结果:只更新第 2 个 li 的文本,新增第 3 个 li

Virtual DOM 整体流程:

  1. 初次渲染 组件生成 VDOM 树 → 渲染成 真实 DOM

  2. 状态更新 组件重新生成 新 VDOM 树

  3. Diff 算法(三大策略)

    • 同层比较:上层变,整棵子树重建
    • 类型比较:类型不同直接重建;类型相同只更属性
    • 列表比较 :用 key 识别节点,最大化复用
  4. 最终更新 React 根据 Diff 结果,只操作变化的真实 DOM,实现最小更新。

key 必须唯一且稳定,绝对不能用 index。


2. Diff 算法:三大策略

策略 1:同层比较(Tree Diff):如果上层 DOM 元素更改,那么上层和下层的 DOM 树会整体新建。

  • 父节点类型变了(div → span)
  • React 直接销毁整棵子树
  • 重新创建全新节点
  • 绝不跨层级移动、复用
javascript 复制代码
// ❌ 不会跨层级比较
<div>              <span>
  <span>A</span>     <span>A</span>
</div>           →  </span>

// React 会:删除 div 和 span,创建新的 span 和 span
// 不会:移动 span 到 span 下

策略 2:类型比较(Component Diff):如果是类型更改,直接替换这部分新的 VDOM 为 DOM。如果类型相同,只改 ClassName,原生 DOM 只更新属性。

javascript 复制代码
// 类型不同 → 直接替换
<div>Hello</div>  →  <span>Hello</span>
// React:删除 div,创建 span

// 类型相同 → 更新属性
<div className="old">Hello</div>  →  <div className="new">Hello</div>
// React:只更新 className

策略 3:列表比较(Element Diff)- key 的作用

没有 key 的问题:

javascript 复制代码
// 旧列表
<ul>
  <li>A</li>
  <li>B</li>
</ul>

// 新列表(在头部插入 C)
<ul>
  <li>C</li>
  <li>A</li>
  <li>B</li>
</ul>

// ❌ 没有 key:React 会认为
// 第 1 个 li: A → C (更新)
// 第 2 个 li: B → A (更新)
// 第 3 个 li: 新增 B (创建)
// 结果:3 次操作

有 key 的优化:

javascript 复制代码
// 旧列表
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

// 新列表
<ul>
  <li key="c">C</li>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

// ✅ 有 key:React 识别出
// key="a" 和 key="b" 没变,只是移动了位置
// key="c" 是新增的
// 结果:1 次插入操作

实战案例:为什么不能用 index 做 key?

javascript 复制代码
function TodoList() {
  const [todos, setTodos] = useState(['A', 'B', 'C']);
  
  // ❌ 错误:用 index 做 key
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input type="checkbox" />
          {todo}
        </li>
      ))}
    </ul>
  );
}

// 问题演示:
// 初始状态:
// [0] A ☐
// [1] B ☐
// [2] C ☐

// 用户勾选 B,然后删除 A:
// [0] B ☑  (原来的 [1])
// [1] C ☐  (原来的 [2])

// React 看到的:
// key=0: A → B (更新文本)
// key=1: B → C (更新文本)
// key=2: 删除

// 结果:checkbox 的选中状态错位了!B 的勾选丢失

正确做法:

javascript 复制代码
// ✅ 用唯一 ID 做 key
{todos.map(todo => (
  <li key={todo.id}>
    <input type="checkbox" />
    {todo.text}
  </li>
))}

3. 生命周期:类组件 vs Hooks

类组件生命周期:

javascript 复制代码
class MyComponent extends React.Component {
  // 1. 挂载阶段
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  componentDidMount() {
    // DOM 已渲染,可以:
    // - 发起请求
    // - 订阅事件
    // - 操作 DOM
    console.log('组件挂载完成');
  }
  
  // 2. 更新阶段
  shouldComponentUpdate(nextProps, nextState) {
    // 返回 false 可以阻止渲染(性能优化)
    return nextState.count !== this.state.count;
  }
  
  componentDidUpdate(prevProps, prevState) {
    // 更新后执行
    if (prevState.count !== this.state.count) {
      console.log('count 变化了');
    }
  }
  
  // 3. 卸载阶段
  componentWillUnmount() {
    // 清理:取消订阅、清除定时器
    console.log('组件即将卸载');
  }
  
  render() {
    return <div>{this.state.count}</div>;
  }
}

Hooks 对应关系:

javascript 复制代码
function MyComponent() {
  const [count, setCount] = useState(0);
  
  // componentDidMount
  useEffect(() => {
    console.log('组件挂载完成');
  }, []); // 空依赖数组 = 只执行一次
  
  // componentDidUpdate (count 变化时重渲染)
  useEffect(() => {
    console.log('count 变化了');
  }, [count]);
  
  // componentWillUnmount
  useEffect(() => {
    return () => {
      console.log('组件即将卸载');
    };
  }, []);
  
 
 // 正常写法:
  useEffect(() => {
    console.log('挂载或 count 变化');
    
    return () => {
      console.log('清理');
    };
  }, [count]);
  
  return <div>{count}</div>;
}

实战案例:WebSocket 订阅:

javascript 复制代码
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    // 订阅,创建实时通信长连接
    const ws = new WebSocket(`ws://api.com/room/${roomId}`);

    //收到后端推送的消息,把消息追加到页面
    ws.onmessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };
    
    // 清理函数:roomId 变化或组件卸载时执行
    return () => {
      ws.close();
      console.log(`断开房间 ${roomId}`);
    };
  }, [roomId]); // roomId 变化时,先执行清理,再重新订阅
  
  return (
    <ul>
      {messages.map((msg, i) => <li key={i}>{msg}</li>)}
    </ul>
  );
}

类组件生命周期:

  • 阶段式:挂载 → 更新 → 卸载
  • 更新全跑,不精准
  • 清理只在卸载

Hooks 生命周期:

  • 响应式:依赖驱动
  • 精准控制执行时机
  • 每次更新前都会清理
  • 代码更简洁、逻辑更内聚

WebSocket + useEffect:

  • 建立实时连接
  • 接收实时消息
  • 依赖变化自动重连
  • 组件卸载自动断开
  • 完美避免内存泄漏

4.并发请求:

场景 叫法 代码方案
多个请求互不依赖,要快 并行 Promise.all
请求之间有先后依赖 串行 .then() 链式
频繁触发,会错乱 竞态 标志位 / AbortController
统称 并发 以上全部

场景 1:多个独立请求并行

javascript 复制代码
function Dashboard() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);
  
  useEffect(() => {
    // ✅ 并发请求,不用等待
    Promise.all([
      fetch('/api/user').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json()),
    ]).then(([userData, postsData, commentsData]) => {
      setUser(userData);
      setPosts(postsData);
      setComments(commentsData);
    });
  }, []);
  
  return <div>...</div>;
}

场景 2:依赖请求(串行)

javascript 复制代码
function UserPosts({ userId }) {
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    // 先获取用户信息,再获取帖子
    fetch(`/api/user/${userId}`)
      .then(r => r.json())
      .then(user => {
        return fetch(`/api/posts?author=${user.name}`);
      })
      .then(r => r.json())
      .then(setPosts);
  }, [userId]);
  
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

场景 3:竞态条件处理

标志位法:

javascript 复制代码
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    let ignore = false; // 标志位
    
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => {
        if (!ignore) { // 只处理最新请求
          setResults(data);
        }
      });
    
    return () => {
      ignore = true; // 清理时标记为忽略
    };
  }, [query]);
  
  return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}

更好的方案:AbortController

Kotlin 复制代码
useEffect(() => {
  const controller = new AbortController();
  
  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err);
      }
    });
  
  return () => controller.abort(); // 取消旧请求
}, [query]);
相关推荐
Qlittleboy2 小时前
<el-form @submit.native.prevent> elementUI的里面的input的元素的回车事件后总是自动提交表单
前端·javascript·elementui
Carsene2 小时前
Docsify 文档缓存问题终极解决方案:拦截请求自动添加版本号
前端·javascript
Linncharm2 小时前
重写一个「年久失修」的开源项目:把 jQuery + CoffeeScript 的 3D 户型图工具迁移到 TypeScript + Three.js r181
前端
竹林8182 小时前
从“后端验证”到“前端签名”:我在Web3项目中重构用户身份认证的实战记录
前端·javascript
农夫山泉不太甜2 小时前
Expo开发App实战指南:从技术选型到架构设计
前端
进击的尘埃2 小时前
Vite 插件开发入门:从零写一个自动生成路由的插件
javascript
高桥凉介发量惊人2 小时前
状态管理与架构篇-异步状态管理:加载、空态、错误态统一处理
前端
清汤饺子2 小时前
搞懂 Cursor 后,我一行代码都不敲了《进阶篇》
前端·javascript·后端
诗句藏于尽头2 小时前
MacBook上的Safari安装油猴插件
前端·safari