钩子
hooks
在探索 useEffect 原理的时候,一直被一个问题困扰:useEffect 作用和用途是什么?当然,用于函数的副作用这句话谁都会讲。举个例子吧:
function App () {
const [num, setNum] = useState(0)
useEffect(() => {
// 模拟异步请求后端数据
setTimeout(() => {
setNum(num + 1)
}, 1000)
}, [])
return <div>{!num ? '请求后端数据...' : `后端数据是 ${num}`}</div>
}
这段代码,虽然这样组织可读性更高,毕竟可以将这个请求理解为函数的副作用。但这并不是必要的。完全可以不使用useEffect
,直接使用setTimeout
,并且它的回调函数中更新函数组件的 state。
在 useEffect 的第二个参数中,我们可以指定一个数组,如果下次渲染时,数组中的元素没变,那么就不会触发这个副作用(可以类比 Class 类的关于 nextprops 和 prevProps 的生命周期)。好处显然易见,相比于直接裸写在函数组件顶层,useEffect 能根据需要,避免多余的 render。
下面是一个不包括销毁副作用功能的 useEffect 的 TypeScript 实现:
// 还是利用 Array + Cursor的思路
const allDeps: any[][] = []
let effectCursor = 0
function useEffect (callback: () => void, deps: any[]) {
if (!allDeps[effectCursor]) {
// 初次渲染:赋值 + 调用回调函数
allDeps[effectCursor] = deps
++effectCursor
callback()
return
}
const currenEffectCursor = effectCursor
const rawDeps = allDeps[currenEffectCursor]
// 检测依赖项是否发生变化,发生变化需要重新render
const isChanged = rawDeps.some(
(dep: any, index: number) => dep !== deps[index]
)
if (isChanged) {
callback()
allDeps[effectCursor] = deps // 感谢 juejin@carlzzz 的指正
}
++effectCursor
}
// function render () {
// ReactDOM.render(<App />, document.getElementById('root'))
// effectCursor = 0 // 注意将 effectCursor 重置为0
// }
对于 useEffect 的实现,配合下面案例的使用会更容易理解。当然,你也可以在这个 useEffect 中发起异步请求,并在接受数据后,调用 state 的更新函数,不会发生爆栈的情况。
function App () {
const [num, setNum] = useState < number > 0
const [num2] = useState < number > 1
// 多次触发
// 每次点击按钮,都会触发 setNum 函数
// 副作用检测到 num 变化,会自动调用回调函数
useEffect(() => {
console.log('num update: ', num)
}, [num])
// 仅第一次触发
// 只会在compoentDidMount时,触发一次
// 副作用函数不会多次执行
useEffect(() => {
console.log('num2 update: ', num2)
}, [num2])
return (
<div>
<div>num: {num}</div>
<div>
<button onClick={() => setNum(num + 1)}>加 1</button>
<button onClick={() => setNum(num - 1)}>减 1</button>
</div>
</div>
)
}
useEffect 第一个回调函数可以返回一个用于销毁副作用的函数,相当于 Class 组件的 unmount 生命周期。这里为了方便说明,没有进行实现。
参考文档:
-
useState
-
useEffect
-
useContext
-
useReducer
-
useMemo
-
useCallback
-
useRef
-
useImperativeHandle
-
useLayoutEffect
-
useDebugValue
React v18中的hooks
-
useSyncExternalStore
-
useTransition
-
useDeferredValue
-
useInsertionEffect
-
useId
简单介绍一下 react 18 新增的 hooks
useSyncExternalStore
useSyncExternalStore
:是一个推荐用于读取和订阅外部数据源的 hook
,其方式与选择性的 hydration
和时间切片等并发渲染功能兼容
const state = useSyncExternalStore(
subscribe,
getSnapshot[getServerSnapshot]
)
subscribe
: 订阅函数,用于注册一个回调函数,当存储值发生更改时被调用。此外,useSyncExternalStore
会通过带有记忆性的getSnapshot
来判别数据是否发生变化,如果发生变化,那么会强制更新数据。getSnapshot
: 返回当前存储值的函数。必须返回缓存的值。如果getSnapshot
连续多次调用,则必须返回相同的确切值,除非中间有存储值更新。getServerSnapshot
:返回服务端(hydration模式下)渲染期间使用的存储值的函数
useTransition
useTransition
:返回一个状态值表示过渡任务的等待状态, 以及一个启动该过渡任务的函数。
过渡任务 在一些场景中,如:输入框
、tab切换
、按钮
等,这些任务需要视图上立刻做出响应,这些任务可以称之为立即更新的任务
但有的时候,更新任务并不是那么紧急,或者来说要去请求数据等,导致新的状态不能立马更新,需要用一个loading...
的等待状态,这类任务就是过度任务
const [isPending, startTransition] = useTransition()
isPending
:过渡状态的标志,为true
时是等待状态startTransition
:可以将里面的任务变成过渡任务
useDeferredValue
useDeferredValue
:接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。
如果当前渲染是一个紧急更新的结果,比如用户输入,React
将返回之前的值,然后在紧急渲染完成后渲染新的值。
也就是说useDeferredValue
可以让状态滞后派生。
const deferredValue = useDeferredValue(value)
value
:可变的值,如useState
创建的值deferredValue
: 延时状态
useTransition和useDeferredValue做个对比
相同点:
useDeferredValue
和useTransition
一样,都是过渡更新任务 不同点:useTransition
给的是一个状态,而useDeferredValue
给的是一个值
useInsertionEffect
useInsertionEffect
:与 useLayoutEffect
一样,但它在所有 DOM 突变之前同步触发
在执行顺序上 useInsertionEffect
> useLayoutEffect
> useEffect
seInsertionEffect
应仅限于css-in-js
库作者使用。 优先考虑使用useEffect
或useLayoutEffect
来替代。
useId
useId
: 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免hydration
不匹配的 hook。
参考文档
hooks 和 memorizedState 之间的关系
在React中,Hooks是一种特殊的函数,用于在函数组件中添加和管理状态以及其他React特性。而memorizedState
是React内部用于存储和管理Hooks状态的数据结构。
当你在函数组件中使用Hooks(如useState
、useEffect
等)时,React会在组件首次渲染时创建一个memorizedState
链表。这个链表中的节点包含了组件的各个状态值。
每个节点都包含了两个重要的属性:memoizedState
和next
。memoizedState
是该节点对应的状态值,而next
是指向下一个节点的指针。这样就形成了一个链表,其中的节点对应于组件中的不同状态。
当组件重新渲染时,React会通过memorizedState
链表找到与组件对应的节点,并将其中的状态值返回给组件。当调用状态更新的函数时,React会在memorizedState
链表中找到与组件对应的节点,并将其中的状态值更新为新的值。
因此,Hooks和memorizedState
是紧密相关的,Hooks通过memorizedState
实现了状态的管理和更新。这种关系使得在函数组件中使用Hooks能够实现声明式的、可持久的状态管理,并且方便React进行性能优化。
hooks 和 memorizedState 是怎么关联起来的?
在React中,Hooks和memorizedState
通过一种特殊的数据结构关联起来,这个数据结构被称为Fiber节点。
每个函数组件都对应一个Fiber节点,Fiber节点中包含了组件的各种信息,包括组件的状态(memorizedState
)、props、子节点等。
当一个函数组件被调用时,React会创建一个新的Fiber节点,并将其与函数组件关联起来。在这个Fiber节点中,React会通过memoizedState
属性存储组件的状态值。
当函数组件重新渲染时,React会更新对应的Fiber节点。在更新过程中,React会根据函数组件中的Hooks调用顺序,遍历memorizedState
链表中的节点。
React会根据Hooks调用的顺序,将当前的memorizedState
链表中的节点与新的Hooks调用结果进行比较,并更新memoizedState
中的值。
这个过程中,React会使用一些算法来比较和更新memorizedState
链表中的节点,以确保状态的正确性和一致性。例如,React可能会使用链表的插入、删除、移动等操作来更新memorizedState
链表。
通过这样的机制,Hooks和memorizedState
实现了状态的管理和更新。Hooks提供了一种声明式的方式,让我们能够在函数组件中使用和更新状态,而memorizedState
则是React内部用于存储和管理这些状态的数据结构。
useState 和 memorizedState 状态举例
当组件首次渲染时(mount阶段),React会创建一个新的Fiber节点,并在其中创建一个memorizedState
来存储useState
hook的初始值。这个memorizedState
会被添加到Fiber节点的memoizedState
属性中。
在更新阶段(update阶段),当组件重新渲染时,React会通过比较前后两次渲染中的memorizedState
来判断状态是否发生了变化。
React会根据useState
hook的调用顺序来确定memorizedState
的位置。例如,在一个组件中多次调用了useState
hook,React会按照调用的顺序在memoizedState
属性中创建对应的memorizedState
。
举一个例子,假设我们有一个表单组件,其中使用了两个useState
hook来存储用户名和密码的值:
import React, { useState } from 'react';
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
return (
<form>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
在这个例子中,我们在组件函数中分别调用了两次useState
hook,创建了username
和password
这两个状态。
在首次渲染时(mount阶段),React会为每一个useState
hook创建一个memorizedState
对象,并将它们存储在组件的Fiber节点的memoizedState
属性中。
当我们输入用户名或密码并触发onChange事件时,React会进入更新阶段(update阶段)。在这个阶段,React会比较前后两次渲染中的memorizedState
,并根据变化的状态来更新UI。
React会比较username
和password
的旧值和新值,如果有变化,会更新对应的Fiber节点中的memoizedState
,然后重新渲染组件,并将最新的username
和password
值传递给相应的input元素。
通过比较memorizedState
,React能够检测到状态的变化,并只更新发生变化的部分,以提高性能和优化渲染过程。
shouldComponentUpdate 的作用
react 如何处理事件,Synthetic Event 的作用
在 React 中,绑定事件的原理是基于合成事件(SyntheticEvent)的机制。合成事件是一种由 React 自己实现的事件系统,它是对原生 DOM 事件的封装和优化,提供了一种统一的事件处理机制,可以跨浏览器保持一致的行为。
当我们在 React 组件中使用 onClick
等事件处理函数时,实际上是在使用合成事件。React 使用一种称为“事件委托”的技术,在组件的最外层容器上注册事件监听器,然后根据事件的目标元素和事件类型来触发合适的事件处理函数。这种机制可以大大减少事件监听器的数量,提高事件处理的性能和效率。
在使用合成事件时,React 会将事件处理函数包装成一个合成事件对象(SyntheticEvent),并将其传递给事件处理函数。合成事件对象包含了与原生 DOM 事件相同的属性和方法,例如 target
、currentTarget
、preventDefault()
等,但是它是由 React 实现的,并不是原生的 DOM 事件对象。因此,我们不能在合成事件对象上调用 stopPropagation()
或 stopImmediatePropagation()
等方法,而应该使用 nativeEvent
属性来访问原生 DOM 事件对象。
绑定事件的实现原理也涉及到 React 的更新机制。当组件的状态或属性发生变化时,React 会对组件进行重新渲染,同时重新注册事件监听器。为了避免不必要的事件处理函数的创建和注册,React 会对事件处理函数进行缓存和复用,只有在事件处理函数发生变化时才会重新创建和注册新的事件处理函数。这种机制可以大大提高组件的性能和效率,尤其是在处理大量事件和频繁更新状态的情况下。
在 React 中,合成事件是一种封装了浏览器原生事件对象的高级事件机制。它是由 React 提供的一种用于处理事件的抽象层,可以让开发者更方便地处理和管理事件。
React 的合成事件机制提供了一些优秀的特性:
-
跨浏览器兼容性:React 的合成事件可以屏蔽浏览器的差异,保证在各种浏览器上运行一致。
-
性能优化:React 的合成事件可以对事件进行池化处理,重用事件对象,避免创建大量的事件对象,从而提高性能。
-
事件委托:React 的合成事件可以实现事件委托机制,将事件处理程序绑定在组件树的根节点上,统一管理和处理组件内部和外部的事件,从而避免多次绑定事件处理程序的问题。
-
支持自定义事件:React 的合成事件可以支持自定义事件,开发者可以自定义组件事件,提供更多的自定义能力。
React 的合成事件机制通过事件冒泡和事件委托来实现。当在组件中触发事件时,React 会将该事件包装成一个合成事件对象,并在组件树中冒泡传递,直到根节点处。在组件树中,React 使用事件委托机制将事件处理程序绑定到根节点上,统一处理所有组件的事件。
在处理合成事件时,React 提供了一些常用的事件处理函数,例如 onClick
、onMouseOver
、onSubmit
等,可以在组件中直接使用。此外,开发者还可以自定义事件处理函数,通过 on
前缀加上事件名称的方式来绑定自定义事件。例如,我们可以定义一个 onCustomEvent
方法来处理自定义事件:
jsxCopy codeclass MyComponent extends React.Component {
handleCustomEvent() {
// 处理自定义事件
}
render() {
return (
<div>
<button onClick={this.handleCustomEvent}>触发自定义事件</button>
</div>
);
}
}
在这个例子中,我们定义了一个名为 handleCustomEvent
的方法来处理自定义事件,然后在组件中通过 onClick
属性来绑定该方法。当用户点击按钮时,React 会将该事件包装成一个合成事件对象,并调用 handleCustomEvent
方法来处理事件。
useState
流程图如下:renderWithHooks 根据current来判断当前是首次渲染还是更新。 hooks加载时调用对应的mount函数,更新时调用对应的update函数。 hooks生成单向链表,通过next连接,最后一个next指向null。 state hooks会生成update循环链表, effects会生成另外一个effectList循环链表。
renderWithHooks
react-reconciler/src/ReactFiberHooks.js
// renderWithHooks中判断是否是首次渲染
function renderWithHooks(current, workInProgress, Component, props, nextRenderLanes) {
//当前正在渲染的车道
renderLanes = nextRenderLanes
currentlyRenderingFiber = workInProgress;
//函数组件更新队列里存的effect
workInProgress.updateQueue = null;
//函数组件状态存的hooks的链表
workInProgress.memoizedState = null;
//如果有老的fiber,并且有老的hook链表
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
//需要要函数组件执行前给ReactCurrentDispatcher.current赋值
const children = Component(props);
currentlyRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
renderLanes = NoLanes;
return children;
}
HooksDispatcherOnMount和HooksDispatcherOnUpdate
对象分别存放hooks的挂载函数和更新函数
hooks的注册
function resolveDispatcher () {
return ReactCurrentDispatcher.current
}
/**
*
@param {*} reducer 处理函数,用于根据老状态和动作计算新状态
@param {*} initialArg 初始状态
*/
export function useState (initialState) {
const dispatcher = resolveDispatcher()
return dispatcher.useState(initialState)
}
/**
构建新的hooks, 其主要作用是在 Fiber 树中遍历到某个组件时,
根据该组件的类型和当前处理阶段(mount 或 update),处理该组件的 Hook 状态。
*/
function updateWorkInProgressHook () {
// 获取将要构建的新的hook的老hook
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate
currentHook = current.memoizedState
} else {
currentHook = currentHook.next
}
// 根据老hook创建新hook
const newHook = {
memoizedState: currentHook.memoizedState,
queue: currentHook.queue,
next: null,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue
}
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook
} else {
workInProgressHook = workInProgressHook.next = newHook
}
return workInProgressHook
}
useState 实现
接收一个初始状态值,返回一个数组,包含当前状态值和更新状态值的方法。可以通过调用更新方法来改变状态值,并触发组件的重新渲染
参考文档
useContext
如何合理使用 useContext
useContext 是 React 中提供的一种跨组件传递数据的方式,可以让我们在不同层级的组件之间共享数据,避免了繁琐的 props 传递过程。使用 useContext 可以大大简化组件之间的通信方式,提高代码可维护性和可读性。
下面是一些使用 useContext 的最佳实践:
- 合理使用 context 的层级
context 可以跨组件传递数据,但是过多的 context 层级会使代码变得复杂、难以维护,而且会影响性能。因此,应该尽量避免嵌套过多 context 的层级,保持简单的组件结构。
- 将 context 统一定义在一个文件中
为了方便管理和使用,我们应该将 context 的定义统一放在一个文件中,这样能够避免重复代码,也能方便其他组件引用。
- 使用 context.Provider 提供数据
使用 context.Provider 来提供数据,将数据传递给子组件。在 Provider 中可以设置 value 属性来传递数据。
- 使用 useContext 获取数据
使用 useContext hook 来获取 context 中的数据。useContext 接收一个 context 对象作为参数,返回 context 的当前值。这样就可以在组件中直接使用 context 中的数据。
- 避免滥用 useContext
虽然 useContext 可以方便地跨组件传递数据,但是滥用 useContext 也会使代码变得难以维护。因此,在使用 useContext 时,应该优先考虑组件通信是否真的需要使用 useContext。只有在需要跨越多级组件传递数据时,才应该使用 useContext 解决问题。
如何避免使用 context 的时候, 引起整个挂载节点树的重新渲染?
使用 context 时,如果 context 中的值发生了变化,会触发整个组件树的重新渲染。这可能会导致性能问题,特别是在组件树较大或者数据变化频繁的情况下。
为了避免这种情况,可以采用以下方法:
- 对 context 值进行优化
如果 context 中的值是一个对象或者数组,可以考虑使用 useMemo 或者 useCallback 对其进行优化。这样可以确保只有在值发生变化时才会触发重新渲染。
- 将 context 的值进行拆分
如果 context 中的值包含多个独立的部分,可以考虑将其进行拆分,将不需要更新的部分放入另一个 context 中。这样可以避免因为一个值的变化而导致整个组件树的重新渲染。
- 使用 shouldComponentUpdate 或者 React.memo 进行优化
对于一些需要频繁更新的组件,可以使用 shouldComponentUpdate 或者 React.memo 进行优化。这样可以在值发生变化时,只重新渲染需要更新的部分。
- 使用其他数据管理方案
如果 context 不能满足需求,可以考虑使用其他数据管理方案,如 Redux 或者 MobX。这些方案可以更好地控制数据更新,避免不必要的渲染。
如果 context 中的值是一个对象或者数组,可以考虑使用 useMemo 或者 useCallback 对其进行优化
代码举例: 以下是一个使用 useMemo 对 context 值进行优化的示例代码:
import React, { useMemo, createContext } from 'react'
// 创建一个 Context
const MyContext = createContext()
// 创建一个 Provider
const MyProvider = ({ children }) => {
// 定义一个复杂的数据对象
const data = useMemo(() => {
// 这里可以是一些复杂的计算逻辑
return {
name: 'Alice',
age: 18,
hobbies: ['Reading', 'Traveling', 'Sports'],
friends: [
{ name: 'Bob', age: 20 },
{ name: 'Charlie', age: 22 },
{ name: 'David', age: 24 }
]
}
}, [])
return (
// 将 data 作为 value 传入 context.Provider
<MyContext.Provider value={data}>
{children}
</MyContext.Provider>
)
}
// 在 Consumer 中使用 context
const MyConsumer = () => {
return (
<MyContext.Consumer>
{data => (
<div>
<div>Name: {data.name}</div>
<div>Age: {data.age}</div>
<div>Hobbies: {data.hobbies.join(', ')}</div>
<div>Friends:
<ul>
{data.friends.map(friend => (
<li key={friend.name}>
{friend.name} ({friend.age})
</li>
))}
</ul>
</div>
</div>
)}
</MyContext.Consumer>
)
}
// 使用 MyProvider 包裹需要使用 context 的组件
const App = () => {
return (
<MyProvider>
<MyConsumer />
</MyProvider>
)
}
export default App
在上面的示例中,我们使用了 useMemo 对复杂的数据对象进行了缓存。这样,当 context 中的值变化时,只会重新计算数据对象的值,而不是重新创建一个新的对象。这样可以有效地减少不必要的渲染。
useEffect 依赖为空数组与 componentDidMount 区别
useEffect
是 React 函数组件的生命周期钩子,它是替代类组件中 componentDidMount
、componentDidUpdate
和 componentWillUnmount
生命周期方法的统一方式。
当你给 useEffect
的依赖项数组传入一个空数组([]
),它的行为类似于 componentDidMount
,但实质上有些区别:
- 执行时机:
componentDidMount
:在类组件的实例被创建并插入 DOM 之后(即挂载完成后)会立即被调用一次。useEffect
(依赖为空数组):在函数组件的渲染结果被提交到 DOM 之后,在浏览器绘制之前被调用。React 保证了不会在 DOM 更新后阻塞页面绘制。
- 清除操作:
componentDidMount
:不涉及清理机制。useEffect
:可以返回一个清理函数,React 会在组件卸载或重新渲染(当依赖项改变时)之前调用这个函数。对于只依赖空数组的useEffect
,此清理函数只会在组件卸载时被调用。
- 执行次数:
componentDidMount
:在 render 执行之后,componentDidMount 会执行,如果在这个生命周期中再一次 setState ,会导致再次 render ,返回了新的值,浏览器只会渲染第二次 render 返回的值,这样可以避免闪屏。useEffect
:是在真实的 DOM 渲染之后才会去执行,在这个 hooks 中再一次 setState, 这会造成两次 render ,有可能会闪屏。
实际上 useLayoutEffect
会更接近 componentDidMount
的表现,它们都同步执行且会阻碍真实的 DOM 渲染的
useEffect
钩子的工作原理涉及到 React 的渲染流程和副作用的调度机制。以下是其工作原理的详细说明:
-
调度副作用:当你在组件内部调用
useEffect
时,你实际上是将一个副作用函数及其依赖项数组排队等待执行。这个函数并不会立即执行。 -
提交阶段(Commit Phase):React 渲染组件并且执行了所有的纯函数组件或类组件的渲染方法后,会进入所谓的提交阶段。在这个阶段,React 将计算出的新视图(新的 DOM 节点)更新到屏幕上。一旦这个更新完成,React 就知道现在可以安全地执行副作用函数了,因为这不会影响到正在屏幕上显示的界面。
-
副作用执行:提交阶段完成后,React 会处理所有排队的副作用。如果组件是首次渲染,所有的副作用都会执行。如果组件是重新渲染,React 会首先对比副作用的依赖项数组:如果依赖项未变,副作用则不会执行;如果依赖项有变化,或者没有提供依赖项数组,副作用会再次执行。
-
清理机制:如果副作用函数返回了一个函数,那么这个函数将被视为清理函数。在执行当前的副作用之前,以及组件卸载前,React 会先调用上一次渲染中的清理函数。这样确保了不会有内存泄漏,同时能撤销上一次副作用导致的改变。
-
延迟副作用:尽管
useEffect
会在渲染之后执行,但它是异步执行的,不会阻塞浏览器更新屏幕。这意味着 React 会等待浏览器完成绘制之后,再执行你的副作用函数,以此来确保副作用处理不会导致用户可见的延迟。
通过这种机制,useEffect
允许开发者以一种优化的方式来处理组件中可能存在的副作用,而不需要关心渲染的具体时机。退出清理功能确保了即使组件被多次快速创建和销毁,应用程序也能保持稳定和性能。
useReducer
useReducer
是 React Hooks 的一个部分,它为状态管理提供了一个更加灵活的方法。useReducer
特别适合处理包含多个子值的复杂状态逻辑,或者当下一个状态依赖于之前的状态时。与useState
相比,useReducer
更适合于复杂的状态逻辑,它使组件的状态管理更加清晰和可预测。
基础使用
const [state, dispatch] = useReducer(reducer, initialState);
state
:当前管理的状态。dispatch
:一个允许你分发动作(action)来更新状态的函数。reducer
:一个函数,接受当前的状态和一个动作对象作为参数,并返回一个新的状态。initialState
:初始状态值。
Reducer 函数
Reducer 函数的格式如下:
function reducer (state, action) {
switch (action.type) {
case 'ACTION_TYPE': {
// 处理动作并返回新的状态
return newState
}
// 更多的动作处理
default:
return state
}
}
动作(Action)
动作通常是一个包含type
字段的对象。type
用于在 reducer 函数中标识要执行的动作。动作对象也可以包含其他数据字段,用于传递动作所需的额外信息。
示例
以下是一个使用useReducer
的简单示例:
import React, { useReducer } from "react";
// 定义reducer函数
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
// 初始化状态和dispatch函数
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
在上面的例子中,我们创建了一个简单的计数器。当用户点击按钮时,会分发一个包含type
的动作到useReducer
钩子。然后,reducer
函数根据动作type
来决定如何更新状态。
使用场景
- 管理局部组件的状态。
- 处理复杂的状态逻辑。
- 当前状态依赖上一状态时,可以通过上一状态计算得到新状态。
useReducer
通常与Context
一起使用可以实现不同组件间的状态共享,这在避免 prop drilling(长距离传递 prop)的同时使状态更新更为模块化。
context
要避免在 React 开发中使用 context
时引起整个挂载节点树的重新渲染,可以采取以下方法:
- React Context 数据分割:把提供
context value
的部分提取到单独的组件中,并且仅在该组件中修改context value
。这样,当context value
变化时,只有真正使用该context
的消费组件会重新渲染,而非所有挂载节点都会重新渲染。
假设我们有一个应用,需要管理主题颜色和用户信息两个不同的数据。
首先,创建两个 Context:
import React from "eact";
// 创建主题颜色 Context
const ThemeContext = React.createContext({ theme: "light" });
// 创建用户信息 Context
const UserContext = React.createContext({ user: null });
在顶层组件中,提供这两个 Context 的 Provider,并设置相应的值:
class App extends React.Component {
state = {
theme: "dark",
user: { name: "John Doe", age: 25 },
};
render() {
return (
<ThemeContext.Provider value={this.state.theme}>
<UserContext.Provider value={this.state.user}>
<Toolbar />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
然后,在需要使用主题颜色的组件中,可以通过以下方式获取:
class ThemedButton extends React.Component {
static contextType = ThemeContext;
render() {
const theme = this.context;
return <Button theme={theme} />;
}
}
在需要使用用户信息的组件中,同样方式获取:
class UserProfile extends React.Component {
static contextType = UserContext;
render() {
const user = this.context;
return (
<div>
<p>用户名:{user.name}</p>
<p>年龄:{user.age}</p>
</div>
);
}
}
在上述例子中,我们将主题颜色和用户信息分割到不同的 Context 中。ThemeContext
用于传递主题相关的数据,UserContext
用于传递用户相关的数据。这样,不同的组件可以根据自己的需求订阅相应的 Context,获取所需的数据,而不会相互干扰。每个组件只需要关注自己所使用的 Context,提高了代码的可读性和可维护性。同时,当某个 Context 的数据发生变化时,只有订阅了该 Context 的组件才会重新渲染,避免了不必要的重新渲染。
- 对消费组件使用
React.memo()
进行包裹:React.memo
可以对函数组件进行浅比较,如果组件的 props 没有变化,就不会触发重新渲染。通过将消费context
的组件用React.memo()
包裹,可以避免不必要的重新渲染。
例如,假设有一个 ContextProvider
组件提供 context value
,以及一个使用该 context
的子组件 ConsumerComponent
,优化后的代码可能如下所示:
const ContextProvider = ({ children }) => {
// 管理 context value 的状态
const [value, setValue] = useState(/* 初始值 */);
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
};
const ConsumerComponent = React.memo(({ contextValue }) => {
// 仅根据 context value 进行渲染或处理逻辑
return <div>{/* 使用 context value 的相关逻辑 */}</div>;
});
在上述示例中,ContextProvider
负责管理 context value
的状态变化,而 ConsumerComponent
是使用 context
的消费组件,并通过 React.memo()
进行了包裹。这样,当 value
发生变化时,只有 ConsumerComponent
会根据浅比较来决定是否重新渲染,而不是整个挂载节点树都重新渲染。
通过以上方式,可以减少使用 context
时不必要的重新渲染,提高应用的性能。但具体的优化策略还需要根据项目的实际情况进行选择和调整。同时,还需注意避免在 context
中传递过于复杂或频繁变化的数据,以减少不必要的渲染次数。
createContext 和 useContext 有什么区别, 是做什么用的
createContext
和 useContext
createContext
和useContext
是React中用于处理上下文(Context)的两个钩子函数,它们用于在组件之间共享数据。
createContext
用于创建一个上下文对象,该对象包含Provider
和Consumer
两个组件。createContext
接受一个初始值作为参数,该初始值将在没有匹配的Provider
时被使用。
useContext
用于在函数组件中访问上下文的值。它接受一个上下文对象作为参数,并返回当前上下文的值。
具体区别和用途如下:
createContext
:createContext
用于创建一个上下文对象,并指定初始值。它返回一个包含Provider
和Consumer
组件的对象。Provider
组件用于在组件树中向下传递上下文的值,而Consumer
组件用于在组件树中向上获取上下文的值。
const MyContext = createContext(initialValue);
useContext
:useContext
用于在函数组件中访问上下文的值。它接受一个上下文对象作为参数,并返回当前上下文的值。使用useContext
可以避免使用Consumer
组件进行嵌套。
const value = useContext(MyContext);
使用上下文的主要目的是在组件树中共享数据,避免通过逐层传递props
的方式传递数据。上下文可以在跨组件层级的情况下方便地共享数据,使组件之间的通信更加简洁和灵活。
使用步骤如下:
- 使用
createContext
创建一个上下文对象,并提供初始值。 - 在组件树中的某个位置使用
Provider
组件,将要共享的数据通过value
属性传递给子组件。 - 在需要访问上下文数据的组件中使用
useContext
钩子,获取上下文的值。
需要注意的是,上下文中的数据变化会触发使用该上下文的组件重新渲染,因此应谨慎使用上下文,避免无谓的性能损耗。
代码示范
当使用createContext
和useContext
时,以下是一个简单的代码示例:
import React, { createContext, useContext } from 'react';
// 创建上下文对象
const MyContext = createContext();
// 父组件
function ParentComponent() {
const value = 'Hello, World!';
return (
// 提供上下文的值
<MyContext.Provider value={value}>
<ChildComponent />
</MyContext.Provider>
);
}
// 子组件
function ChildComponent() {
// 使用 useContext 获取上下文的值
const value = useContext(MyContext);
return <div>{value}</div>;
}
// 使用上述组件
function App() {
return <ParentComponent />;
}
在上述示例中,我们首先使用createContext
创建一个上下文对象MyContext
。然后,在ParentComponent
组件中,我们通过MyContext.Provider
组件提供了上下文的值,值为'Hello, World!'
。在ChildComponent
组件中,我们使用useContext
钩子获取了上下文的值,并将其显示在页面上。
最终,我们在App
组件中使用ParentComponent
组件作为根组件。当渲染应用程序时,ChildComponent
将获取到上下文的值并显示在页面上。
通过这种方式,ParentComponent
提供了上下文的值,ChildComponent
通过useContext
钩子获取并使用该值,实现了组件之间的数据共享。
useMemo
React.memo
和 useMemo
是在 React 中处理性能优化的两个工具,虽然它们名称相似,但是它们的作用和使用方法是不同的。
React.memo
是高阶组件,它可以用来优化函数组件的渲染性能。它会比较当前组件的 props
和 state
是否发生了变化,如果都没有变化,就不会重新渲染该组件,而是直接使用之前的结果。例如:
import React from 'react';
const MyComponent = React.memo(props => {
return <div>{props.value}</div>;
});
在上面的代码中,React.memo
包装了一个简单的函数组件 MyComponent
。如果该组件的 value
prop 和 state
没有发生变化,那么就会直接使用之前的结果不会重新渲染。
useMemo
是 React
中一个 hooks,它可以用来缓存计算结果,从而优化组件渲染性能。它接受两个参数:要缓存的计算函数和依赖项数组。每当依赖项发生变化时,该计算函数就会重新计算,并返回一个新的结果。例如:
import React, { useMemo } from 'react';
const MyComponent = props => {
const result = useMemo(() => expensiveComputation(props.value), [props.value]);
return <div>{result}</div>;
};
在上面的代码中,我们传递了一个计算函数 expensiveComputation
,以及一个依赖项数组 [props.value]
。如果依赖项没有发生变化,myValue
就会被缓存起来,不会重新计算。
总的来说:
React.memo
的作用是优化函数组件的渲染性能,它可以比较组件的 props
和 state
是否发生变化,如果没有变化,就不会重新渲染。
useMemo
的作用是缓存计算结果,从而避免重复计算,它接受一个计算函数和一个依赖项数组,当依赖项发生变化时,计算函数就会重新计算,返回一个新的结果,否则就会使用之前的缓存结果。
useCallback 是否支持异步函数
useRef ?
答案
- 不触发视图更新的信息,可以使用 useRef 来存储。
- 典型使用场景
- 保留 dom 引用
- 记录 timer 等句柄信息
function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); const intervalRef = useRef(null); function handleStart() { setStartTime(Date.now()); setNow(Date.now()); clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setNow(Date.now()); }, 10); } function handleStop() { clearInterval(intervalRef.current); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> <button onClick={handleStop}> Stop </button> </> ); }
- 注意事项
延伸阅读
- Referencing values with refs 官方文档说明 reference 的使用
- Refs and the DOM 官方文档详细讲解 Refs
useLayoutEffect 和 useEffect 有什么区别?
关键词:useLayoutEffect 和 useEffect 区别
useLayoutEffect 和 useEffect 的主要区别在于它们执行的时机。
-
useLayoutEffect: useLayoutEffect 是在 DOM 更新完成但在浏览器绘制之前同步执行的钩子函数。它会在 DOM 更新之后立即执行,阻塞浏览器的绘制过程。这使得它更适合于需要立即获取最新 DOM 布局信息的操作,如测量元素尺寸或位置等。使用 useLayoutEffect 可以在更新后同步触发副作用,从而保证 DOM 的一致性。
-
useEffect: useEffect 是在组件渲染完毕后异步执行的钩子函数。它会在浏览器完成绘制后延迟执行,不会阻塞浏览器的绘制过程。这使得它更适合于处理副作用操作,如发送网络请求、订阅事件等。使用 useEffect 可以将副作用操作放到组件渲染完成后执行,以避免阻塞浏览器绘制。
总结:
- useLayoutEffect 是同步执行的钩子函数,在 DOM 更新后立即执行,可能会阻塞浏览器的绘制过程;
- useEffect 是异步执行的钩子函数,在浏览器完成绘制后延迟执行,不会阻塞浏览器的绘制过程。
通常情况下,应优先使用 useEffect,因为它不会阻塞浏览器的渲染,并且能够满足大多数的副作用操作需求。只有在需要获取最新的 DOM 布局信息并立即触发副作用时,才需要使用 useLayoutEffect。