react Effect副作用 - 避免滥用Effect
react Effect副作用
基础概率
阅读文章
React新文档:不要滥用effect哦
react 中文文档
什么是纯函数? 什么是副作用函数?
纯函数
仅执行计算操作,不做其他操作,这类函数通常被称为纯函数。
纯函数的特征
1.只负责自己的任务,它不会更改在该函数调用前就已存在的对象或变量。
2.输入相同,则输出相同,给定相同的输入,纯函数应总是返回相同的结果。
React
便围绕着这个概念进行设计,假设编写的所有组件都是纯函数。
React
的渲染过程必须自始至终是纯粹的,不改变在渲染前,就已存在的任何对象或变量。 – 这将会使其变得不纯粹,也就是我们说的产生副作用。
/*
案例1:不纯粹组件的写法
该组件正在读写其外部声明的 guest 变量。这意味着 多次调用这个组件会产生不同的 JSX!并且,如果 其他 组件读取 guest ,它们也会产生不同的 JSX,其结果取决于它们何时被渲染!这是无法预测的。
*/
let guest = 0;
function Cup() {
// Bad:正在更改预先存在的变量!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
// 案例2:纯粹组件的写法
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
React 提供了 “严格模式”,在严格模式下开发时,会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。严格模式在生产环境下不生效,因此不会降低应用程序的速度。如需引入严格模式,可以用 <React.StrictMode> 包裹根组件。
副作用函数
在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在组件内部定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数。
可以理解副作用为:额外发生的事情,与渲染过程无关。
如果无法为副作用找到合适的事件处理程序,可以选择使用useEffect
。
什么时候使用Effect
React
中有两个重要的概念
Rendering code
渲染代码是不带副作用的纯函数,开发者编写的组件渲染逻辑,最终会返回一段JSX。// 渲染代码 function App() { const [age, setAge] = useState(10); return <div>{age}</div>; } // 包含副作用:在 React 中,JSX 的渲染必须是纯粹操作,不应该包含任何像修改 DOM 的副作用。 import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); // 渲染期间不能调用 `play()`,获取不到ref.current的值。 } else { ref.current.pause(); // 同样,调用 `pause()` 也不行。 } return <video ref={ref} src={src} loop playsInline />; }
Event handlers
事件处理器是组件内部的函数,用于执行用户操作,可以包含副作用。function App() { const [age, setAge] = useState(10); const changAge = () => { setAge(11); } return <div onClick={changAge }>{age}</div>; }
但是并不是所有副作用都能在Event handlers
事件处理器中解决,比如初始化进入页面之后需要请求数据,也就是说不是由用户触发的可以让useEffect
处理。
所以使用Event handlers
还是useEffect
的一个思路是:判断需求是否由用户行为触发
如何使用Effect
每个React组件都经历相同的生命周期
1.当组件被添加到屏幕上时,会进行组件的 挂载。
2.当组件接收到新的 props
或 state
时,通常是作为对交互的响应,它会进行组件的更新。
3.当组件从屏幕上移除时,它会进行组件的卸载。
使用说明
1.useEffect
的参数只能是一般函数,不能是异步函数(async)。如果在useEffect
里使用异步函数请求数据,需要其外部包装一个一般函数并调用。
2.默认情况下,Effect
会在每次渲染后都会执行。如果添加依赖,当依赖发生变化时Effect
会执行。
3.useEffect
参数函数会在组件每次渲染完毕(dom渲染完毕)后执行。
4.在useEffect的回调函数中,可以返回一个函数,该函数被称为清理函数,该函数会在下次Effect
执行前调用。可以在清理函数中,清除上一次Effect执行所带来的影响。
// 初始化 先Effect回调再清理函数
// 其他情况,先清理函数再Effect
const [keyword,setKeyword] = useState();
useEffect(()=>{
const timer = setTimeout();//初始化时,先设置一个定时器A
// 清理函数
return ()=>{
/*
这里形成了一个闭包,timer是定时器A的值。
下一次Effect执行前,先清理定时器A再生成新的定时器
*/
clearTimeout(timer);
}
},[keyword])
}
避免滥用Effect
核心:Effect
通常用于暂时“跳出” React
代码并与一些外部系统进行同步。
思路
1.能使用Event handlers
的优先使用Event handlers
,useEffect
是最后的选择。
2.可以在渲染时期进行的计算,就在渲染期间执行。
根据 props 或 state 来更新 state
先是用 fullName 的旧值执行了整个渲染流程,然后useEffect
修改了fullName
立即使用更新后的值又重新渲染了一遍。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 避免:多余的 state 和不必要的 Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
如果一个值可以基于现有的 props
或 state
计算得出,不要把它作为一个 state
,而是在渲染期间直接计算这个值。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ 非常好:在渲染期间进行计算
const fullName = firstName + ' ' + lastName;
// ...
}
当 props 变化时重置所有 state
一个ProfilePage
组件,它接收一个userId
代表当前正在操作的用户,里面有一个评论输入框,用一个state
来记录输入框中的内容。为了防止切换用户后,原用户输入的内容被当前的用户发出这种误操作,有必要在userId
改变时置空state
,包括ProfilePage
组件的所有子组件中的评论state
。
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 避免:当 prop 变化时,在 Effect 中重置 state
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
当在相同的位置渲染相同的组件时,React 会保留状态。通过组件的key
来判断当前的组件是否相同,每当 key
(这里是 userId)变化时,React
将重新加载组件。
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
const [comment, setComment] = useState('');
// ...
}
将数据传递给父组件
父组件:将修改state
的方法传递给子组件
子组件:调用修改state
的方法
在 React 中,数据从父组件流向子组件,当子组件在Effect
中更新其父组件的 state
时,数据流变得非常难以追踪。
// 不是很理解怎么会有这种写法??
function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}
function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 避免:在 Effect 中传递数据给父组件
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}
如果组件和父组件都需要相同的数据,那么可以让父组件获取那些数据,并将其向下传递给子组件
function Parent() {
const data = useSomeAPI();
// ...
// ✅ 非常好:向子组件传递数据
return <Child data={data} />;
}
function Child({ data }) {
// ...
}
获取异步数据
非常常见的一种写法是在effect
中异步获取数据,但这种代码存在一个问题
假设快速地输入 “hello”。那么 query 会从 “h” 变成 “he”,“hel”,“hell” 最后是 “hello”。这会触发一连串不同的数据获取请求,但无法保证对应的返回顺序。例如,“hell” 的响应可能在 “hello” 的响应 之后 返回。这种情况被称为 竞态条件:两个不同的请求 “相互竞争”,并以与你预期不符的顺序返回。
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 🔴 避免:没有清除逻辑的获取数据
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
可以给Effect
添加一个清理函数,来忽略较早的返回结果。下面的案例采用一个变量ignore
来控制这个Effect
回调的"有效性",只要是执行了下一个Effect
回调,上一个的ignore
就变成了true
,此时如果刚好上一个Effect
的请求结束,由于ignore=true
会跳过setResults
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}