一 使用ref引用值
当你希望组件“记住”某些信息,但又不想让这些信息触发新的渲染时,可以使用 ref 。
我们先来看看函数组件里的普通变量、ref引用值和状态变量
const Child = memo((props) => {
console.log("组件重新渲染");
let sum = 0;
const sumRef = useRef(0);
const handleClick = () => {
sum += 1;
console.log("sum:", sum);
sumRef.current += 1;
console.log("sumRef:", sumRef.current);
};
const [count, setCount] = useState(0);
const handleClick2 = () => {
console.log("改变状态");
setCount(count + 1);
};
return (
<>
<div onClick={() => handleClick()}>改变非状态</div>
<div onClick={() => handleClick2()}>改变状态</div>
{/* <div>{count}</div> */}
</>
);
});
- 状态变量:每次修改状态会触发函数组件重新调用,组件重新渲染,状态值会被记住。(需要重新触发渲染用)
- ref引用值:ref引用值每次修改不会引发组件重新渲染,ref引用值会被记住。(中间状态,在需要记住其值且不触发重新渲染用)
-
普通变量:普通变量修改不会引发组价重新渲染,在组件每次渲染之前,可以任意修改其值,但是组件每次由于props、state变化导致组价重新渲染(函数重新调用),如果普通变量设置了初始值,则会恢复到初始值。(每次重新渲染不需要记住其值用)
二 使用ref操作dom
有时可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置等。
2.1. 基本使用
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
2.2. 与map配合使用
借用集合方式实现
const Child = memo((props) => {
const [list, setList] = useState([
{
id: 1,
name: "zs",
age: 20,
},
{
id: 2,
name: "ls",
age: 20,
},
]);
const divRef = useRef(null);
const getMap = () => {
if (!divRef.current) {
divRef.current = new Map();
}
return divRef.current;
};
const handleClick = (id) => {
const map = getMap();
const node = map.get(id);
node.scrollIntoView();
};
return (
<>
<button onClick={() => handleClick(1)}>zs按钮</button>
<button onClick={() => handleClick(2)}>ls按钮</button>
{list.map((item, index) => {
return (
<div
key={item.id}
style={{
height: "100vh",
backgroundColor: "red",
borderBottom: "2px solid green",
}}
ref={(node) => {
const map = getMap();
if (node) {
map.set(item.id, node);
} else {
map.delete(item.id);
}
}}
>
{item.name}
</div>
);
})}
</>
);
});
2.3. React 何时添加 refs
- 在 渲染 阶段, React 调用你的组件来确定屏幕上应该显示什么。 在第一次渲染期间,DOM 节点尚未创建,因此
ref.current
将为null
。在渲染更新的过程中,DOM 节点还没有更新。所以读取它们还为时过早 - 在 提交 阶段, React 把变更应用于 DOM。更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。
注:通常从事件处理器访问 refs。 如果想使用 ref 执行某些操作,但没有特定的事件可以执行此操作,你可能需要一个 effect,在页面渲染完成再执行。
2.4. 使用 refs 操作 DOM 的注意事项
注意,在我们使用refs操作DOM时候,要注意避免和state控制的节点产生冲突
import { useState, useRef } from 'react';
export default function Counter() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={() => {
setShow(!show);
}}>
通过 setState 切换
</button>
<button
onClick={() => {
ref.current.remove();
}}>
从 DOM 中删除
</button>
{show && <p ref={ref}>Hello world</p>}
</div>
);
}
三 使用 Effect 同步
Effects 会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。允许你指定由渲染本身,而不是特定事件引起的副作用-----由渲染引起的副作用。
3.1. 如何编写Effect
- 声明 Effect。
import { useEffect } from 'react'; function MyComponent() { useEffect(() => { // 每次渲染后都会执行此处的代码 }); return <div />; }
注意:
-
effect的使用是在组件渲染完成后需要执行的一些逻辑代码(例如DOM的操作,必须等到组件渲染完成)
-
Effect 通常应该使组件与 外部 系统保持同步,避免在Effect修改状态,以下代码会陷入死循环(状态改变-》重新渲染-》执行Effect-》状态改变-》...)
const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); });
-
- 指定 Effect 依赖
useEffect(() => { }, [...]); // ……依赖在此处声明!
依赖数组如果是个空数组,则代码只会在首次渲染完成后执行。依赖数组可以包含多个依赖项。当指定的所有依赖项在上一次渲染期间的值与当前值完全相同时,React 会跳过重新运行该 Effect。
注意:不要对依赖项说谎。只要useEffect中的代码涉及到外部变量,都应该考虑添加到依赖项。useEffect(() => { // 这里的代码会在每次渲染后执行 }); useEffect(() => { // 这里的代码只会在组件挂载后执行 }, []); useEffect(() => { //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行 }, [a, b]);
- 必要时添加清理(cleanup)函数
有时 Effect 需要指定如何停止、撤销,或者清除它的效果。例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”等。
例如:链接和断开链接useEffect(() => { const connection = createConnection(); connection.connect(); return () => { connection.disconnect(); }; }, []);
每次重新执行 Effect 之前,React 都会调用清理函数;组件被卸载时,也会调用清理函数.
注意:组件由于严格模式影响Effect调用两次的情况。
3.2. 获取数据(避免竞态)
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);
}
// ...
}
四 不需要使用effect情形
4.1. 需要根据props或者state变化计算新值的情形
unction Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;
}
其在渲染期间就可以将需要的结果计算出来渲染,避免使用effect产生级联渲染。
4.2. 缓存昂贵的计算
可以使用 useMemp Hook 缓存,传入 useMemp 的函数会在渲染期间执行,所以它仅适用于 纯函数 场景。
import { useMemo } from 'react';
function TodoList({ todos, filter }) {
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
4.3. 当 props key属性值变化时重置所有 state
传统的处理方式(会导致级联更新渲染)
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
高效的处理方式
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
const [comment, setComment] = useState('');
// ...
}
4.4. 避免将事件逻辑函数移到effect
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ 非常好:这个逻辑应该在组件显示时执行
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 避免:在 Effect 中处理属于事件特定的逻辑
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}
注:
- effect 使用场景:组件渲染完成后需要执行的逻辑
- 事件执行场景:当发起特定的交互操作需要执行的逻辑
4.5. 避免链式渲染
useEffect(() => {
...
setB('')
}, [a]);
useEffect(() => {
...
setC('')
}, [b]);
useEffect(() => {
...
setD('')
}, [c]);
...
这种方式非常低效,尽可能在渲染期间进行计算,以及在事件处理函数中调整 state。
4.6. 初始化应用时
某些逻辑应该只在应用程序启动时运行一次。比如,验证登陆状态和加载本地程序数据。可以将其放在组件之外
if (typeof window !== 'undefined') { // 检查是否在浏览器中运行
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ……
}
这保证了这种逻辑在浏览器加载页面后只运行一次。
五 effect 生命周期
Effect 描述了如何将外部系统与当前的 props 和 state 同步。随着代码的变化,同步的频率可能会增加或减少。
5.1 Effect 的生命周期与组件的生命周期区别
组件生命周期:
- 当组件被添加到屏幕上时,它会进行组件的 挂载。
- 当组件接收到新的 props 或 state 时,通常是作为对交互的响应,它会进行组件的 更新。
- 当组件从屏幕上移除时,它会进行组件的 卸载。
Effect生命周期:
- 主体部分指定了如何 开始同步
- 返回的清理函数指定了如何 停止同步
useEffect(()=>{
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
},[roomId])
每次在组件重新渲染后,React 都会查看传递的依赖项数组。如果数组中的任何值与上一次渲染时在相同位置传递的值不同,React 将重新同步 Effect。
注:effect同步代码中涉及的响应式值必须包含在依赖项中
5.2 每个 Effect 表示一个独立的同步过程
避免将与 Effect 无关的逻辑添加到已经编写的 Effect 中,仅仅因为这些逻辑需要与 Effect 同时运行。
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}
代码中的每个 Effect 应该代表一个独立的同步过程。
5.3 在组件主体中声明的所有变量都是响应式的
Props 和 state 并不是唯一的响应式值。从它们计算出的值也是响应式的。如果 props 或 state 发生变化,组件将重新渲染,从中计算出的值也会随之改变。这就是为什么 Effect 使用的组件主体中的所有变量都应该在依赖列表中。
5.4 不想进行重新同步时该怎么办
如果 effect主体代码设计的变量不依赖于渲染并且始终具有相同的值,可以将它们移到组件外部或者移动到 Effect 内部。
//外部
const serverUrl = 'https://localhost:1234'; // serverUrl 不是响应式的
const roomId = 'general'; // roomId 不是响应式的
function ChatRoom() {
useEffect(() => {
//内部
const serverUrl = 'https://localhost:1234'; // serverUrl 不是响应式的
const roomId = 'general'; // roomId 不是响应式的
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ 声明的所有依赖
// ...
}