面试官问我useEffect和useLayoutEffect的区别,我掏出了外卖订单…

878 阅读3分钟

🍱 面试现场:外卖引发的血案

面试官:(突然亮出手机)你看这个外卖App,下单后页面要刷新数据。用useEffect还是useLayoutEffect

:(战术微笑)这得看您是想要饿肚子等大餐(useEffect)还是厨房现场监工(useLayoutEffect)!

面试官:(怒点送达按钮)说人话!

:如果数据刷新不怕页面闪动就用useEffect,要避免UI抖动就用useLayoutEffect


🕰️ 核心差异:React的时空法则

1. 执行时机对比

// 浏览器渲染流水线
1. 组件渲染 → 2. DOM更新 → 3. 浏览器绘制 → 4. useEffect执行
                                      ↗
useLayoutEffect插队 → 2.5. useLayoutEffect执行

2. 可视化比喻

useEffectuseLayoutEffect
性格慢性子外卖小哥急性子急诊医生
工作方式等页面画完再干活必须立刻马上处理
适用场景数据获取/日志记录DOM尺寸测量/紧急调整

🔍 一、执行流程深度解剖

实验1:闪烁的副作用

function BlinkComponent() {
  const [width, setWidth] = useState(0);
  const divRef = useRef();

  useLayoutEffect(() => {
    // 在绘制前测量
    setWidth(divRef.current.offsetWidth);
  }, []);

  useEffect(() => {
    // 这里测量会导致二次渲染抖动
    // setWidth(divRef.current.offsetWidth);
  }, []);

  return <div ref={divRef}>宽度:{width}px</div>;
}

现象

  • useLayoutEffect:宽度直接正确显示
  • useEffect:先显示0,再闪动到实际值

实验2:事件监听竞速赛

useEffect(() => {
  const handleClick = () => console.log('Effect监听');
  document.addEventListener('click', handleClick);
  return () => document.removeEventListener('click', handleClick);
}, []);

useLayoutEffect(() => {
  const handleClick = () => console.log('Layout监听');
  document.addEventListener('click', handleClick);
  return () => document.removeEventListener('click', handleClick);
}, []);

打印顺序

  1. Layout监听 ← 先绑定
  2. Effect监听 ← 后绑定
    执行顺序LayoutEffect清理 → LayoutEffect执行 → Effect清理 → Effect执行

🕵️ 二、获取前朝遗老:上一次的state

方案1:useRef时空胶囊

function Component() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count; // 本次渲染结束后保存
  });

  return (
    <div>
      当前:{count},之前:{prevCountRef.current}
    </div>
  );
}

方案2:自定义Hook时间机器

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value; // 在effect中更新
  });
  return ref.current;
}

// 使用
const prevCount = usePrevious(count);

方案3:函数式更新考古

const [user, setUser] = useState({ id: 1, name: '老王' });

// 修改时捕获旧值
const updateUser = () => {
  setUser(prevUser => {
    console.log('旧值:', prevUser);
    return { ...prevUser, name: '隔壁老王' };
  });
};

💥 三、高频灵魂拷问

Q1:两者会阻塞渲染吗?

// useLayoutEffect会!
// 伪代码实现
function commitWork() {
  commitDOMUpdates(); // 更新DOM
  flushLayoutEffects(); // 同步执行LayoutEffect
  requestPaint(); // 通知浏览器绘制
  scheduleEffects(); // 异步调度Effect
}

结论

  • useLayoutEffect:同步执行,可能阻塞渲染
  • useEffect:异步执行,不阻塞

Q2:服务端渲染(SSR)能用吗?

// 服务端会警告!
function ServerComponent() {
  useLayoutEffect(() => { 
    // 服务端没有DOM,这里会报错
    console.log(document.body); 
  }, []);
}

逃生方案

const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Q3:如何避免无限循环?

// 危险操作
useEffect(() => {
  setCount(count + 1); // 每次执行都改依赖项
}, [count]);

// 正确做法
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1); // 函数式更新不依赖count
  }, 1000);
  return () => clearInterval(timer);
}, []); // 空依赖

🚀 四、性能优化黑魔法

1. 副作用防抖术

useEffect(() => {
  const handler = setTimeout(() => {
    // 延迟执行避免高频更新
    fetchData(query);
  }, 500);

  return () => clearTimeout(handler);
}, [query]); // query变化时重置定时器

2. 依赖数组缩骨功

const [user, setUser] = useState({ id: 1, name: '老王' });

useEffect(() => {
  // 只关心id变化
}, [user.id]); // 而不是整个user对象

3. 清理函数分身术

useLayoutEffect(() => {
  const element = ref.current;
  const observer = new ResizeObserver(handleResize);
  observer.observe(element);

  return () => {
    observer.unobserve(element); // 精准清理
    observer.disconnect();
  };
}, []);

🎙️ 面试官の终极大招

面试官:(突然掏出VR眼镜)如果在这个虚拟列表组件里,同时用useEffectuseLayoutEffect处理滚动位置,会发生什么?

:(戴上眼镜)这得看VR设备的渲染管线!如果是WebGL渲染:

  1. useLayoutEffect:在DOM更新后立即修正位置,避免视觉错位
  2. useEffect:可能等到下一帧才调整,用户会看到闪烁

不过...(突然摘掉眼镜)您这眼镜根本没开机啊!


后记:后来在真实项目中优化滚动列表时,发现useLayoutEffect配合requestAnimationFrame才是王道。就像外卖准时达,技术选型也要卡准时机!(完)