本文目录
[[toc]]
React 前置知识
React 重要版本更新
- 16 开始正式稳定,大版本兼容
- 16.3-16.8 添加生命周期、 hooks
- 17 将 document 的事件委托,修改委托目标为根 DOM 容器,开始进行渐进式升级
- 18 全自动并发渲染、
Suspense组件、startTransition、 删除事件池、useEffect异步清理、jsx禁止返回undefined
React 设计原则
- 声明式 UI: UI 通过代码声明确定,而不是运行时手动修改更新。整个 React 就是大型的
render,建立了UI = render(state)的映射关系,根据 state 的变化映射出新的 UI - 单项数据流: 组件就是纯函数,单向传递
- 虚拟 DOM:
- 解耦底层渲染,支持在其他平台运行,比如 Android 、 iOS 等
- 支持快照一样的时间穿梭功能
- 组件化: 将 UI 、 逻辑进行抽象,便于复用与维护
React 组件对象
如下 React 组件
const App = () => (
<div>
Component
</div>
)转换为 js 即为:
const App = () => React.createElement(
// component name
'div',
// props
{},
// rest args as children
'Component',
)在 createElement 执行后会转换为:
const App = () => ({
$$typeof: Symbol(react.element),
ref: null,
key: null,
props: {
children: 'Component',
},
// 自定义组件的 type 会是函数
type: 'div',
})React 组件设计
开闭原则
开闭原则,用 React 的话来说,就是 “组件应该易于扩展,而无需更改其现有代码” 。
示例
一个常见的反模式设计如下:
const Button = ({ label, onClick, variant }: ButtonProps) => {
let className = "button"
if (variant === 'primary') {
className += ' button-primary'
} else if (variant === 'secondary') {
className += ' button-secondary'
} else if (variant === 'danger') {
className += ' button-danger'
}
return (
<button className={className} onClick={onClick}>
{label}
</button>
)
};主要的问题点在于:
- 支持新的模式需要修改组件
- 组件需要支持所有可能的模式
- 每次添加都会让测试变得更复杂
按照开闭原则重构后如下:
type ButtonBaseProps = {
label: string
onClick: () => void
className?: string
children?: React.ReactNode
}
const ButtonBase = ({
label,
onClick,
className = '',
children,
}: ButtonBaseProps) => (
<button className={`button ${className}`.trim()} onClick={onClick}>
{children || label}
</button>
);
// 每个模式单独定义组件,并且复用基础组件
const PrimaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-primary" />
);
const SecondaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-secondary" />
);
const DangerButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-danger" />
);组合模式
上面示例是往下抽象,提取更加底层的基础组件,并将其属性透传,实现的开闭原则。
但是对于业务场景来说,能继续往下抽取的情况并不常见,更多的是外层不变而内层动态改变。
在 React 中,还可以通过组合模式以及 HOC 实现开闭原则,也就是将可能改变的子组件传递给组件,这种实现方式称为组合模式。
type WithLoadingProps = {
isLoading?: boolean
}
const withLoading = <P extends object>(
WrappedComponent: React.ComponentType<P>
) => {
return ({ isLoading, ...props }: P & WithLoadingProps) => {
if (isLoading) {
return <div className="loading">Loading...</div>
}
return <WrappedComponent {...(props as P)} />
}
}
// Usage
const UserProfileWithLoading = withLoading(UserProfile)除了组件之外, hook 也可以实现可组合,例如:
const useDataFetching = <T>(url: string) => {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData()
}, [url])
const fetchData = async () => {
try {
const response = await fetch(url)
const result = await response.json()
setData(result)
} catch (e) {
setError(e as Error)
} finally {
setLoading(false)
}
}
return { data, error, loading, refetch: fetchData }
}
// Extended without modification
const useUserData = (userId: string) => {
const result = useDataFetching<User>(`/api/users/${userId}`)
// Add user-specific functionality
const updateUser = async (data: Partial<User>) => {
// Update logic
}
return { ...result, updateUser }
}对比 extends
React 团队推荐的“组合优于继承”内详细说明了原因。
使用 class component 复用的时候少了一些可扩展能力。
从另一种角度来说, class component 本质上是 带有生命周期、 effect 的 class ,而组合模式则是 带有生命周期、 effect 的 function ,从颗粒度上也可以看得出两者的灵活性差异。
单一职责
参考 关注点分离
render 性能优化
re-render
整个 render 就是一个大型的映射系统,用于处理 UI = react(state) 这对映射关系。
所以 state 是 re-render 的触发器,一切 re-render 是并且只能是 由于 state 变化。
整个 re-render 的流程有一个比较反直觉的现象: 即当父组件被 re-render 时,子组件即使 state 、 props 等都未发生变化,也会被 re-render ,为了规避这个问题, React 提供了 React.memo 机制。
了解 React.memo 之前,需要先对 re-render 再进一步剖析,为什么父组件 re-render 会导致子组件必定 re-render 。
虽然 React 推崇使用纯函数思想封装组件,但是 React 无法保证每个开发者都是这么做的,例如某个组件在 render 的时候会从 Date.now() 获取时间并渲染。
当我们将变量插入到 jsx 内的时候, React 是无法确定该值在上次渲染后,是否发生了变化,所以它需要使用悲观的方式处理这个问题,即重新渲染,可以理解为并发锁中的悲观锁。
当然,可以通过手动声明的方式,告诉 React 这个组件是无副作用的,那就是 React.memo 、 React.PureComponent ,这两种方式可以像 cjs 依赖包的 "sideEffect": false 配置一样,帮助构建器确认这是一个 “纯” 的组件/功能
使用 React Devtools ,开启 Record why each component rendered while profiling. 选项,可以在 UI 界面高亮触发 re-render 的组件,这对于调试很有帮助。
React.memo
React.memo 会对组件进行一次缓存,只要 props 、 state 没有发生变化,则可以继续重用之前的缓存。
但是,这里隐藏了一个陷阱,由于父组件 re-render 的时候,会重新执行 render 函数,在特定条件下,会导致 React.memo 失效,参考以下示例:
const PrueBox = ({ boxes }) => {
return <ul>
{
boxes.map(box => <li>{ box.name }</li>)
}
</ul>
}
const Box = React.memo(PrueBox)
const App = () => {
const [count, setCount] = useState()
const boxes = [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
]
return <>
<Box boxes={boxes} />
<h1>{ count }</h1>
<button onClick={() => setCount(count + 1)}>submit</button>
</>
}由于数组是引用类型,并且定义到 render 函数中,所以,当触发 re-render 的时候,会重新生成一份 boxes 数组,哪怕值没有变化,但是引用地址已经改变了,所以 Box 组件依旧会被 re-render 。
为了解决这个问题,需要对于 boxes 也进行缓存,避免每次 render 都修改,在极少数场景下我们可以抽取常量到独立文件,但是大部分场景下, boxes 并不是永远不会改变,而是在特定条件下改变,那么可以使用 useMemo 来处理:
const PrueBox = ({ boxes }) => {
return <ul>
{
boxes.map(box => <li>{ box.name }</li>)
}
</ul>
}
const Box = React.memo(PrueBox)
const App = () => {
const [count, setCount] = useState()
const boxes = React.useMemo(() => [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
], [/* 如果 memo 内用到了 state ,需要在这里声明,否则值一直不会变化 */])
return <>
<Box boxes={boxes} />
<h1>{ count }</h1>
<button onClick={() => setCount(count + 1)}>submit</button>
</>
}由于 useMemo 接收的参数就是函数,处理的是对象、数组。
但是如果需要将函数传递给子组件的时候,使用 useMemo 就会比较麻烦,我们需要多套一层函数,使用 React.useMemo(() => pruneFunction, []) 的形式处理。
所以 React 提供了 useCallback ,使用上与 useMemo 完全相同,但是作用于函数,而不是对象、数组,上述示例可以修改为 React.useCallback(pruneFunction, [])
useCallback 用在自定义 hook 中是比较常见的,可以参考以下示例:
function useToggle(initialValue) {
const [value, setValue] = React.useState(initialValue)
const toggle = React.useCallback(() => {
setValue(v => !v)
}, [])
return [value, toggle]
}而需要组件传参时,可以优先考虑是否需要对属性进行缓存,例如:
const AuthContext = React.createContext({})
function AuthProvider({ user, status, forgotPwLink, children }){
const memoizedValue = React.useMemo(() => {
return {
user,
status,
forgotPwLink,
}
}, [user, status, forgotPwLink])
return (
<AuthContext.Provider value={memoizedValue}>
{children}
</AuthContext.Provider>
)
}小结
- 如果组件是纯组件,并且计算量比较大,或者需要在多个地方使用,推荐使用
React.memo缓存 - 如果引用值(数组、函数、对象)需要给多个组件使用,推荐使用
useMemo/useCallback缓存 - 如果本身就是纯展示组件,没有必要进行缓存,除非分析后发现经常被 re-render ,需要手动处理
- 如果不是上述情况,不推荐使用
memo缓存,一方面是有额外的内存开销,另一方面是由于缺少 render ,可能导致视图没有及时更新,导致展示效果不符合预期
Reconciler
即协调器,是 React 更新 DOM 以匹配组件树的过程。
老的 Reconciler 使用 stack 架构,新版本使用了 fiber 架构,两种架构对比可以看 在线链接
在 stack 架构中,栈是同步结构,在执行时会对线程造成阻塞,而栈又需要清空了,才能继续接下来的工作,也就导致渲染卡顿( 更新时间超过 1 / 60 s )。
Fiber 架构中,将任务拆分成细小的阶段,将非必要立即执行的任务修改为异步,支持暂停与恢复。
在使用 Fiber 架构后, Fiber 会将 diff 任务划分为一个个的 fiber 单元,并给定优先级,优先级高的立即处理 ( requestAnimationFrame ) ,优先级低的等待空闲时处理 ( requestIdleCallback ) ,整个 Fiber 可以视为一个异步的 虚拟 stack 。
Fiber 将虚拟 DOM 渲染按照组件划分为链表形式,链表存在 next 与 sibling , next 指向第一个子节点, sibling 指向下一个兄弟节点,所以 Fiber 架构中,顺序是很重要的。
在修改成异步架构后, vDOM tree 存在一个渲染中阶段,即异步任务还未被执行完成,所以 vDOM tree 在 re-render 的时候,会有两份 vDOM 存在,在 Fiber 执行完任务后交换两个 vDOM tree , vDOM tree 实际上不是完全新的,有一部分节点依旧会被复用,比如 key 相同的节点。
工作原理
Reconciliation 工作流程如下:
- 调用组件 render 创建一个新的 element tree
- 将新的 tree 与旧的 tree 进行比较
- 找出需要使用哪些 DOM 操作来更新旧 tree
- 有效的执行 DOM 操作
名词定义
re-render: 重新调用 render 函数,用于 element tree 的比较。rebuild: 重新构建 element tree ,不进行复用。- element tree: 可以理解为构建完成的 虚拟 DOM 树,实际上在 Reconciler 的视角应该是 element tree 。
比较原则
在比较的时候遵循以下原则:
- 元素决定身份: 只有
type相同才会进行 patch ,type不同会直接 rebuild- HTML 元素的
type为 tag name - 组件的
type为组件的 function ,例如const Component = () => <div />,Component的type就是Component函数。
- HTML 元素的
- 位置很重要: React 会观察原来的节点位置是否发生 type 切换,所以在条件判断的时候:
{ condition ? <A /> : <B /> }: 会卸载原有节点,重新构建一个新的替换{ condition ? <A propsA /> : <A propsB /> }: 更新组件 props ,不会进行销毁
key可以替代位置标识组件: 使用key的时候,哪怕位置不同,也会标识为同一组件,并进行复用
key
根据上述的 比较原则 , key 是可以替代索引对组件进行标识的,所以在渲染列表的时候,尽可能为子组件添加 key 可以有效避免 rebuild 。
除了列表之外,静态组件 react 不要求提供 key ,因为其相对位置是不会发生改变的。
而动态 key 可以有效对组件进行排序,例如:
const Component = () => {
const [isReverse, setIsReverse] = useState(false)
return (
<>
{/* 可以将用户输入的值,移动到另一个组件中,也就是组件排序 */}
<Input key={isReverse ? "some-key" : null} />
<Input key={!isReverse ? "some-key" : null} />
</>
)
}然而由于位置标识的原因,大部分情况下是动态组件与静态组件一起渲染,例如:
<>
{items.map((item) => (
<ListItem key={item.id} />
))}
<StaticElement /> {/* Will this re-mount if items change? */}
</>在高版本的 React 中对这个结构进行了优化,这段代码实际上会构建为
[
// 动态组件通过额外的包裹层合并,避免影响顺序
[
{ type: ListItem, key: "1" },
{ type: ListItem, key: "2" },
],
// 位置会变成固定的第 2 个元素
{ type: StaticElement },
]React 组件开发建议
不要使用内联组件
const Parent = () => {
// InnerComponent 的 type 每次调用 render 都会更新(不是同一引用)
// 所以每次调用 render 都会重新创建 InnerComponent ,会有大量的性能损耗
const InnerComponent = () => <div>Inner content</div>
return <InnerComponent />
}尽可能进行组合
const CounterButton = () => {
// 当 state 更新的时候,只有 CounterButton 需要更新
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}
const Parent = () => {
return (
<div>
<CounterButton />
<ExpensiveComponent />
</div>
)
}通过 key 复用组件
key 是标识组件的方式中,优先级最高的一种,哪怕 type 、 顺序都不相同,只要 key 相同,就会被认为是统一组件。
参考如下代码, React 会认为这是同一个组件,但是改变了 type ,而不是不同组件。
const TabContent = ({ activeTab }) => {
// 所有 tab 都有相同的 key ,在进行 tab 切换的时候,并不会进行 rebuild ,哪怕组件的 type 并不相同。
return (
<div>
{activeTab === "profile" && <ProfileTab key="tab-content" />}
{activeTab === "settings" && <SettingsTab key="tab-content" />}
{activeTab === "activity" && <ActivityTab key="tab-content" />}
</div>
)
}这样的话,组件内部的 input 之类的原生 DOM 缓存值,也会被保留,哪怕不是同一个组件。
隔离 state 无关的组件
即状态托管( State Colocation )模式,让组件尽可能让组件靠近使用的地方,参考以下代码:
const App = () => {
const [filterText, setFilterText] = useState("")
const filteredUsers = users.filter((user) => user.name.includes(filterText))
return (
<>
<SearchBox filterText={filterText} onChange={setFilterText} />
<UserList users={filteredUsers} />
{/* 当修改 filterText 时, ExpensiveComponent 也会被 re-render */}
<ExpensiveComponent />
</>
)
}ExpensiveComponent 实际上与 state 是无关的,可以将 state 以及对应的组件隔离到子组件中,这样可以减少 re-render
const UserSection = () => {
const [filterText, setFilterText] = useState("")
const filteredUsers = users.filter((user) => user.name.includes(filterText))
return (
<>
<SearchBox filterText={filterText} onChange={setFilterText} />
<UserList users={filteredUsers} />
</>
)
}
const App = () => {
return (
<>
<UserSection />
<ExpensiveComponent />
</>
)
}组件设计
如果组件执行效率低、渲染慢,可以考虑是否在设计上还能继续优化:
- 是不是单一职责?
- 状态隔离了吗?
如果都满足了,再考虑使用 memo 进行缓存。
Recociliation Clean Architecture
Recociliation 中的设计原则:
- 单一职责: 每个组件应该有一个更新的理由,比如某个状态的变化。
- 依赖倒置: 组件应该依赖于抽象,而不是具体的实现,这使得通过组合优化性能变得更加容易。
- 接口隔离: 组件应该有最少的、集中的接口,减少了 props 修改触发 re-render 的机会。
hooks
useState
useState 是 React 中最基础的一个 Hook,它允许我们在函数组件中添加状态。其工作原理如下:
数据结构
- React 使用 Fiber 架构,每个组件对应一个 Fiber 节点
- 每个 Fiber 节点都有一个
memoizedState属性,用于存储 hooks 链表 - 每个 hook 都是一个对象,包含
memoizedState(当前状态值)和next(指向下一个 hook)
实现原理
// 伪代码实现
let currentHook = null; // 当前正在处理的 hook
let workInProgressHook = null; // 正在构建的 hook
let isMount = true; // 是否是首次渲染
function useState(initialState) {
// 获取当前 hook
let hook;
if (isMount) {
// 首次渲染,创建新的 hook
hook = {
memoizedState: initialState,
next: null,
queue: { pending: null }
};
if (!workInProgressHook) {
workInProgressHook = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
// 获取当前状态
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = typeof action === 'function' ? action(baseState) : action;
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = { action, next: null };
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
scheduleWork();
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useState 时,会从链表中取出对应的 hook
- 调用 setState 时,会将更新添加到 hook 的更新队列中
- React 会在合适的时机(如事件处理完成后)批量处理更新队列
- 处理更新时,会按顺序执行队列中的更新,得到最终状态
注意事项
- hooks 必须在函数组件的顶层调用,不能在条件语句、循环或嵌套函数中调用
- 这是因为 React 依赖 hooks 的调用顺序来正确关联状态
- 每次渲染时,hooks 的调用顺序必须保持一致
- 如果需要在条件语句中使用状态,应该将条件判断放在 hook 内部
useEffect
useEffect 是 React 中用于处理副作用的 Hook,它允许我们在函数组件中执行副作用操作(如数据获取、订阅、手动修改 DOM 等)。其工作原理如下:
数据结构
- 每个 useEffect 对应的 hook 对象包含:
memoizedState: 存储 effect 对象next: 指向下一个 hook
- effect 对象包含:
create: 副作用函数destroy: 清理函数deps: 依赖数组next: 指向下一个 effect
实现原理
// 伪代码实现
function useEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 标记当前 hook 为 effect hook
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive,
create,
undefined,
nextDeps,
);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
// 将 effect 添加到更新队列
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
// 在 commit 阶段执行 effect
function commitHookEffectList(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// 执行 effect
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}工作流程
- 组件挂载时:
- 创建 effect 对象
- 将 effect 添加到更新队列
- 在 commit 阶段执行 effect
- 组件更新时:
- 比较依赖数组是否变化
- 如果依赖变化,先执行清理函数,再执行新的 effect
- 如果依赖未变化,跳过 effect 执行
- 组件卸载时:
- 执行所有 effect 的清理函数
注意事项
- useEffect 在浏览器完成渲染后异步执行
- 清理函数会在组件卸载或依赖变化时执行
- 依赖数组为空时,effect 只会在组件挂载和卸载时执行
- 依赖数组未提供时,effect 会在每次渲染后执行
- 可以在 effect 中返回一个清理函数,用于清理副作用
- 不要在 effect 中执行阻塞渲染的操作
与 useLayoutEffect 的区别
- useEffect 在浏览器完成渲染后异步执行
- useLayoutEffect 在 DOM 更新后同步执行
- useLayoutEffect 会阻塞浏览器渲染
- 大多数情况下应该使用 useEffect
- 只有在需要同步读取 DOM 布局时才使用 useLayoutEffect
useLayoutEffect
useLayoutEffect 是 React 中用于处理副作用的 Hook,它允许我们在函数组件中执行副作用操作(如数据获取、订阅、手动修改 DOM 等)。其工作原理如下:
数据结构
- 每个 useLayoutEffect 对应的 hook 对象包含:
memoizedState: 存储 effect 对象next: 指向下一个 hook
- effect 对象包含:
create: 副作用函数destroy: 清理函数deps: 依赖数组next: 指向下一个 effect
实现原理
// 伪代码实现
function useLayoutEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 标记当前 hook 为 effect hook
hook.memoizedState = pushEffect(
HookHasEffect | HookLayout,
create,
undefined,
nextDeps,
);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
// 将 effect 添加到更新队列
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
// 在 commit 阶段执行 effect
function commitHookEffectList(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// 执行 effect
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}工作流程
- 组件挂载时:
- 创建 effect 对象
- 将 effect 添加到更新队列
- 在 commit 阶段执行 effect
- 组件更新时:
- 比较依赖数组是否变化
- 如果依赖变化,先执行清理函数,再执行新的 effect
- 如果依赖未变化,跳过 effect 执行
- 组件卸载时:
- 执行所有 effect 的清理函数
注意事项
- useLayoutEffect 在 DOM 更新后同步执行
- useLayoutEffect 会阻塞浏览器渲染
- 只有在需要同步读取 DOM 布局时才使用 useLayoutEffect
useCallback
useCallback 是 React 中用于缓存回调函数的 Hook,它允许我们在函数组件中缓存回调函数。其工作原理如下:
数据结构
- 每个 useCallback 对应的 hook 对象包含:
memoizedState: 存储回调函数next: 指向下一个 hook
- 回调函数对象包含:
deps: 依赖数组next: 指向下一个回调函数
实现原理
// 伪代码实现
function useCallback(callback, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 标记当前 hook 为 callback hook
hook.memoizedState = {
deps: nextDeps,
next: callback,
};
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useCallback 时,会从链表中取出对应的 hook
- 如果依赖数组未变化,返回缓存的回调函数
- 如果依赖数组变化,返回新的回调函数
注意事项
- useCallback 在首次渲染时会创建回调函数
- 如果需要在条件语句中使用回调函数,应该将条件判断放在 hook 内部
useMemo
useMemo 是 React 中用于缓存计算结果的 Hook,它允许我们在函数组件中缓存计算结果。其工作原理如下:
数据结构
- 每个 useMemo 对应的 hook 对象包含:
memoizedState: 存储计算结果next: 指向下一个 hook
- 计算结果对象包含:
deps: 依赖数组next: 指向下一个计算结果
实现原理
// 伪代码实现
function useMemo(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 标记当前 hook 为 memo hook
hook.memoizedState = {
deps: nextDeps,
next: create(),
};
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useMemo 时,会从链表中取出对应的 hook
- 如果依赖数组未变化,返回缓存的计算结果
- 如果依赖数组变化,重新计算并返回新的计算结果
注意事项
- useMemo 在首次渲染时会创建计算结果
- 如果需要在条件语句中使用计算结果,应该将条件判断放在 hook 内部
useRef
useRef 是 React 中用于创建可变引用的 Hook,它允许我们在函数组件中创建可变引用。其工作原理如下:
数据结构
- 每个 useRef 对应的 hook 对象包含:
memoizedState: 存储引用对象next: 指向下一个 hook
- 引用对象包含:
current: 存储引用值
实现原理
// 伪代码实现
function useRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useRef 时,会从链表中取出对应的 hook
- 返回一个新的引用对象
注意事项
- useRef 在首次渲染时会创建引用对象
- 如果需要在条件语句中使用引用,应该将条件判断放在 hook 内部
useTransition
useTransition 是 React 中用于处理过渡状态的 Hook,它允许我们在函数组件中处理过渡状态。其工作原理如下:
数据结构
- 每个 useTransition 对应的 hook 对象包含:
memoizedState: 存储过渡状态对象next: 指向下一个 hook
- 过渡状态对象包含:
isPending: 是否处于过渡状态startTransition: 开始过渡的函数
实现原理
// 伪代码实现
function useTransition() {
const hook = mountWorkInProgressHook();
const transition = {
isPending: false,
startTransition,
};
hook.memoizedState = transition;
return transition;
}
function startTransition(callback) {
// 实现过渡逻辑
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useTransition 时,会从链表中取出对应的 hook
- 返回一个新的过渡状态对象
注意事项
- useTransition 在首次渲染时会创建过渡状态对象
- 如果需要在条件语句中使用过渡状态,应该将条件判断放在 hook 内部
useReducer
useReducer 是 React 中用于管理状态的 Hook,它允许我们在函数组件中管理状态。其工作原理如下:
数据结构
- 每个 useReducer 对应的 hook 对象包含:
memoizedState: 存储状态next: 指向下一个 hook
- 状态对象包含:
reducer: 状态管理函数state: 存储状态
实现原理
// 伪代码实现
function useReducer(reducer, initialState) {
const hook = mountWorkInProgressHook();
const state = {
reducer,
state: initialState,
};
hook.memoizedState = state;
return state;
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useReducer 时,会从链表中取出对应的 hook
- 返回一个新的状态对象
注意事项
- useReducer 在首次渲染时会创建状态对象
- 如果需要在条件语句中使用状态,应该将条件判断放在 hook 内部
useContext
useContext 是 React 中用于访问上下文的 Hook,它允许我们在函数组件中访问上下文。其工作原理如下:
数据结构
- 每个 useContext 对应的 hook 对象包含:
memoizedState: 存储上下文对象next: 指向下一个 hook
- 上下文对象包含:
context: 存储上下文
实现原理
// 伪代码实现
function useContext(context) {
const hook = mountWorkInProgressHook();
const contextValue = React.useContext(context);
hook.memoizedState = contextValue;
return contextValue;
}工作流程
- 首次渲染时,React 会按顺序创建 hooks 链表
- 每次调用 useContext 时,会从链表中取出对应的 hook
- 返回上下文对象
注意事项
- useContext 在首次渲染时会创建上下文对象
- 如果需要在条件语句中使用上下文,应该将条件判断放在 hook 内部