React: Hooks 高级用法与最佳实践
概念深度
为什么要引入React Hook
?
其实跟Composition API
一样都是为了解决传统版本的遗留问题,这里React Hook
也是为了解决传统 React
类组件的问题:
-
逻辑服用困难:
在传统的类组件当中,复用逻辑通常需要使用高阶组件(
HOC
)。比如说,有一个计算器的功能需要多个组件都去使用,如果使用类组件,可能需要创建一个高阶组件来处理计算器的逻辑。会导致组件之间的嵌套,代码复杂。举例:
在传统的类组件中:
// 高阶组件 const withCounter = (WrappedComponent) => { return class extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } increment = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <WrappedComponent count={this.state.count} increment={this.increment} {...this.props} />; } }; }; // 使用高阶组件的组件 class MyComponent extends React.Component { render() { return ( <div> <p>Count: {this.props.count}</p> <button onClick={this.props.increment}>Increment</button> </div> ); } } const MyComponentWithCounter = withCounter(MyComponent);
-
代码冗余:在传统的类组件当中需要定义许多生命周期函数处理各种逻辑,这可能导致代码分散在不同的方法中,使得代码的可读性和可维护性降低。在React Hook当中我们
正是由于使用传统的类组件会出现上面的问题,所以React 又推出了React Hook它有什么优势呢?
-
性能优化:开发者可以使用
React Hook
比如useMemo
、useCallback
等Hook
可以更精细地控制组件的重新渲染,提高应用的性能。 -
增强逻辑复用性:上面的例子当中,使用
React Hook
来实现计数器功能,代码更加简洁和直观。使用自定义
React Hook
实现:import { useState } from'react'; const useCounter = () => { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; return { count, increment }; }; function MyComponent() { const { count, increment } = useCounter(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }
-
简洁的代码结构:使用
Hook
可以将相关的逻辑集中在一个函数组件中,避免了类组件中复杂的生命周期方法,使代码更加简洁、易读和可维护。
React Hook
的关键模块有什么?
React Hook 包括以下几个关键的模块:
状态管理 Hooks:
在React
中我们使用“状态”来描述对应的创建的变量。
-
useState
:- 对于这个
hook
在React
中我们的描述是用于添加组件状态。 - 它接受一个初始值作为参数,并返回一个数组,其中第一个元素是当前状态值,第二个元素是用于更新状态的函数。
- 对于这个
-
useReducer
:- 用于管理复杂的状态逻辑。
- 它接受一个
reducer
函数和一个初始状态作为参数,并返回当前的状态以及一个dispatch
函数,用于触发状态的更新。
-
useContext
:- 用于在组件树中共享数据。
- 它接受一个
Context
对象作为参数,并返回该Context
对象的值。
举例:
-
使用
useState
:import { Button } from 'antd' import { useState } from 'react' // 使用 useState创建一个基本的计数器组件 const About = () => { const [count, setCount] = useState(0) return ( <div> <p>{count}</p> <Button type="primary" onClick={() => { setCount(count + 1) }} > + </Button> <Button onClick={() => { setCount(count - 1) }} > - </Button> </div> ) } export default About
-
使用
useReducer
:import { Button } from 'antd' import { useReducer } from 'react' // 定义reducer函数 const todoReducer = (state: any, action: any) => { switch (action.type) { case 'ADD_TODO': return [...state, { id: action.id, text: action.text, completed: false }] case 'TOGGLE_TODO': return state.map((todo: any) => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ) case 'DELETE_TODO': return state.filter((todo: any) => todo.id !== action.id) default: return state } } const UseReducerDemo = () => { // 使用 useReducer 管理状态 const [todos, dispatch] = useReducer(todoReducer, []) const addTodo = (text: any) => { const newTodo = { id: Date.now(), text, completed: false } dispatch({ type: 'ADD_TODO', id: newTodo.id, text }) } const toggleTodo = (id: any) => { dispatch({ type: 'TOGGLE_TODO', id }) } const deleteTodo = (id: any) => { dispatch({ type: 'DELETE_TODO', id }) } return ( <div> <Button onClick={() => addTodo('New Task')}>Add Task</Button> {todos.map((todo: any) => ( <div key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> <span>{todo.text}</span> <button onClick={() => deleteTodo(todo.id)}>Delete</button> </div> ))} </div> ) } export default UseReducerDemo
-
使用
useContext
:import React, { createContext, useContext, useState } from 'react' // 定义上下文的类型 interface ThemeContextType { theme: string setTheme: React.Dispatch<React.SetStateAction<string>> } // 创建主题上下文 const ThemeContext = createContext<ThemeContextType>({ theme: 'light', setTheme: () => {}, }) const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [theme, setTheme] = useState('light') return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ) } const Button = () => { const { theme, setTheme } = useContext(ThemeContext) const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')) } return ( <button style={{ backgroundColor: theme === 'light' ? 'white' : 'black' }} onClick={toggleTheme} > Toggle Theme </button> ) } function App() { return ( <ThemeProvider> <Button /> </ThemeProvider> ) } export default App
问题:
-
关于
useState
我在这里就不详细介绍了,就是当我们要使用的时候就去直接就调用对应的状态即可,当我们要去修改的时候我们就直接调用setXXX()
去修改状态即可。 -
对于这个
useReducer
的使用的流程是不是感觉有点看懂了又好像有点陌生呢?-
首先在使用
useReducer
的时候它会要求你去定义一个reducer
函数,这个函数会要求你传入两个参数一个是维护的状态,另外一个参数是对应的动作。 -
在用户触发对应的操作之后通过
dispatch()
函数去触发reducer
函数的触发,比如说dispatch({ type: 'ADD_TODO', id: newTodo.id, text })
,它会去reducer
函数寻找对应的type
,一般在reducer
函数中实现的switch
,根据传入的不同类型,返回对应不同的状态。
-
-
可以使用
useState
不使用useReducer
吗?-
其实在上面那个例子中你是可以通过
useState
来管理状态的,但是你会发现你用useState
来管理状态的话,你在对应不同的动作action的时候需要去做对应的变量修改,这样会显得非常的冗余,且代码不好维护。 -
当你使用
useReducer
时,你会发现这种模式使得状态的改变非常清晰和可预测,因为每一次状态的更新都是由一个特定的动作触发的。而且也便于你扩展对应的功能,只需要在reducer
函数添加对应的动作即可。
-
-
useContext
的使用流程:-
首先我们需要确定
useContext
是做什么用的?它能实现什么效果。它能够在组件中共享数据。
-
要使用
useContext
的话,我们必须要先创建一个上下文,接着我们通过上下文.Provider
标签配合value
属性值即共享的值。作为父组件包裹子组件,这样就实现了组件创建的上下文的值的共享。
-
-
useReducer
和useContext
使用有什么好处呢?使用的场景是什么呢?useReducer
根据上面的例子我们会发现当状态更新逻辑较为复杂,涉及多个子操作或依赖多个条件时,useReducer
可以是逻辑更加清晰和可维护。useContext
的实现效果就是共享数据,那么它的好处不言而喻肯定可以避免层层传递props
,只需要在外层创建(createContext
),多个组件直接通过useContext
使用。 -
网上我看有一个说法是
useReducer
和useContext
配合就是redux
。这句话是有问题的。因为它们其实是有区别的。确实它们是有相似点,比如说可以集中管理状态都可以实现将状态的管理集中在一个地方。通过定义特定的函数(如
reducer
函数)来控制状态的更新方式,确保状态更新的逻辑清晰和可预测。但是它们也是有区别的:
Redux
通常被用于大型和复杂的应用,它具有更丰富的中间件、严格的架构模式和更多的工具支持。而
useReducer
+useContext
组合在小型到中型项目中可能更简洁和轻量。你可以想象如果你要使用很多的
useReducer
+useContext
组合,后面随着组件的复杂程度,肯定是非常难维护的。但是你在使用Redux
它有明确的实现架构,代码逻辑也十分清晰,但是你的useReducer
+useContext
组合是嵌套在组件内部的。
当我们通过上面的例子的使用以及问题的讲解之后,希望您会对上面三个常用的状态管理 Hooks有更加深入的理解。
副作用管理 Hooks:
副作用指的是那些影响组件外部的操作,比如数据获取、DOM
操作等。
-
useEffect
:- 用于处理组件的副作用操作。
- 可以在函数组件中执行副作用逻辑,支持在组件挂载、更新和卸载时执行不同的操作。
- 接收一个回调函数和一个依赖数组作为参数,通过依赖数组控制副作用的触发时机。
-
useLayoutEffect
:- 类似于
useEffect
,但在浏览器进行布局和绘制之前同步执行副作用。 - 适用于那些需要在页面布局更新前立即执行的副作用操作。
- 类似于
问题:
对于上面的副作用管理,我们不用像上面的来举具体的例子,把下面的几个问题讲解一下应该就能够领会这其中奥秘。
-
两者区别:
对
useEffect
来说:- 执行时机:
useEffect
是在浏览器完成渲染后,但在浏览器开始新的帧之前执行的。这意味着它允许浏览器先完成当前的渲染队列,然后才执行副作用操作。 - 异步性:由于它在渲染之后执行,因此它是异步的。这意味着即使
useEffect
中的代码抛出错误,也不会阻止页面的渲染或破坏用户界面。 - 用途:
useEffect
通常用于数据获取、订阅/取消订阅、执行网络请求或任何不会直接影响当前渲染结果的副作用操作。
对
useLayoutEffect
来说:- 执行时机:
useLayoutEffect
是在浏览器完成渲染更新但未重绘之前同步执行。这意味着它在浏览器进行布局(重排)和重绘之前发生。 - 同步性:因为
useLayoutEffect
在同一个动画帧内执行,所以它是同步的。如果在此钩子中抛出错误,它可能会影响当前的渲染流程,甚至导致渲染失败。 - 用途:
useLayoutEffect
更适合那些需要立即访问最新的 DOM 状态或需要在 DOM 更新后立即执行的操作,例如测量 DOM 元素的尺寸、在 DOM 更新后立即修改样式或进行精确的 DOM 操作。
- 执行时机:
-
选择哪个钩子?
- 如果你的副作用操作不需要在 DOM 更新的瞬间立刻执行,或者可能会阻塞浏览器渲染,那么你应该使用
useEffect
。 - 如果你的副作用操作依赖于最新的 DOM 状态,并且需要在浏览器重排和重绘之前完成,那么你应该使用
useLayoutEffect
。
- 如果你的副作用操作不需要在 DOM 更新的瞬间立刻执行,或者可能会阻塞浏览器渲染,那么你应该使用
-
在
useEffect
中它有两个参数:一个副作用函数和一个依赖数组。依赖数组是useEffect
的第二个参数,它告诉React
何时重新运行副作用函数。依赖数组可以为空数组[]
,也可以包含一个或多个值。-
空依赖数组
[]
:当依赖数组为空时,副作用函数只会在组件首次渲染后运行一次,并且在组件卸载时运行清理函数(如果有)。这是类似于
componentDidMount
和componentWillUnmount
的组合。 -
非空依赖数组:
当依赖数组包含一个或多个值时,副作用函数将在这些值变化时重新运行。这意味着每当依赖项中的任何一个值发生改变,
useEffect
的副作用函数就会重新执行。 -
特殊情况:省略依赖数组:
如果省略了依赖数组,
useEffect
将在每次组件渲染后运行副作用函数。当你使用这种方式多了的话,会造成性能缺陷,因为它会导致不必要的重新计算和渲染。
-
优化相关 Hooks:
用于优化组件的性能和渲染效率。
-
useMemo
:- 用于缓存计算结果。
- 只有当依赖项发生变化时,才会重新计算结果,避免不必要的重复计算。
-
useCallback
:- 用于缓存函数。
- 只有当依赖项发生变化时,才会重新创建函数,避免子组件不必要的重新渲染。
举例:
-
使用
useMemo
:import { Button } from 'antd' import React, { useState, useMemo } from 'react' const MyComponent = () => { const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]) const [extraValue, setExtraValue] = useState(0) // 不使用 useMemo function sumWithoutMemo() { console.log('不使用 useMemo') return numbers.reduce((acc, curr) => acc + curr, 0) } // 使用 useMemo const sumWithMemo = useMemo(() => { console.log('使用 useMemo') return numbers.reduce((acc, curr) => acc + curr, 0) }, [numbers]) return ( <div> <h1>不使用 useMemoo: {sumWithoutMemo()}</h1> <h1>使用 useMemo: {sumWithMemo}</h1> <Button onClick={() => setNumbers([...numbers, extraValue])}>新增</Button> <Button onClick={() => setExtraValue(extraValue + 1)}> 设置extraValue </Button> </div> ) } export default MyComponent
运行结果:
界面:
打印:
当点击新增的时候:
界面:
打印:
当点击设置
extraValue
的时候:界面:
打印:
但是当我们再次去点击新增的时候:
界面:
-
使用
useCallback
:import React, { useState, useCallback, useEffect } from 'react' import { Button } from 'antd' const CounterComponent = () => { const [countNoUseCallback, setCountNoUseCallback] = useState(0) const [countUseCallback, setCountUseCallback] = useState(0) let prevIncrementNoUseCallback: any = null let prevIncrementUseCallback: any = null // 不使用 useCallback const incrementNoUseCallback = () => { setCountNoUseCallback(countNoUseCallback + 1) } // 使用 useCallback const incrementUseCallback = useCallback(() => { setCountUseCallback(countUseCallback + 1) }, [countUseCallback]) useEffect(() => { if (prevIncrementNoUseCallback !== incrementNoUseCallback) { console.log('不使用 useCallback') prevIncrementNoUseCallback = incrementNoUseCallback } }, [incrementNoUseCallback]) useEffect(() => { if (prevIncrementUseCallback !== incrementUseCallback) { console.log('使用 useCallback') prevIncrementUseCallback = incrementUseCallback } }, [incrementUseCallback]) return ( <div> <h2>不使用 useCallback:</h2> <p>Count: {countNoUseCallback}</p> <Button onClick={incrementNoUseCallback}> 新增 (不使用 useCallback) </Button> <h2>使用 useCallback:</h2> <p>Count: {countUseCallback}</p> <Button onClick={incrementUseCallback}>新增 (使用 useCallback)</Button> </div> ) } export default CounterComponent
运行结果:
点击新增 (不使用
useCallback
)按钮:点击新增 (使用
useCallback
)按钮:
问题:
-
useMemo
的工作流程:-
在使用
useMemo
中我们可以根据上面的界面展示和打印结果知道很多东西的。在我们第一次进入这个组件的时候,useMemo
必须被调用来初始化memoized
值即我们渲染了组件,所以会去执行方法,在控制台打印不使用Memo
和使用Memo
。 -
当我们去点击新增的时候因为
useMemo
依赖于numbers
,新增会去修改numbers
,所以会去执行sumWithMemo
函数,打印使用Memo
。因为状态改变触发重新渲染,所以在渲染的时候重新执行sumWithoutMemo
函数,打印不使用Memo
。 -
当我们去点击设置
extraValue
的时候,我们会发现区别,第一我们只会打印不使用usememo
,是因为useMemo
的依赖是[numbers]
数组,numbers
数组的引用没有变化,useMemo
就不会重新计算。我们这里能够触发渲染的只有numbers数组,这里是因为让我们观察的更加清晰这么设置的。 -
当我们去新增的时候,通过渲染的结果我们又会发现其实设置
extraValue
是去做了修改的,但是useMemo
是懒加载的,只有当它变化我们才会去加载。
-
-
为什么第一次渲染的时候会先输出
使用Memo
,在输出不使用Memo
?useMemo
是一个 React Hook,它会在组件的渲染阶段被调用。sumWithoutMemo
函数是在组件渲染中直接被调用的。这通常发生在useMemo
已经完成初始化之后。seMemo
在组件生命周期的渲染阶段早期执行,而sumWithoutMemo
则在渲染阶段的后期,当组件正在构建时被调用。
-
useCallback
的工作流程:不使用
useCallback
的情况 (incrementNoUseCallback
)- 函数创建: 每次组件重新渲染时,
incrementNoUseCallback
都会被重新定义。这是因为函数是在组件函数的主体中定义的,而组件函数每次渲染时都会重新执行。 - 控制台输出: 当
incrementNoUseCallback
的引用发生变化时,useEffect
的依赖数组中包含的incrementNoUseCallback
会触发useEffect
的回调函数。在这个回调函数中,我们检查prevIncrementNoUseCallback
是否与当前的incrementNoUseCallback
引用不同。如果是,我们就打印'不使用 useCallback'
,并且更新prevIncrementNoUseCallback
的值。
使用
useCallback
的情况 (incrementUseCallback
)- 函数创建:
incrementUseCallback
只会在依赖项改变或组件首次渲染时重新创建。依赖项在这个例子中是countUseCallback
。由于useCallback
的设计,只要countUseCallback
的值没有变化,incrementUseCallback
的引用将保持不变。 - 控制台输出: 类似于
incrementNoUseCallback
的情况,但是incrementUseCallback
的引用在大多数情况下保持不变,除非countUseCallback
的值发生变化。因此,'使用 useCallback'
这条消息只会在组件首次渲染时打印,或者当countUseCallback
发生变化时打印。
- 函数创建: 每次组件重新渲染时,
-
useCallback
的首次渲染情况:- 首次渲染: 当组件首次渲染时,你将看到两条消息被打印,一条表示
incrementNoUseCallback
被创建,另一条表示incrementUseCallback
被创建。这是正常的,因为在这时所有的函数都需要被定义。 - 后续渲染: 当你点击任意按钮或组件重新渲染时(例如,由于外部状态变化),你将只看到
'不使用 useCallback'
的消息被打印,因为incrementNoUseCallback
的引用在每次渲染时都会改变。而'使用 useCallback'
的消息将不会再次出现,除非countUseCallback
的值发生变化。
- 首次渲染: 当组件首次渲染时,你将看到两条消息被打印,一条表示
-
useCallback
函数和useMemo
函数的区别以及如何选择:useCallback
:-
useCallback
主要解决的问题确实是与性能优化相关的,尤其是在处理函数作为 prop 传递给子组件的场景下。 -
当一个函数被作为
prop
传递给子组件时,如果这个函数在每次父组件重新渲染时都创建一个新的引用,那么即使子组件的props
实际上没有发生变化,子组件也会被认为接收到新的props
而重新渲染。这可能导致不必要的性能开销,尤其是当子组件较为复杂或性能敏感时。
useMemo
:- 它主要用于避免不必要的计算,特别是在组件渲染过程中对复杂数据结构或昂贵计算结果的重复计算。
useMemo
的主要作用是缓存计算结果,当依赖项没有改变时,它会返回缓存的结果,而不是重新计算。
如何选择
选择
useCallback
或useMemo
主要取决于你想要优化的对象类型:- 如果你想要优化的是函数的创建过程,避免每次渲染都创建新的函数实例,那么你应该使用
useCallback
。 - 如果你想要优化的是复杂计算的结果,避免每次渲染都重新执行相同的计算,那么你应该使用
useMemo
。
-
引用相关 Hooks:
用于获取对 DOM
元素或其他普通变量的引用。
其实就是可以通过一个hook
去对DOM
元素进行操作
-
useRef
:- 可以获取
DOM
元素的引用,方便进行直接的DOM
操作。 - 也可以在组件的多次渲染之间保存不变的值。
- 可以获取
-
useRef
的使用场景:- 访问 DOM 元素:我们通常使用
ref
属性和createRef()
方法来访问和操作DOM
节点。 - 保存函数组件内的可变状态:
useRef
可以用来存储那些不需要引起组件重新渲染的可变数据。 - 保存前一次的 props 或 state:当组件在重新渲染时,
props
和state
的最新值会被更新。有时你需要访问前一次渲染的值,这时你可以使用useRef
来存储前一次的值。
- 访问 DOM 元素:我们通常使用
举例:
import { Button, Input } from 'antd'
import React, { useRef, useEffect } from 'react'
const TextInputWithFocusButton = () => {
// 创建一个 ref
const inputEl: any = useRef(null)
// 在组件挂载后聚焦到输入框
useEffect(() => {
inputEl.current.focus()
}, [])
return (
<>
<Input ref={inputEl} type="text" />
<Button onClick={() => inputEl.current.focus()}>聚焦</Button>
</>
)
}
export default TextInputWithFocusButton
运行结果:
点击聚焦:
自定义 hooks:
对于react
中的自定义hooks
使用的方式和定义的方法是跟Vue 3
中的自定义hook
类似的。自定义 Hooks
是一种将这些功能封装到可重用的函数中的方式,这可以显著提高代码的可读性和可维护性。
示例:
比如说现在有一个**表单处理的 Hook
**示例:
import { useState } from 'react';
function useForm(initialState) {
const [values, setValues] = useState(initialState);
const handleChange = event => {
setValues({
...values,
[event.target.name]: event.target.value,
});
};
const handleSubmit = event => {
event.preventDefault();
// 处理表单提交逻辑
};
return {
values,
handleChange,
handleSubmit,
};
}
对于上面表单处理Hook
的使用:
import React from 'react'
import useForm from './useForm'
import { Input } from 'antd'
const ContactForm = () => {
const initialState = { name: '', email: '' }
const { values, handleChange, handleSubmit } = useForm(initialState)
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<Input
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
</label>
</form>
)
}
export default ContactForm
对于自定义 Hooks
的是用来说也是十分方便的,跟使用其它的hooks
一样直接引入即可。
Suspense与错误边界
Suspense:
对于Suspense
,在Vue 3
中也是有涉及的,它的主要作用其实跟在Vue 3
中类似用于处理异步操作(如数据获取)的一种方式。它允许你在等待数据时渲染一个“加载中”状态,并在数据可用时自动替换为实际内容。
示例:
import { Suspense, lazy } from 'react';
// 异步加载的组件或依赖于异步数据的组件
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
当组件尚未加载完毕时,Suspense
组件会显示 fallback
属性提供的内容,直到 LazyComponent
准备好为止。
错误边界:
错误边界是一个 React
组件,它可以捕获并打印发生在其子组件树任何位置的 JavaScript
错误,并渲染出备用 UI
而不是崩溃的组件树。它们像普通的 React
组件一样工作,但包含 static getDerivedStateFromError()
和 componentDidCatch()
方法。
示例:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新状态,使得下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你可以在这里记录错误信息
console.error('Uncaught error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// 渲染降级后的 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children; // 渲染子组件树
}
}
要使用错误边界,只需将它作为其他组件的父组件:
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyWidget from './MyWidget';
const App = () => {
return (
<div className="App">
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
</div>
);
}
export default App;
如果 MyWidget
或其任何子组件抛出一个未被捕获的错误,ErrorBoundary
将捕获该错误,并渲染一个简单的错误消息,而不是崩溃整个应用。
请注意,错误边界只能捕获发生在其子组件树中的错误,不能捕获事件处理器、异步代码(例如在 setTimeout
或 requestAnimationFrame
中)、服务器端渲染、或者构造函数中的错误。对于异步代码中的错误,可以考虑使用 Promise.catch()
或者 async/await
结合 try/catch
块来处理。
实战示例
状态管理:
假设我们有一个计数器应用,它可以在多个页面间共享状态,我们希望能够在不同的地方增加或减少计数器的值。
第一步,做好准备工作,定义计数器上下文,定义计数器状态,定义计数器提供者:
// 定义一个计数器上下文
interface CounterContextType {
count: number
dispatch: React.Dispatch<{ type: string; payload?: any }>
}
// 定义一个计数器上下文
const CounterContext: any = createContext<CounterContextType>({
count: 0,
dispatch: () => {},
})
// 定义一个计数器状态
const counterReducer = (
state: number,
action: { type: string; payload?: number }
) => {
switch (action.type) {
case 'INCREMENT':
return state + (action.payload || 1)
case 'DECREMENT':
return state - (action.payload || 1)
default:
return state
}
}
// 定义一个计数器提供者
const CounterProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}: any) => {
const [count, dispatch] = useReducer(counterReducer, 0)
return (
<CounterContext.Provider value={{ count, dispatch }}>
{children}
</CounterContext.Provider>
)
}
第二步,在需要的组件使用上下文和reducer:
const CounterDemo = () => {
const context: any = useContext(CounterContext)
const increment = () => {
console.log(context)
context.dispatch({ type: 'INCREMENT' })
}
const decrement = () => {
context.dispatch({ type: 'DECREMENT' })
}
return (
<>
<Button onClick={increment}>+</Button>
<span style={{ margin: '10px' }}>{context.count}</span>
<Button onClick={decrement}>-</Button>
</>
)
}
第三步:包裹应用:
// 使用 CounterProvider 包裹你的应用
const App: React.FC = () => (
<CounterProvider>
<CounterDemo />
</CounterProvider>
)
export default App
实现效果:
通过上面的代码实现了一个组合使用useContext
和useReducer hooks
的demo
。
通用深度与实例
深入响应式系统:
对于Vue 3
响应式系统来说它利用了 JavaScript
的 Proxy
对象,这使得 Vue 3
的响应式系统更加高效且易于理解。
-
Proxy
对象:可以拦截并定制对象的各种操作,这使得Vue 3
能够在不修改原始对象的情况下创建响应式代理,从而提高了性能和可维护性。 -
响应式引用:
Vue 3
引入了ref
和reactive
API。ref
的话就引入了.value
属性来实现响应式引用,而reactive
则将一个普通对象转换为响应式对象。 -
Composition API
:Vue 3 的 Composition API 允许开发者以函数式的方式组合逻辑,这与 React 的 Hooks 类似,使得代码更加模块化和可复用。
对于React Hooks
来说提供了一种无需编写类组件即可使用 React 状态和生命周期的能力,这极大地简化了功能组件的使用。React Hooks
的响应式系统主要关注于状态管理和副作用处理。
useState
和useContext
:useState
用于创建组件的局部状态,而useContext
允许在组件树中共享全局状态,这使得状态管理变得简单且可预测。useEffect
:useEffect
是用于处理副作用(如数据获取、订阅、清理等)的Hook
。它可以让你在函数组件中执行类似生命周期的方法,但更加灵活和可组合。useReducer
和useContext
:对于更复杂的状态管理,useReducer
提供了一种使用 Reducer 函数处理状态更新的方式,这通常与useContext
结合使用,以便在组件之间共享状态。
总体上面来说呢,其实Vue 3
的响应式系统能够更有效地追踪数据变化,同时减少了对数据的直接操作。因为它主要是实现的依赖记录追踪和更新。React
的 Hooks
提供了更细粒度的状态管理和更灵活的副作用处理。所以在使用层面来说React Hooks
更加灵活,Vue 3
中呢会更加的直观和易于调试。
当然关于React
中还有很多核心的内容,包括 Fiber
,虚拟DOM
、Diff
算法等等这个后面再仔细了解分享,到时候我们再玩玩它们的源码。
希望能对您的学习有帮助!如果有什么问题,欢迎您跟我一起交流交流!