跳到主要内容

[Medium] React useEffect 与 Virtual DOM

1. What is useEffect?

什么是 useEffect

核心概念

useEffect 是 React 函数组件中负责管理副作用(side effects)的 Hook。它会在组件渲染之后执行异步数据请求、订阅、DOM 操作或手动同步状态,对应 class 组件的 componentDidMountcomponentDidUpdatecomponentWillUnmount 等生命周期方法。

常见用途

  • 获取远程数据并更新组件状态
  • 维护订阅或事件监听(如 resizescroll
  • 与浏览器 API 交互(如更新 document.title、操作 localStorage
  • 清除前一次渲染遗留的资源(如取消请求、移除监听器)
点此展开基本使用示例
import { useEffect, useState } from 'react';

function Example() {
const [count, setCount] = useState(0);

useEffect(() => {
document.title = `点击次数:${count}`;
});

return (
<button type="button" onClick={() => setCount((prev) => prev + 1)}>
点我
</button>
);
}

2. When does useEffect run?

useEffect 何时会执行?

useEffect 的第二个参数是依赖数组(dependency array),用来控制副作用的执行时机。React 会逐一比较数组中的每个值,在检测到变化时重新执行副作用,并于下一轮执行前触发清除函数。

2.1 常见依赖模式

// 1. 每次渲染后执行(含第一次)
useEffect(() => {
console.log('任意 state 改变都会触发');
});

// 2. 仅在初次渲染时执行一次
useEffect(() => {
console.log('只会在 component mount 时执行');
}, []);

// 3. 指定依赖变量
useEffect(() => {
console.log('仅在 selectedId 改变时触发');
}, [selectedId]);

2.2 清除函数与资源回收

useEffect(() => {
const handler = () => {
console.log('监听中');
};

window.addEventListener('resize', handler);

return () => {
window.removeEventListener('resize', handler);
console.log('解除监听');
};
}, []);

上述示例运用清除函数解除事件监听。React 会在组件卸载或依赖变量更新之前先执行清除函数,确保不留下内存泄漏与重复监听。

3. What is the difference between Real DOM and Virtual DOM?

真实 DOM 与虚拟 DOM 的差异是什么?

比较维度Real DOM(真实 DOM)Virtual DOM(虚拟 DOM)
结构由浏览器维护的实体节点由 JavaScript 对象描述的节点
更新成本直接操作会触发排版与重绘,成本高先计算差异再批次应用,成本低
更新策略立即反映至画面先在内存建立新树再比较差异
扩展性需手动控制更新流程可以插入中间逻辑(Diff、批次)

React 为何采用虚拟 DOM

// 简化流程示意(非实际 React 源码)
function renderWithVirtualDOM(newVNode, container) {
const prevVNode = container.__vnode;
const patches = diff(prevVNode, newVNode);
applyPatches(container, patches);
container.__vnode = newVNode;
}

虚拟 DOM 让 React 可以先在内存中进行 Diff,取得最小化的更新清单,再一次同步至真实 DOM,避免频繁的重排与重绘。

4. How to coordinate useEffect and Virtual DOM?

useEffect 与虚拟 DOM 如何协作?

React 的渲染流程分成 Render Phase 与 Commit Phase。useEffect 与虚拟 DOM 的配合重点在于:副作用必须等待真实 DOM 更新完成后才能执行。

Render Phase(渲染阶段)

  • React 建立新的虚拟 DOM,并计算上一版虚拟 DOM 的差异
  • 此阶段是纯函数运算,可被中断或重新执行

Commit Phase(提交阶段)

  • React 将差异应用到真实 DOM
  • useLayoutEffect 会在此阶段同步执行,以确保 DOM 已更新

Effect Execution(副作用执行时机)

  • useEffect 会在 Commit Phase 结束、浏览器绘制完成后执行
  • 这样可以避免副作用阻塞画面更新,提升用户体验
useEffect(() => {
const controller = new AbortController();

fetch('/api/profile', { signal: controller.signal })
.then((res) => res.json())
.then(setProfile)
.catch((error) => {
if (error.name !== 'AbortError') {
console.error('加载失败', error);
}
});

return () => {
controller.abort(); // 确保在依赖变量更新或组件卸载时取消请求
};
}, [userId]);

5. Quiz Time

小测验时间 模拟面试情境

题目:请说明下列代码的执行顺序并写出输出结果

import { useEffect, useState } from 'react';

function Demo() {
const [visible, setVisible] = useState(false);

useEffect(() => {
console.log('effect 1');
return () => {
console.log('cleanup 1');
};
});

useEffect(() => {
console.log('effect 2');
}, [visible]);

return (
<>
<p>状态:{visible ? '显示' : '隐藏'}</p>
<button type="button" onClick={() => setVisible((prev) => !prev)}>
切换
</button>
</>
);
}
点击查看答案
  • 初次渲染后依序输出 effect 1effect 2。第一个 useEffect 没有依赖数组,第二个 useEffect 依赖 visible,初始值为 false 仍会执行一次。
  • 切换按钮后触发 setVisible,下一轮渲染会先执行上一轮的清除函数,输出 cleanup 1,再执行新的 effect 1effect 2
  • 由于 visible 每次切换都会改变,effect 2 会在每次切换后重新执行。

最终输出顺序为:effect 1effect 2 → (点击后)cleanup 1effect 1effect 2

6. Best Practices

最佳实践

推荐做法

  • 审慎维护依赖数组,搭配 ESLint 规则 react-hooks/exhaustive-deps
  • 依据职责拆分多个 useEffect,减少大型副作用造成的耦合。
  • 在清除函数中释放监听器或取消异步请求,避免内存泄漏。
  • 需要在 DOM 更新后立即读取布局信息时改用 useLayoutEffect,但要评估性能影响。

示例:拆分不同职责

useEffect(() => {
document.title = `当前用户:${user.name}`;
}, [user.name]); // 管理 document.title

useEffect(() => {
const subscription = chatClient.subscribe(roomId);
return () => subscription.unsubscribe();
}, [roomId]); // 管理聊天室连接

7. Interview Summary

面试总结

快速复习

  1. useEffect 通过依赖数组控制执行时机,清除函数负责资源回收。
  2. 虚拟 DOM 通过 Diff 算法找出最小更新集,减少真实 DOM 操作成本。
  3. 理解 Render Phase 与 Commit Phase,可以精准回答副作用与渲染流程的关联。
  4. 面试延伸可补充性能提升策略,例如批次更新、懒加载与 memoization。

面试回答模板

"React 在渲染时会先建立虚拟 DOM,计算差异后才进入 Commit Phase 更新真实 DOM。useEffect 会在提交完成、浏览器绘制后执行,因此适合处理异步请求或事件监听。只要维护正确的依赖数组并记得清除函数,就能避免内存泄漏与竞态问题。"

Reference

参考资料