这几天小孩生病发烧,天天跑医院,搞得精力实在跟不上。写作的时间大大减少,写出的质量也不高。关于昨天写的函数组件的内容有点乱,很多都是直接参考了官方文档,今天再对相关的知识作个补充。请大家谅解。
由于状态是封闭的,所以组件与组件之间不能共享状态,我们只能把数据通过Props的方式传递给子组件,子组件再通过props传递给它的子组件,这样一级一级的传递下去。如果层级很多,数据很多,就很繁琐。Context
时候就能大展伸手了。
React
中的 Context
提供了一种在组件之间共享数据的方式,而不必通过组件树的逐层传递 props
。它适用于在应用程序中全局管理状态、配置信息、主题等情况下。Context 主要由三个部分组成:
Context
对象,该对象包含共享的数据。通常,你会在一个单独的文件中创建 Context
对象。Context
对象的提供者,它用于提供数据。Provider
通过 value
属性传递数据给其后代组件。Context
对象的消费者,用于订阅 Context
的变化。它允许在组件中访问 Context
中的值。以下是如何在 React
中使用 Context
的基本步骤:
// context.js
import React from 'react';
const defaultValue = { /* 默认的共享数据 */ }
const MyContext = React.createContext(defaultValue);
export default MyContext;
上面我们使用 createContext
创建了一个Context, 它的初始化值为 defaultValue
。并将这个Context赋于变量 MyContext
并导出,此时 MyContext
就唯一代表了这个Context
的类型。我们把它可以当作一个数据组件。对,此时的它也是一种组件。
在需要共享数据的组件层级中使用 Provider
// App.js
import React from 'react';
import MyContext from './context';
import ChildComponent from './ChildComponent';
function App() {
const sharedData = { /* 共享的数据 */ };
return (
<MyContext.Provider value={sharedData}>
<ChildComponent />
</MyContext.Provider>
);
}
export default App;
我们在App.jsx中导入了 MyContext
组件,所有它的子组件中都可以通过一定的方式获取到这个组件的值。 我们也可以通过对其value重新赋值,就像上面的示例所操作的一样。一定要用 Provider 才能向子组件提供数据共享。所有要使用这个数据的子组件我们称之为消费者 Consumer
。
我们来看看如何在消费者组件中使用 Consumer
// ChildComponent.js
import React from 'react';
import MyContext from './context';
function ChildComponent() {
return (
<MyContext.Consumer>
{value => (
<div>
{/* 在这里可以使用从 Context 中获取的值 */}
<p>Shared data: {value}</p>
</div>
)}
</MyContext.Consumer>
);
}
export default ChildComponent;
Provider
可以嵌套:可以在组件树的不同层级中使用多个 Provider,每个
Provider
可以传递不同的值给其后代。Provider
时,Consumer 组件可以使用默认值。Provider
的 value 可以是动态的,可以是状态、上下文或任何从 props
中获取的值。Context
中的值发生变化时,所有使用该 Context
的组件都会重新渲染。因此,在设计时需要考虑性能影响。Context
适用于在组件树中传递数据,但是并不适用于所有数据共享的场景。在某些情况下,使用 Context
可能会使代码变得更加复杂,因此需要谨慎使用。你可能对上面这个嵌套的用法有点不知如何下手,看下面的示例你就明白了:
function NestContextDemo{
...
return (
<SideMenuState.Provider value={ menuState }>
<SideMenuBadge.Provider value={badge}>
<DispatchMenuState.Provider value={updateMenuState}>
<DispatchMenuBadge.Provider value={updateBadgeHandler}>
<SideMenuData.Provider value={menuData}>
{
/* 你的消费组件 */
children
}
</SideMenuData.Provider>
</DispatchMenuBadge.Provider>
</DispatchMenuState.Provider>
</SideMenuBadge.Provider>
</SideMenuState.Provider>
)
}
你看,上面嵌套了多少层。是不是很魔幻。
当有多个Context层次嵌套的时候,你会发现往往 Consumer 也有很多嵌套。这就有点无法接受了。本来很清爽的组件,外面弄了多个嵌套层,我是无论如何都不能接受的。 还好,React 已经想到了。
在 React
中,除了使用 Consumer
外,还有另一种更简单的方式来使用 Context,那就是使用
useContext
钩子。useContext
钩子可以让你更方便地从 Context
中读取值。
useContext
钩子接收一个 context
对象(由 React.createContext
创建)并返回该 context
的当前值。当组件上下文发生变化时,useContext
将使组件重新渲染。
// ChildComponent.js
import React, { useContext } from 'react';
import MyContext from './context';
function ChildComponent() {
const value = useContext(MyContext);
return (
<div>
{/* 在这里可以直接使用从 Context 中获取的值 */}
<p>Shared data: {value}</p>
</div>
);
}
export default ChildComponent;
通过 useContext
,我们可以直接从上下文中获取值,并在组件中使用它,而无需嵌套多余的组件。这使得代码更加简洁和易读。
使用场景比较
Consumer
vs useContext
:
性能方面:
在性能方面,两者并没有显著差异,但是 useContext
的语法更为简洁,可读性更高。
总之,useContext
提供了一种更简洁的方式来使用 Context
,特别是在函数组件中。但是在一些情况下,如需要进行条件渲染或者动态修改上下文值时,Consumer
仍然是一种更灵活的选择。
除了上面的 useContext
, 在React18.2
的版本中,提供了另一种使用Context
的方法 use
。
与其他 React Hook
不同的是,可以在循环和条件语句(如 if)中调用 use
。但需要注意的是,调用 use
的函数仍然必须是一个组件或 Hook
。如下所示:
function HorizontalRule({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}
if 语句内部调用了 use,允许有条件地从 context 中读取值。
依赖项是指当 useEffect 中使用了组件内 useEffect 之外的变量。 一般我们都要指定这个依赖,这能极大地优化性能。依赖项指定后,当只有依赖项的数据发生变化,组合才会更新渲染。它会对依赖项的上次值进行比较,如果不相同就刷新渲染。
function ChatRoom({ roomId }) { // 这是一个响应式值
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 这也是一个响应式值
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 这个 Effect 读取这些响应式值
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ 因此你必须指定它们作为 Effect 的依赖项
// ...
}
useEffect 中最后一个参数就是依赖项列表,可以看出上列中 useEffect内部使用了 变量 serverUrl
和roomId
即 依赖项。注意,依赖项必须是组件内的变量,包括 props 和 state 。组件外的变量不用指定为依赖项。如下面把 serverUrl
的定义移到组件外进行定义, 这就可以把它从依赖项中移除。
const serverUrl = 'https://localhost:1234'; // 不再是一个响应式值
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 所有声明的依赖项
// ...
}
如果 Effect 的代码不使用任何响应式值,则其依赖项列表应为空([]):
const serverUrl = 'https://localhost:1234'; // 不再是响应式值
const roomId = 'music'; // 不再是响应式值
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ 所有声明的依赖项
// ...
}
空依赖项 [] 和 没有传入依赖项是不同的。空依赖项表示 useEffect 只执行一次。而 null 则表示只要变量发生变化就会重新渲染。一定要慎用。
这个 Hook
是 用于优化性能。它的作用是返回一个记忆化的回调函数,这个回调函数仅在依赖项发生变化时才会更新。通常情况下,它用于将回调函数传递给子组件,以避免不必要的函数重新创建,从而减少不必要的组件重新渲染。
useCallback 的基本语法
const memoizedCallback = useCallback(
() => {
// 回调函数体
},
[依赖项数组]
);
避免不必要的函数重新创建:当你需要将回调函数传递给子组件时,使用 useCallback 可以确保只有在依赖项变化时才会重新创建函数,避免不必要的重新渲染。这句话一定要牢记。
优化性能:特别是在性能敏感的场景下,避免不必要的函数创建可以提高性能。
useCallback。
示例
import React, { useCallback, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
// 使用 useCallback 优化回调函数
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // 依赖项为 count
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
在这个示例中,handleClick 回调函数使用了 count 状态,因此我们将 count 添加到依赖项数组中。这样,每次 count 发生变化时,handleClick 才会重新创建。
useMemo
是 用于在渲染过程中执行昂贵的计算,并且仅在其中的某些值发生更改时才执行。它的作用类似于 useCallback
,但是它用于返回记忆化的计算值,而不是记忆化的回调函数。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
依赖项数组的作用:确保你传递的依赖项数组包含所有在计算函数中使用的外部变量,否则可能会导致不一致的结果。
不滥用:只有在确定会带来性能提升的情况下才使用 useMemo。滥用 useMemo 可能会导致代码变得更加复杂,同时也会带来额外的开销。
import React, { useMemo, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
// 使用 useMemo 优化昂贵的计算
const expensiveValue = useMemo(() => {
return computeExpensiveValue(count);
}, [count]); // 依赖项为 count
return (
<div>
<p>Count: {count}</p>
<p>Expensive Value: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// 假设这是一个昂贵的计算函数
function computeExpensiveValue(count) {
console.log("Calculating expensive value...");
// 这里可以是任何复杂的计算逻辑
return count * 2;
}
在这个示例中,computeExpensiveValue
函数执行了一些昂贵的计算,我们使用 useMemo
将其结果缓存起来,当 count
发生变化时,才会重新执行这个计算函数。这样可以避免在每次渲染时都重新执行昂贵的计算逻辑。
用于在函数组件中创建可变的 ref 对象。它提供了一种在函数组件中访问 DOM 元素或者在组件渲染周期之间共享持久化数据的方式。
const refContainer = useRef(initialValue);
initialValue
是 ref
对象的初始值,可以是任何 JavaScript
值。
createRef 是类组件中创建 ref 对象的方式,而 useRef 是函数组件中创建 ref 对象的方式。最主要的区别在于 createRef 每次渲染都会返回一个新的 ref 对象,而 useRef 则会在多次渲染之间保持相同的 ref 对象。
import React, { useRef, useEffect } from 'react';
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
// 在组件加载后聚焦输入框
inputRef.current.focus();
}, []);
return (
<div>
<input type="text" ref={inputRef} />
</div>
);
}
在这个示例中,我们创建了一个 inputRef,然后将它赋值给了 元素的 ref 属性。在组件加载后,useEffect 钩子会执行,它会将焦点聚焦到输入框上,这是通过 inputRef.current.focus() 实现的。
当使用类组件时,可以使用 createRef
创建 ref。下面是一个简单的示例,演示如何在类组件中使用 createRef
:
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
// 创建一个ref对象来存储 DOM 元素的引用
this.textInput = React.createRef();
}
// 当组件加载完成后,将焦点聚焦到输入框上
componentDidMount() {
this.textInput.current.focus();
}
render() {
return (
<div>
{/* 将创建的 ref 关联到 input 元素上 */}
<input type="text" ref={this.textInput} />
<button onClick={() => this.textInput.current.focus()}>Focus Input</button>
</div>
);
}
}
export default MyComponent;
在这个示例中,我们创建了一个 textInput 的 ref 对象,并将其赋值给了 元素的 ref 属性。当组件加载完成后,componentDidMount 生命周期方法会被调用,这时我们通过 this.textInput.current.focus() 将焦点聚焦到输入框上。
总之,createRef 可以帮助我们在类组件中创建和管理 ref,使得在操作 DOM 元素或者其他组件时更加方便。
useReducer
是 React
提供的一个钩子函数,用于在函数组件中管理具有复杂状态逻辑的状态。它类似于 Redux
中的 reducer
,接收一个当前状态和一个 action``,并返回新的状态。通常情况下,useReducer
适用于管理多个相关的状态值,或者当下一个状态依赖于之前的状态时。 这个useReduce这里如果看不明白没有关系,因为后面我会专门讲解功能更为强大的Redux Reducer
插件,学习完那个章节后再回头来看这个就一目了然了。
const [state, dispatch] = useReducer(reducer, initialState);
reducer
是一个函数,接收当前状态 (state) 和一个描述发生了什么事件的 action,然后返回新的状态。它的签名是 (state, action) => newState。管理复杂状态逻辑:当状态逻辑变得复杂,或者有多个相关的状态需要管理时,可以考虑使用 useReducer。
组件间共享状态逻辑:useReducer 可以帮助将状态逻辑抽象成可重用的函数,并且可以在多个组件中共享。
状态依赖于之前的状态:当下一个状态的计算依赖于之前的状态时,使用 useReducer 可以更清晰地表达状态之间的关系。
让我们通过一个简单的计数器示例来演示 useReducer 的用法:
import React, { useReducer } from 'react';
// reducer函数,根据action的type来更新state
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
// 初始化状态和dispatch函数
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
{/* 当点击按钮时,dispatch一个action来触发状态的更新 */}
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;
在这个示例中,我们首先定义了一个 reducer
函数来处理状态的更新逻辑。然后,我们使用 useReducer
创建了一个状态 state 和一个 dispatch
函数,dispatch
函数用于向 reducer
发送一个 action
,从而更新状态。
当点击按钮时,我们调用 dispatch
函数并传递一个描述性的 action
对象,reducer
函数会根据 action
的 type
来更新状态。最后,我们根据新的状态重新渲染了组件,从而实现了一个简单的计数器功能。
总之,useReducer
可以帮助我们更好地管理状态逻辑,并且在某些情况下可以比 useState
更清晰地表达状态之间的关系。