useCallback是一个React Hook,它允许您在重新渲染之间缓存函数定义。
const cachedFn = useCallback(fn, dependencies)
fn:要缓存的函数值。它可以接受任何参数并返回任何值。React将在初始渲染期间返回(而不是调用!)您的函数。在下一次渲染中,如果自上次渲染以来依赖项没有更改,React将再次为您提供相同的函数。否则,它将为您提供在当前渲染过程中传递的函数,并将其存储起来,以备以后重用。React不会调用您的函数。函数将返回给您,以便您可以决定何时以及是否调用它。
dependencies:fn代码内部引用的所有反应值的列表。反应值包括props、state以及直接在组件体内声明的所有变量和函数。如果您的linter配置为React,它将验证每个React值是否正确指定为依赖项。依赖项列表必须具有恒定数量的项,并且像[dep1,dep2,dep3]一样以内联方式编写。React将使用Object.is的比较算法将每个依赖项与其以前的值进行比较。
在初始渲染时,useCallback返回您传递的fn函数。
在随后的渲染过程中,它将从上次渲染中返回一个已经存储的fn函数(如果依赖项没有更改),或者返回您在此渲染过程中传递的fn函数。
useCallback是一个Hook,所以您只能在组件的顶层或您自己的Hook中调用它。不能在循环或条件内部调用它。如果需要,请提取一个新组件并将状态移动到其中。
在以下渲染中,React将把依赖项与您在上一次渲染中传递的依赖项进行比较。如果任何依赖项都没有更改(与Object.is相比),useCallback将返回与以前相同的函数。否则,useCallback将返回在此渲染中传递的函数。
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
默认情况下,当组件重新渲染时,React会递归地重新渲染其所有子级。这就是为什么当ProductPage使用不同的主题重新渲染时,ShippingForm组件也会重新渲染。这对于不需要太多计算即可重新渲染的组件来说很好。但是,如果验证了重新渲染的速度较慢,则可以通过将其包装在备忘录中,告诉ShippingForm在其道具与上次渲染相同时跳过重新渲染
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
使用了memo包裹子组件只有prop变化时才会重新渲染,否则就跳过重新渲染
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
在JavaScript中,函数(){}或()=>{}总是创建不同的函数,类似于{}对象文字总是创建新对象的方式。通常情况下,这不会是一个问题,但这意味着ShippingForm道具将永远不会相同,您的备忘录优化也不会起作用。在这个例子中父组件的prop变化时函数会重新创建,即使子组件使用memeo依然不起作用,因为handleSubmit每次都不同
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
现在我们就可以将他等着在useCallback,只有依赖项变化时才会返回新的函数定义。一般情况下我们不必将函数使放在useCallback中,除非处于某种特定原因
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
useMemo缓存调用函数的结果。useCallback缓存函数本身。
const requirements = useMemo(() => { // Calls your function and caches its result
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
可以想象成这样:
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
如果你的应用程序像这个网站,并且大多数交互都很粗糙(比如替换一个页面或整个部分),那么通常不需要记忆。另一方面,如果你的应用程序更像一个绘图编辑器,并且大多数交互都是细粒度的(比如移动的形状),那么你可能会发现记忆非常有用。
使用useCallback缓存函数只有在少数情况下才有价值:
1、您将函数作为餐厨传递给了memo包裹的子组件
2、您传递的函数稍后将用作某个Hook的依赖项。例如,包装在useCallback中的另一个函数依赖于它,或者您依赖于useEffect中的此函数。
在其他情况下,用useCallback包装函数没有任何好处。这样做也没有重大危害,所以一些团队选择不考虑个别案例,并尽可能多地记忆。缺点是代码的可读性变差。此外,并非所有的记忆都是有效的:一个“总是新的”值就足以破坏整个组件的记忆。
useContext是一个React Hook,它允许您从组件中读取和订阅上下文。
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
// ...
SomeContext:您之前使用createContext创建的上下文。
useContext返回调用组件的上下文值,返回的值始终是最新的
相应的
深层数据传递、通过context传递更新数据
react 会找桑钱上下文最近的provider,不论层级多深都可以使用useContext(ThemeContext),访问到
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
// ... renders buttons inside ...
}
useEffect是一个React Hook,用于将组件与外部系统同步。
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
setup:具有Effect逻辑的函数。您的设置函数也可以选择返回一个清理函数。
可选依赖项:设置代码内部引用的所有反应值的列表。反应值包括props、state以及直接在组件体内声明的所有变量和函数。
连接到外部系统:某些组件在页面上显示时,需要保持与网络、某些浏览器API或第三方库的连接。这些系统不受React控制,所以它们被称为外部系统。
例如:
一个由setInterval()和clearInterval.()管理的计时器。
使用window.addEventListener()和window.removeEventLister()的事件订阅。
带有API的第三方动画库,如animation.start()和animation.reset()
感觉官网的解释有些晦涩,一般我们是这么理解的
可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
情况一:不传第二个参数(模拟 class 组件的 DidMount 和 DidUpdate )
useEffect(() => {
/** 执行逻辑 */
})
情况二:传递一个空数组(模拟 class 组件的 DidMount )
useEffect(() => {
/** 执行逻辑 */
},[])
情况三:传递数组有依赖项(模拟 class 组件的 DidUpdate )
useEffect(() => {
/** 执行逻辑 */
},[age,name])
情况四:第一个参数可以返回一个回调函数(模拟 WillUnMount 组件销毁的时候 停止计时器 )
const [time, setTime] = useState(0)
useEffect(() => {
const InterVal = setInterval(() => {
setTime(time + 1)
},1000)
return () => {
clearInterval(InterVal )
}
},[])
useMemo是一个React Hook,可以在重新渲染之间缓存计算结果。
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
calculateValue:计算要缓存的值的函数。
dependencies:calculateValue代码内部引用的所有反应值的列表。
在初始呈现时,useMemo返回不带参数的调用calculateValue的结果。
在接下来的渲染过程中,它将返回上次渲染中已经存储的值(如果依赖项没有更改),或者再次调用calculateValue,并返回calculateValue返回的结果。
跳过昂贵的计算
一般来说,除非您正在创建或循环数千个对象,否则这可能并不昂贵。如果你想获得更多的信心,你可以添加一个控制台日志来测量在一段代码中花费的时间:
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
如果记录的总时间加起来相当大(比如1毫秒或更长),那么将计算结果记忆起来可能是有意义的。作为实验,您可以将计算结果封装在useMemo中,以验证该交互的总记录时间是否减少。
1、计算较慢,并且依赖值很少变化
2、将该函数结果作为参数传给memo包裹的子组件
3、该结果作为其他hook的依赖项
在其他情况下,将计算包装在useMemo中没有任何好处。这样做也没有重大危害,所以一些团队选择不考虑个别案例,并尽可能多地记忆。这种方法的缺点是代码的可读性变差
useState是一个React Hook,它允许您将状态变量添加到组件中。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
initialState:您希望状态初始为的值。它可以是任何类型的值,但函数有一种特殊的行为。此参数在初始渲染后将被忽略。
如果将函数作为initialState传递,它将被视为初始值设定项函数。
current state :当前状态,在第一次渲染期间,它将与您通过的initialState相匹配。
set函数:用于将状态更新为不同的值并触发重新渲染。您可以直接传递下一个状态,也可以传递根据上一个状态计算的函数
setName('Taylor');
setAge(a => a + 1);
向组件添加状态,React将存储下一个状态,使用新值再次渲染组件,并更新UI。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
您可以将对象和数组置于状态。在React中,状态被认为是只读的,所以您应该替换它,而不是突变现有的对象。
// Don't mutate an object in state like this:
form.firstName = 'Taylor';
// ✅ Replace state with a new object
setForm({
...form,
firstName: 'Taylor'
});
const [todos, setTodos] = useState(createInitialTodos());
尽管createInitialTodos()的结果仅用于初始渲染,但您仍在每次渲染时调用此函数。如果创建大型数组或执行昂贵的计算,这可能是浪费。
const [todos, setTodos] = useState(createInitialTodos);
要解决此问题,可以将其作为初始值设定项函数传递给useState您传递的是函数本身createInitialTodos,而不是调用它的结果createInitialTodos()。如果您将函数传递给useState,React只会在初始化期间调用它。
useRef是一个React Hook,它允许您引用渲染不需要的值。
mport { useRef } from 'react';
function MyComponent() {
const intervalRef = useRef(0);
const inputRef = useRef(null);
// ...
initialValue:您希望ref对象的当前属性初始为的值。它可以是任何类型的值。此参数在初始渲染后将被忽略。
current:最初,它被设置为您传递的initialValue。您可以稍后将其设置为其他内容。
①使用ref定义一个无需引起页面重新渲染的状态
改变一个ref的值不会触发重新渲染。这意味着引用非常适合存储不影响组件视觉输出的信息。
function handleStopClick() {
const intervalId = intervalRef.current;
clearInterval(intervalId);
}
②使用ref操作DOM
使用ref来操作DOM是特别常见的。React对此有内置支持。
首先,声明一个初始值为null的ref对象:
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// ...
return <input ref={inputRef} />;
React创建DOM节点并将其放在屏幕上后,React将把ref对象的当前属性设置为该DOM节点。现在,您可以访问<input>的DOM节点,并调用focus()等方法:
③ 避免重新创建引用内容
React将初始ref值保存一次,并在下次渲染时忽略它。
function Video() {
const playerRef = useRef(new VideoPlayer());
尽管新VideoPlayer()的结果仅用于初始渲染,但您仍在每次渲染时调用此函数。如果创建昂贵的对象,这可能是浪费。
要解决此问题,您可以按如下方式初始化ref:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
默认情况下,组件不会将其DOM节点公开给父组件,这么写会报错
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
如果您希望MyInput的父组件能够访问<input>DOM节点,则必须选择使用forwardRef:
import { forwardRef } from 'react';
const MyInput = forwardRef(({ value, onChange }, ref) => {
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
);
});
export default MyInput;
React的useImperativeHandle是自定义钩子,可以让父组件获取并执行子组件内某些自定义函数(方法),或者获取子组件的状态。
useImperativeHandle本质上其实是子组件将自己内部的函数(方法)通过useImperativeHandle添加到父组件中useRef定义的对象中。如果想使用useImperativeHandle,那么还要结合useRef、React.forwardRef一起使用。
结合这个例子:
使用forwardRef后父组件可以访问子组件的Dom
import { forwardRef } from 'react';
const MyInput = forwardRef(({ value, onChange }, ref) => {
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
);
});
export default MyInput;
假设您不想公开整个<input>DOM节点,但希望公开它的两个方法:focus和scrollIntoView。要做到这一点,请将真实的浏览器DOM保存在一个单独的引用中。然后使用useImperativeHandle只使用您希望父组件调用的方法来公开句柄:
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
现在,如果父组件获得对MyInput的引用,它将能够调用其上的焦点和scrollIntoView方法。但是,它将无法完全访问底层的<input>DOM节点。