useEffect处理副作用,诸如dom交互或者请求api等
基本使用
useEffect(() => {doSomething(a, b);},[dependency],);
第一个参数是function ,称之为side-effect function
第二个参数是依赖,称之为dependency array
函数式组件是没有生命周期钩子的,通过useEffect来模拟各个生命周期,我们可以在useEffect中处理side-effect。
依赖数组在判断元素是否发生改变时使用了 Object.is 进行比较,因此当 deps 中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而每次都会触发 Effect,失去了 deps 本身的意义。
渲染机制

动画中有以下需要注意的点:
- 每个 Effect 必然在渲染之后执行,因此不会阻塞渲染,提高了性能
- 在运行每个 Effect 之前,运行前一次渲染的 Effect Cleanup 函数(如果有的话)
- 当组件销毁时,运行最后一次 Effect 的 Cleanup 函数
提示
将 Effect 推迟到渲染完成之后执行是出于性能的考虑,如果你想在渲染之前执行某些逻辑(不惜牺牲渲染性能),那么可使用 useLayoutEffect 钩子,使用方法与 useEffect 完全一致,只是执行的时机不同。
再来看看 useEffect 的第二个参数:deps (依赖数组)。从上面的演示动画中可以看出,React 会在每次渲染后都运行 Effect。而依赖数组就是用来控制是否应该触发 Effect,从而能够减少不必要的计算,从而优化了性能。具体而言,只要依赖数组中的每一项与上一次渲染相比都没有改变,那么就跳过本次 Effect 的执行。
仔细一想,我们发现 useEffect 钩子与之前类组件的生命周期相比,有两个显著的特点:
- 将初次渲染(componentDidMount)、重渲染(componentDidUpdate)和销毁(componentDidUnmount)三个阶段的逻辑用一个统一的 API 去解决
- 把相关的逻辑都放到一个 Effect 里面(例如 setInterval 和 clearInterval),更突出逻辑的内聚性
特别的,如果在useEffect中请求 不要写成下面这种:
useEffect(async () => {const response = await fetch('...');// ...}, []);
强烈建议你不要这样做。useEffect 约定 Effect 函数要么没有返回值,要么返回一个 Cleanup 函数。而这里 async 函数会隐式地返回一个 Promise,直接违反了这一约定,会造成不可预测的结果。
最佳实践
在 effect 内部 去声明它所需要的函数:
如果effect中的函数在effect外部定义,并且这个函数依赖了props或者state,effect很难记住这个函数的依赖,最好将函数定义在effect中
// bad,不推荐function Example({ someProp }) {function doSomething() {console.log(someProp);}useEffect(() => {doSomething();}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}// good,推荐function Example({ someProp }) {useEffect(() => {function doSomething() {console.log(someProp);}doSomething();}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}
如果处于某些原因无法把一个函数移动到 effect 内部,还有一些其他办法:
可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了;
万不得已的情况下,可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变;
模拟生命周期
下面我们来通过示例展示useEffect的生命周期
import * as React from 'react';const App = () => {const [toggle, setToggle] = React.useState(true);const handleToggle = () => {setToggle(!toggle);};return <Toggler toggle={toggle} onToggle={handleToggle} />;};const Toggler = ({ toggle, onToggle }) => {return (<div><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>Hello React</div>}</div>);};export default App;
如上,通过一个简单的例子来探究函数式的生命周期,父组件管理状态,子组件通过回调函数来更新状态
const Toggler = ({ toggle, onToggle }) => {React.useEffect(() => {console.log('I run on every render: mount + update.');});return (<div><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>Hello React</div>}</div>);};
这是最基本最直接的effect用法,只专递给effect一个函数,这时,函数会在组件第一次render的时候和每一次re-render的时候被调用
it runs on the first render of the component (also called on mount or mounting of the component) and on every re-render of the component (also called on update or updating of the component).
挂载
如果你想仅仅在组件渲染的时候执行一次,可以传给useEffect第二参数空数组。
const Toggler = ({ toggle, onToggle }) => {React.useEffect(() => {console.log('I run only on the first render: mount.');}, []);return (<div><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>Hello React</div>}</div>);};
第二个参数是一个数组,我们称之为dependency array。
这里是一个空数组,如果dependency array是空数组,意味着side-effect function 仅仅在render的时候执行一次
组件更新(挂载+更新)
const Toggler = ({ toggle, onToggle }) => {React.useEffect(() => {console.log('I run only if toggle changes (and on mount).');}, [toggle]);return (<div><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>Hello React</div>}</div>);};
我们给依赖数组传了一个参数toggle,意味着,只有toggle发生变化的时候,side-effect function才会被调用。注意在组件mout的时候也调用了side-effect function
我们也可以给依赖传递多个参数
const Toggler = ({ toggle, onToggle }) => {const [title, setTitle] = React.useState('Hello React');React.useEffect(() => {console.log('I run if toggle or title change (and on mount).');}, [toggle, title]);const handleChange = (event) => {setTitle(event.target.value);};return (<div><input type="text" value={title} onChange={handleChange} /><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>{title}</div>}</div>);};
这时,title和toggle变化都会调用side-effect function
仅更新
上面的例子,添加依赖,但是mount挂载的时候也会触发,如何实现仅仅某个依赖更新的时候触发呢?
https://stackblitz.com/edit/react-f46vcr
const Toggler = ({ toggle, onToggle }) => {const didMount = React.useRef(false);console.log(didMount);React.useEffect(() => {if (didMount.current) {console.log("I run only if toggle changes.");} else {didMount.current = true;}}, [toggle]);return (<div><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>Hello React</div>}</div>);};
我们通过useRef来模拟,在mount的时候,calledOnce 为false,通过控制calledOnce来实现update的生命周期
如果对ref还不清楚的的同学,可以移步 你不知道的ref
仅一次更新
我们知道,可以通过给useEffect传递一个空数组可以实现组件在mount的时候执行一次,那么,如果我只想在某个值变化的时候执行一次,该怎么操作呢?上代码:
https://stackblitz.com/edit/react-xstfnw
const Toggler = ({ toggle, onToggle }) => {const calledOnce = React.useRef(false);React.useEffect(() => {if (calledOnce.current) {return;}if (toggle === false) {console.log('I run only once if toggle is false.');calledOnce.current = true;}}, [toggle]);return (<div><button type="button" onClick={onToggle}>Toggle</button>{toggle && <div>Hello React</div>}</div>);};
卸载
import * as React from 'react';const App = () => {const [timer, setTimer] = React.useState(0);React.useEffect(() => {const interval = setInterval(() => setTimer(timer + 1), 1000);return () => clearInterval(interval);}, [timer]);return <div>{timer}</div>;};export default App;
通过返回一个函数来实现清除定时器
同时也验证了unmount,清除定时器的触发是在每次组件消除或者重新渲染的时候。
useLayoutEffect
useLayoutEffect和useEffect大部分场景下用法是一样的
区别是:
useEffect:dom更新,浏览器绘制之后执行,不会阻塞渲染。
useLayoutEffect: dom更新之后,浏览器绘制之前执行,会阻塞浏览器渲染。
import React, { useEffect, useLayoutEffect } from "react";import "./style.css";export default function App() {const [count, setCount] = React.useState(0);const ref = React.useRef();const moveTo = (dom, delay, postion) => {dom.style.transform = `translate(${postion.x}px)`;dom.style.transition = `left ${delay}ms`;};// useEffect(() => {// moveTo(ref.current, 600, { x: 200 });// }, []);useLayoutEffect(() => {moveTo(ref.current, 600, { x: 200 });}, []);console.log("RE-RENDER");return (<div ref={ref} style={{ width: 100, height: 100, backgroundColor: "red" }}>方块</div>);}
在 useEffect 里面会让这个方块往后移动 600px 距离,可以看到这个方块在移动过程中会闪一下。但如果换成了 useLayoutEffect 呢?会发现方块不会再闪动,而是直接出现在了 600px 的位置。
原因:
useEffect 是在浏览器绘制之后执行的,所以方块一开始就在最左边,于是我们看到了方块移动的动画。useLayoutEffect 是在绘制之前执行的,会阻塞页面的绘制,页面会在 useLayoutEffect 里面的代码执行结束后才去继续绘制,于是方块就直接出现在了右边。
例2:
import React, { useEffect, useLayoutEffect } from "react";import "./style.css";export default function App() {const [count, setCount] = React.useState(0);let ref = React.useRef(0);useEffect(() => {ref.current = "some value";});useEffect(() => {console.log("useEffect", ref.current);});// then, later in another hook or somethinguseLayoutEffect(() => {console.log("useLayoutEffect", ref.current); // <-- this logs an old value because this runs first!});return <div>Hello,React</div>;}

从打印结果可以看到:
先执行了useLayoutEffect,拿到的是ref的旧值
后执行useEffect,拿到的是更新后的ref值
应用场景:
- 如果改变了dom(获取元素的滚动位置或其他样式),立刻看到改变
- 拿到ref的旧值
setInterval
https://raoenhui.github.io/react/2019/11/07/hooksSetinterval/
const [count, setCount] = useState(0);const myRef = React.useRef(0);useEffect(() => {const id = setInterval(() => {myRef.current += 1;setCount(myRef.current); // 是为了更新页面console.log('监听', myRef.current);}, 1000);//当[] 不会走return () => {console.log('卸载', myRef.current);clearInterval(id);};}, []);
const [count, setCount] = useState(0);const myRef = React.useRef(null);myRef.current = () => {setCount(count + 1);};useEffect(() => {const id = setInterval(() => {myRef.current();// console.log('监听', myRef.current);}, 1000);return () => {console.log('卸载');clearInterval(id);};}, []);
const [count, setCount] = useState(0);function useInterval(fn) {const myRef = useRef(null);myRef.current = fn;useEffect(() => {const id = setInterval(() => {myRef.current();}, 1000);return () => clearInterval(id);}, []);}useInterval(() => setCount(count + 1));
const [count, setCount] = useState(0);function useInterval(fn, delay) {const myRef = useRef(null);useEffect(() => {myRef.current = fn;}, [fn]);useEffect(() => {const id = setInterval(() => {myRef.current();}, delay);return () => clearInterval(id);}, [delay]);}useInterval(() => setCount(count + 1), 1000);
重点总结
- 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快;(componentDidMount 或 componentDidUpdate 会阻塞浏览器更新屏幕)
- useLayoutEffect 和平常写的 Class 组件的 componentDidMount 和 componentDidUpdate 同时执行;
References
https://segmentfault.com/a/1190000018224631
