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 something
useLayoutEffect(() => {
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