本文目录

[[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.memoReact.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 />Componenttype 就是 Component 函数。
  • 位置很重要: 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);
  }
}

工作流程

  • 组件挂载时:
    1. 创建 effect 对象
    2. 将 effect 添加到更新队列
    3. 在 commit 阶段执行 effect
  • 组件更新时:
    1. 比较依赖数组是否变化
    2. 如果依赖变化,先执行清理函数,再执行新的 effect
    3. 如果依赖未变化,跳过 effect 执行
  • 组件卸载时:
    1. 执行所有 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);
  }
}

工作流程

  • 组件挂载时:
    1. 创建 effect 对象
    2. 将 effect 添加到更新队列
    3. 在 commit 阶段执行 effect
  • 组件更新时:
    1. 比较依赖数组是否变化
    2. 如果依赖变化,先执行清理函数,再执行新的 effect
    3. 如果依赖未变化,跳过 effect 执行
  • 组件卸载时:
    1. 执行所有 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 内部