导读
笔者在学习React,经常混淆useEffect
和useLayoutEffect
,傻傻分不清楚。接下来简要结合我的个人博客的开发经历从其底层执行和具体影响来具体分析。
执行时机
在浏览器完成(DOM)渲染后,异步执行,不会阻塞主线程。
在浏览器完成(DOM)渲染后,立刻执行,会阻塞主线程,因此会导致性能问题。
使用场景
获取文章数据使用useEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import React, { useEffect, useState } from 'react';
function DataFetchingExample() { const [data, setData] = useState(null);
useEffect(() => { fetch('https://jsonplaceholder.typicode.com/posts/1') .then((response) => response.json()) .then((json) => { setData(json); }); }, []);
return ( <div> {data ? ( <div> <h2>{data.title}</h2> <p>{data.body}</p> </div> ) : ( <p>Loading...</p> )} </div> ); }
export default DataFetchingExample;
|
订阅事件和取消订阅使用useEffect
当需要监听窗口大小变化时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React, { useEffect, useState } from 'react';
function EventSubscriptionExample() { const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => { const handleResize = () => { setWindowWidth(window.innerWidth); };
window.addEventListener('resize', handleResize);
return () => { window.removeEventListener('resize', handleResize); }; }, []);
return <p>Window width: {windowWidth}px</p>; }
export default EventSubscriptionExample;
|
文章渲染后立刻高亮的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React, { useLayoutEffect, useRef } from 'react'; import Prism from 'prismjs'; import 'prismjs/themes/prism-tomorrow.css';
function BlogPost({ content }) { const contentRef = useRef(null);
useLayoutEffect(() => { if (contentRef.current) { Prism.highlightAllUnder(contentRef.current); } }, [content]);
return ( <div ref={contentRef}> <div dangerouslySetInnerHTML={{ __html: content }} /> </div> ); }
export default BlogPost;
|
图片加载完成前显示占位符使用useLayoutEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React, { useLayoutEffect, useState, useRef } from 'react';
function LazyImage({ src, alt }) { const [isLoaded, setIsLoaded] = useState(false); const imgRef = useRef(null);
useLayoutEffect(() => { const img = imgRef.current; if (img) { img.onload = () => { setIsLoaded(true); }; img.src = src; } }, [src]);
return ( <div style={{ position: 'relative', width: '100%', height: '100%' }}> {!isLoaded && ( <div style={{ background: '#eee', width: '100%', height: '200px' }}> Loading... </div> )} <img ref={imgRef} src={src} alt={alt} style={{ display: isLoaded ? 'block' : 'none', width: '100%' }} /> </div> ); }
export default LazyImage;
|
文章目录(TOC)滚动定位使用useLayoutEffect
在文章定位后立刻绑定ref,确保目录项和文章标题位置对应正确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import React, { useLayoutEffect, useRef } from 'react';
function TableOfContents({ headings }) { const headingRefs = useRef([]);
useLayoutEffect(() => { headingRefs.current = headingRefs.current.slice(0, headings.length); }, [headings]);
const scrollToHeading = (index) => { const heading = headingRefs.current[index]; if (heading) { heading.scrollIntoView({ behavior: 'smooth' }); } };
return ( <div> <ul> {headings.map((heading, index) => ( <li key={index} onClick={() => scrollToHeading(index)}> {heading.text} </li> ))} </ul> {headings.map((heading, index) => ( <h2 key={index} ref={(el) => (headingRefs.current[index] = el)} id={`heading-${index}`} > {heading.text} </h2> ))} </div> ); }
export default TableOfContents;
|
文章锚点自动滚动使用useLayoutEffect
文章页面加载后立刻滚动到指定位置,避免用户看到页面跳动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, { useLayoutEffect } from 'react';
function BlogPost({ content }) { useLayoutEffect(() => { const hash = window.location.hash; if (hash) { const element = document.querySelector(hash); if (element) { element.scrollIntoView(); } } }, []);
return ( <div> <div dangerouslySetInnerHTML={{ __html: content }} /> </div> ); }
export default BlogPost;
|
文章字体随着内容宽度和长度排版使用useLayoutEffect
防止用户看到字体的大小变化过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import React, { useLayoutEffect, useRef } from 'react';
function BlogPost({ content }) { const contentRef = useRef(null);
useLayoutEffect(() => { if (contentRef.current) { const paragraphs = contentRef.current.querySelectorAll('p'); paragraphs.forEach((p) => { if (p.textContent.length > 500) { p.style.fontSize = '14px'; } else { p.style.fontSize = '16px'; } }); } }, [content]);
return ( <div ref={contentRef}> <div dangerouslySetInnerHTML={{ __html: content }} /> </div> ); }
export default BlogPost;
|
文章内容中的数学公式渲染使用useLayoutEffect
确保用户立刻看到渲染完成的可读的数学公式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import React, { useLayoutEffect, useRef } from 'react'; import katex from 'katex'; import 'katex/dist/katex.min.css';
function BlogPost({ content }) { const contentRef = useRef(null);
useLayoutEffect(() => { if (contentRef.current) { const mathElements = contentRef.current.querySelectorAll('.math'); mathElements.forEach((element) => { katex.render(element.textContent, element, { throwOnError: false, }); }); } }, [content]);
return ( <div ref={contentRef}> <div dangerouslySetInnerHTML={{ __html: content }} /> </div> ); }
export default BlogPost;
|
总结
useEffect
钩子常常用于异步任务:数据异步获取、事件订阅、解除订阅时;而``useLayoutEffect`用于立刻渲染,常用于立刻渲染的DOM操作、视觉优化(图片懒加载、文字缩放)以及交互优化(锚点定位等)