Fre 框架 Hooks 实现机制深度解析
Q1: 这个 hooks 实现方式上,在 fiber 上挂 hooks 在用的时候怎么用的呢,怎么消费这个 hooks list?
A1: Hooks 在 Fiber 上的存储与消费机制
Hooks 在 Fiber 上的存储结构
每个 fiber 节点上都有一个 hooks 对象,包含三个数组:
typescript
interface Hooks {
list: HookList[] // 存储所有 hooks 的状态数据(useState、useMemo、useRef 等)
effect: HookEffect[] // 存储 useEffect 的副作用
layout: HookEffect[] // 存储 useLayout 的副作用
}基于 Cursor 的顺序访问机制
typescript
let cursor = 0
export const resetCursor = () => {
cursor = 0
}
export const getSlot = <T extends HookList = HookList>(cursor: number) => {
const current: Fiber = useFiber()
const hooks = current.hooks || (current.hooks = { list: [], effect: [], layout: [] })
if (cursor >= hooks.list.length) {
hooks.list.push([] as any)
}
return [hooks.list[cursor], current] as unknown as [Partial<T>, Fiber]
}组件渲染时的 Hook 消费流程
- 重置 cursor:每次组件重新渲染时,
resetCursor()将 cursor 重置为 0 - 设置上下文:
resetFiber(fiber)设置当前 fiber 为全局上下文 - 执行组件函数:
(fiber.type as FC)(fiber.props)执行组件函数 - 顺序访问:组件内部调用的每个 hook 都通过
getSlot(cursor++)获取对应位置的状态
具体 Hook 的消费示例
以 useState 为例:
typescript
export const useReducer = <S, A>(
reducer?: Reducer<S, A>,
initState?: S | (() => S)
): [S, Dispatch<A>] => {
const [hook, current] = getSlot<HookReducer>(cursor++) // 获取当前位置的 hook 数据
if (hook.length === 0) {
hook[0] = isFn(initState) ? initState() : initState // 首次调用时初始化状态值
}
hook[1] = (value: A | Dispatch<A>) => {
// 创建 dispatch 函数,状态改变时触发组件更新
let v = reducer ? reducer(hook[0], value as any) : isFn(value) ? value(hook[0]) : value
if (hook[0] !== v) {
hook[0] = v
if (typeof window !== 'undefined') {
update(current) // 触发重新渲染
}
}
}
return hook as Required<HookReducer> // 返回 [状态值, dispatch函数]
}副作用 Hook 的执行时机
typescript
const bubble = (fiber: Fiber) => {
if (fiber.isComp) {
if (fiber.hooks) {
side(fiber.hooks.layout) // 同步执行 layout effects
schedule(() => side(fiber.hooks.effect) as undefined) // 异步执行 effects
}
}
}
const side = (effects?: HookEffect[]) => {
effects.forEach((e) => e[2] && e[2]()) // 清理上一次的副作用
effects.forEach((e) => (e[2] = e[0]())) // 执行新的副作用函数
effects.length = 0 // 清空副作用队列
}Q2: hooks 的值是怎么给到 ui 的呢?
A2: Hooks 值传递给 UI 的完整流程
1. Hook 值的返回机制
当组件函数执行时,hooks 直接返回存储在 fiber.hooks.list 中的当前状态值:
typescript
export const useState = <T>(initState: T | (() => T)) => {
return useReducer<T, SetStateAction<T>>(null, initState) // 返回 [状态值, dispatch函数]
}2. 组件函数执行与值传递
typescript
const updateHook = (fiber: Fiber) => {
resetCursor()
resetFiber(fiber)
fiber.node = fiber.node || fragment(fiber)
try {
let children = (fiber.type as FC)(fiber.props) // 执行组件函数,获取 JSX
reconcileChildren(fiber, simpleVnode(children)) // 处理返回的 JSX
} catch (e) {
// 错误处理
}
}3. 值传递的详细过程
以具体组件为例:
javascript
function App() {
const [list, setList] = useState([1, 2, 3]); // hook 返回当前状态值
return (
<div>
{list.map((d) => ( // 直接使用 hook 返回的值构建 JSX
<span>{d}</span>
))}
<button onClick={() => setList(list.concat(4))}>+</button>
</div>
);
}执行流程:
useState([1, 2, 3])从fiber.hooks.list[0]获取状态值[1, 2, 3]list.map((d) => <span>{d}</span>)使用状态值创建子元素数组- 返回包含这些子元素的 JSX 结构
- JSX 被转换为 fiber 节点树,最终渲染为 DOM
4. 状态更新触发重新渲染
当用户交互触发状态更新时:
typescript
hook[1] = (value: A | Dispatch<A>) => {
let v = reducer ? reducer(hook[0], value as any) : isFn(value) ? value(hook[0]) : value
if (hook[0] !== v) {
hook[0] = v // 更新 hook 状态
update(current) // 触发组件重新渲染
}
}重新渲染时会重新执行组件函数,获取更新后的状态值,生成新的 JSX,通过 diff 算法更新 DOM。
Q3: useState([1, 2, 3]) 从 fiber.hooks.list[0][0] 这个怎么知道从 0 获取的?
A3: 基于 Cursor 的位置索引机制
全局 Cursor 变量
typescript
let cursor = 0
export const resetCursor = () => {
cursor = 0
}这个全局的 cursor 变量追踪当前正在处理的 hook 的索引位置。
组件渲染时 Cursor 的重置
typescript
const updateHook = (fiber: Fiber) => {
resetCursor() // 每次组件重新渲染时,cursor 重置为 0
resetFiber(fiber)
// ...
let children = (fiber.type as FC)(fiber.props) // 执行组件函数
}Hook 调用时 Cursor 的递增
typescript
export const useReducer = <S, A>(...) => {
const [hook, current] = getSlot<HookReducer>(cursor++) // 关键:cursor++
// ...
}具体执行顺序示例
假设有这样一个组件:
javascript
function App() {
const [count, setCount] = useState(0); // 第1个hook调用
const [name, setName] = useState('hello'); // 第2个hook调用
const [list, setList] = useState([1, 2, 3]); // 第3个hook调用
return <div>{count} {name} {list.join(',')}</div>;
}首次渲染:
resetCursor()→cursor = 0useState(0)→getSlot(0)→ 访问fiber.hooks.list[0],cursor++→cursor = 1useState('hello')→getSlot(1)→ 访问fiber.hooks.list[1],cursor++→cursor = 2useState([1, 2, 3])→getSlot(2)→ 访问fiber.hooks.list[2],cursor++→cursor = 3
重新渲染时:
resetCursor()→cursor = 0useState(0)→getSlot(0)→ 访问fiber.hooks.list[0](获取之前保存的状态)useState('hello')→getSlot(1)→ 访问fiber.hooks.list[1]useState([1, 2, 3])→getSlot(2)→ 访问fiber.hooks.list[2]
为什么必须保持调用顺序
这就是 React Hooks 规则的原因:
javascript
// ❌ 错误:条件性调用 hooks
function BadComponent({ condition }) {
if (condition) {
const [count, setCount] = useState(0); // 有时在位置0,有时不存在
}
const [name, setName] = useState('hello'); // 位置会变化!
}
// ✅ 正确:始终相同顺序调用
function GoodComponent({ condition }) {
const [count, setCount] = useState(condition ? 0 : null);
const [name, setName] = useState('hello');
}Q4: 所以这个 fiber 是用来描述一个组件的对吗?
A4: Fiber 描述虚拟 DOM 树中的所有节点类型
Fiber 的作用范围
Fiber 不仅仅描述组件,而是描述虚拟 DOM 树中的每个节点,包括:
typescript
export type Fiber = FiberHost | FiberComp | FiberBase
export interface FiberHost extends FiberBase {
type?: string // HTML 元素类型,如 'div', 'span'
props: PropsOf<string>
isComp: false
}
export interface FiberComp extends FiberBase {
type: FC // 函数组件
props: PropsOf<FC>
isComp: true
}Fiber 节点的分类
- 组件节点 (FiberComp):函数组件、类组件等
- HTML 元素节点 (FiberHost):div、span、button 等
- 文本节点 (TextFiber):纯文本内容
- Fragment 节点:文档片段
具体示例的 Fiber 树结构
javascript
function App() {
const [count, setCount] = useState(0);
return (
<div className="container">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}对应的 fiber 树:
RootFiber (根节点)
└── App (FiberComp, isComp: true, type: App函数)
└── div (FiberHost, isComp: false, type: 'div')
├── h1 (FiberHost, isComp: false, type: 'h1')
│ └── "Count: 0" (TextFiber)
└── button (FiberHost, isComp: false, type: 'button')
└── "Click me" (TextFiber)不同类型 Fiber 的处理方式
组件 Fiber (FiberComp):
- 执行组件函数获取 JSX
- 管理 hooks 状态
- 处理副作用
HTML 元素 Fiber (FiberHost):
- 创建真实 DOM 元素
- 设置属性、样式、事件
- 直接对应页面元素
只有组件 fiber 才会有 hooks:
typescript
interface FiberBase {
hooks?: Hooks // 只有组件 fiber 才使用 hooks
// ...
}Q5: 所以其实可以理解为在处理一个 jsx 组件的时候,会执行组件函数的逻辑,把 hooks 函数执行的逻辑挂到组件类型的 fiber 的 hooks 上,然后在更新的时候因为有值了,就会直接拿这个新的值,对于副作用会在 bubble 阶段执行,那 host dom 节点类型的 fiber 是用来直接渲染 dom 吗?
A5: 完全正确!让我补充 Host DOM 节点的渲染机制
组件 Fiber 的处理流程
typescript
const updateHook = (fiber: Fiber) => {
resetCursor() // 重置 hook 索引
resetFiber(fiber) // 设置当前 fiber 上下文
fiber.node = fiber.node || fragment(fiber) // 组件创建文档片段
try {
let children = (fiber.type as FC)(fiber.props) // 执行组件函数,hooks 在此时挂载
reconcileChildren(fiber, simpleVnode(children)) // 处理返回的 JSX
} catch (e) {
// 错误处理
}
}当执行 (fiber.type as FC)(fiber.props) 时:
- 组件函数内的所有 hooks 调用会将状态存储到
fiber.hooks.list中 - 副作用会被收集到
fiber.hooks.effect和fiber.hooks.layout中
副作用在 Bubble 阶段执行
typescript
const bubble = (fiber: Fiber) => {
if (fiber.isComp) {
if (fiber.hooks) {
side(fiber.hooks.layout) // 同步执行 layout effects
schedule(() => side(fiber.hooks.effect) as undefined) // 异步执行 effects
}
}
}Host Fiber 直接对应 DOM 元素
1. 创建真实 DOM 元素:
typescript
export const createElement = (fiber: FiberHost) => {
const dom =
fiber.type === '#text'
? document.createTextNode('') // 文本节点
: fiber.lane & TAG.SVG
? document.createElementNS('http://www.w3.org/2000/svg', fiber.type) // SVG 元素
: document.createElement(fiber.type) // 普通 HTML 元素
updateElement(dom, {}, fiber.props) // 设置属性和事件
return dom
}2. 在 Commit 阶段实际操作 DOM:
typescript
export const commit = (fiber?: FiberFinish) => {
// ...
let { op, ref, cur } = fiber.action || {}
let parent = fiber?.parent?.node
if (op & TAG.INSERT || op & TAG.MOVE) {
// 插入或移动 DOM 节点
parent.insertBefore(cur?.node, ref?.node)
}
if (op & TAG.UPDATE) {
// 更新 DOM 属性
const node = fiber.node
updateElement(
node,
(fiber.alternate as FiberHost)?.props || {},
(fiber as FiberHost).props
)
}
// ...
}3. DOM 属性和事件的处理:
typescript
export const updateElement = (dom: HTMLElementEx, aProps: PropsOf<string>, bProps: PropsOf<string>) => {
jointIter(aProps, bProps, (name, a, b) => {
if (name === 'style' && !isStr(b)) {
// 处理样式对象
} else if (name[0] === 'o' && name[1] === 'n') {
// 处理事件监听器
name = name.slice(2).toLowerCase()
if (a) dom.removeEventListener(name, a)
dom.addEventListener(name, b)
} else if (name in dom && !(dom instanceof SVGElement)) {
// 设置 DOM 属性
dom[name] = b || ''
} else {
// 设置 HTML 属性
dom.setAttribute?.(name, b)
}
})
}Q6: Fre 框架是如何实现时间切片(Time Slicing)和可中断渲染的?
A6: 基于 shouldYield 的协作式调度机制
可中断的 reconcile 循环
typescript
const reconcile = (fiber?: Fiber) => {
while (fiber && !shouldYield()) { // 关键:检查是否需要让出控制权
fiber = capture(fiber) as any
}
return fiber ? reconcile.bind(null, fiber) : null // 返回继续函数,实现可恢复
}调度机制
typescript
export const update = (fiber?: Fiber) => {
if (!fiber.dirty) {
fiber.dirty = true
schedule(() => reconcile(fiber)) // 将 reconcile 任务加入调度队列
}
}核心思想:
- 每处理一个 fiber 节点后检查
shouldYield() - 如果需要让出控制权,返回继续函数
- 通过
schedule将剩余工作重新加入队列 - 实现了可中断、可恢复的渲染过程
Q7: Fre 是如何处理组件的 memo 优化的?具体的 shouldUpdate 逻辑是什么?
A7: 基于 Props 浅比较的 Memo 机制
Memo 检查逻辑
typescript
export const isMemo = (fiber: Fiber) => {
if (
(fiber.type as FC).memo && // 组件标记为 memo
fiber.type === fiber.alternate?.type && // 组件类型相同
fiber.alternate?.props // 存在旧的 props
) {
let scu = (fiber.type as FC).shouldUpdate || shouldUpdate
if (!scu(fiber.props, fiber.alternate.props)) { // props 没有变化
return true // 跳过重新渲染
}
}
return false
}默认的 shouldUpdate 实现
typescript
const shouldUpdate = (
a: Record<string, unknown>,
b: Record<string, unknown>
) => {
for (let i in a) if (!(i in b)) return true // 旧 props 中有新 props 没有的属性
for (let i in b) if (a[i] !== b[i]) return true // 任何属性值发生变化
return false // 所有属性都相同
}Memo 组件的处理流程
typescript
const capture = (fiber: Fiber) => {
fiber.isComp = isFn(fiber.type)
if (fiber.isComp) {
if (isMemo(fiber)) {
fiber.memo = false
return sibling(fiber) // 跳过当前组件,直接处理兄弟节点
}
// 正常处理组件...
}
}优化效果:
- 避免不必要的组件函数执行
- 减少 hooks 重新计算
- 跳过子树的 reconcile 过程
Q8: Fre 的错误边界(Error Boundary)是如何实现的?
A8: 基于 try-catch 和 fiber 树向上查找的错误处理机制
错误捕获和处理
typescript
const updateHook = (fiber: Fiber) => {
resetCursor()
resetFiber(fiber)
fiber.node = fiber.node || fragment(fiber)
try {
let children = (fiber.type as FC)(fiber.props)
reconcileChildren(fiber, simpleVnode(children))
} catch (e) {
if (e instanceof Promise) {
return suspenseRender(fiber, e).sibling // Suspense 处理
} else {
return errorBoundaryRender(fiber, e).child // 错误边界处理
}
}
}错误边界查找和渲染
typescript
const errorBoundaryRender = (fiber, error) => {
const boundary = getBoundary(fiber, ErrorBoundary) // 向上查找错误边界
if (!boundary) throw error // 没有找到边界,继续抛出错误
const formatError = error instanceof Error ? error : new Error(error)
reconcileChildren(
boundary,
isFn(boundary.props.fallback)
? boundary.props.fallback({ error: formatError }) // 执行 fallback 函数
: simpleVnode(boundary.props.fallback) // 直接渲染 fallback
)
return boundary
}
export const getBoundary = (fiber, name) => {
let current = fiber.parent
while (current) {
if (current.type === name) return current // 找到对应类型的边界组件
current = current.parent
}
return null
}错误处理流程:
- 组件执行时捕获错误
- 沿 fiber 树向上查找 ErrorBoundary 组件
- 找到后渲染 fallback UI
- 没找到则继续向上抛出错误
Q9: Fre 的 Suspense 机制是如何实现异步组件加载的?
A9: 基于 Promise 和 WeakMap 的 Suspense 实现
Promise 检测和 Suspense 处理
typescript
const updateHook = (fiber: Fiber) => {
// ...
try {
let children = (fiber.type as FC)(fiber.props)
reconcileChildren(fiber, simpleVnode(children))
} catch (e) {
if (e instanceof Promise) {
return suspenseRender(fiber, e).sibling // 检测到 Promise,进入 Suspense 处理
}
// ...
}
}Suspense 渲染逻辑
typescript
const suspenseRender = (fiber, promise) => {
const boundary = getBoundary(fiber, Suspense) // 查找 Suspense 边界
if (!boundary) throw promise // 没有边界,继续向上抛出
const primaryChildren = boundary.props.children
const primaryChildFragment = {
type: null,
props: { children: primaryChildren },
mode: MODE.OFFSCREEN, // 主要内容设为离屏模式
kids: [],
}
const fallbackFragment = simpleVnode(boundary.props.fallback)
fallbackFragment.key = SUSPENSE_FALLBACK_KEY
boundary.kids = []
reconcileChildren(boundary, [primaryChildFragment, fallbackFragment])
// Promise 状态管理
let pSet = suspendPromiseMap.get(promise)
if(!pSet) {
suspendPromiseMap.set(promise, new Set([boundary]))
promise.then(() => {
const s = suspendPromiseMap.get(promise);
([...s]).filter(b => !(b.flag && (b.flag & TAG.REPLACE || b.flag & TAG.REMOVE)))
.forEach(b => update(b)) // Promise 完成后重新渲染
}).finally(() => suspendPromiseMap.delete(promise))
} else pSet.add(boundary)
return boundary.child
}离屏模式处理
typescript
export const commit = (fiber?: FiberFinish) => {
if (!fiber) return
if(fiber.mode & MODE.OFFSCREEN) return commitSibling(fiber.sibling) // 跳过离屏内容
// 正常提交逻辑...
}Suspense 工作流程:
- 组件抛出 Promise
- 查找最近的 Suspense 边界
- 将主要内容设为离屏模式,渲染 fallback
- Promise 完成后重新渲染,显示主要内容
Q10: Fre 的 diff 算法具体是如何实现的?与 React 的 diff 有什么异同?
A10: 基于双端比较的高效 Diff 算法
Diff 算法主体结构
typescript
const diff = (aCh: Fiber[], bCh: Fiber[]) => {
let aHead = 0, bHead = 0, aTail = aCh.length - 1, bTail = bCh.length - 1
let bMap = {}, temp = [], actions = []
// 第一步:从尾部开始比较
while (aHead <= aTail && bHead <= bTail) {
if (!same(aCh[aTail], bCh[bTail])) break
clone(aCh[aTail], bCh[bTail])
temp.push({ op: TAG.UPDATE })
aTail--
bTail--
}
// 第二步:从头部开始比较
while (aHead <= aTail && bHead <= bTail) {
if (!same(aCh[aHead], bCh[bHead])) break
clone(aCh[aHead], bCh[bHead])
actions.push({ op: TAG.UPDATE })
aHead++
bHead++
}
// 第三步:为剩余新节点建立 key 映射
for (let i = bHead; i <= bTail; i++) {
if (bCh[i].key) bMap[bCh[i].key] = i
}
// 第四步:处理中间的复杂情况
while (aHead <= aTail || bHead <= bTail) {
const aElm = aCh[aHead], bElm = bCh[bHead]
if (aElm === null) {
aHead++
} else if (bTail + 1 <= bHead) {
removeElement(aElm) // 删除多余的旧节点
aHead++
} else if (aTail + 1 <= aHead) {
actions.push({ op: TAG.INSERT, cur: bElm, ref: aElm }) // 插入新节点
bHead++
} else if (same(aElm, bElm)) {
clone(aElm, bElm)
actions.push({ op: TAG.UPDATE })
aHead++
bHead++
} else {
const foundB = aElm.key ? bMap[aElm.key] : null
if (foundB == null) {
removeElement(aElm) // 旧节点在新列表中不存在
aHead++
} else {
if (bHead <= foundB) {
actions.push({ op: TAG.INSERT, cur: bElm, ref: aElm })
bHead++
} else {
clone(aElm, bCh[foundB])
actions.push({ op: TAG.MOVE, cur: aElm, ref: aCh[aHead] }) // 移动节点
aCh[aHead] = null
aHead++
}
}
}
}
// 第五步:合并操作序列
for (let i = temp.length - 1; i >= 0; i--) {
actions.push(temp[i])
}
return actions
}节点比较和克隆
typescript
const same = (a: Fiber, b: Fiber) => a && b && (a.type === b.type && a.key === b.key)
function clone(a: Fiber, b: Fiber) {
b.hooks = a.hooks // 复制 hooks 状态
b.ref = a.ref // 复制 ref
b.node = a.node // 复用 DOM 节点
b.kids = a.kids // 复制子节点
a.flag = TAG.REPLACE
b.alternate = a // 建立新旧 fiber 关联
}与 React Diff 的异同:
相同点:
- 都基于 key 和 type 进行节点比较
- 都采用启发式算法,时间复杂度 O(n)
- 都支持节点的增删改移操作
不同点:
- Fre 采用双端比较,React 16+ 采用单向遍历
- Fre 的实现更简洁,代码量更少
- React 有更复杂的优先级调度机制
Q11: Fre 中的 useEffect 和 useLayoutEffect 有什么区别?执行时机是怎样的?
A11: 基于调度机制的同步/异步副作用执行
副作用的收集
typescript
export const useEffect = (cb: EffectCallback, deps?: DependencyList) => {
return effectImpl(cb, deps!, 'effect')
}
export const useLayout = (cb: EffectCallback, deps?: DependencyList) => {
return effectImpl(cb, deps!, 'layout')
}
const effectImpl = (
cb: EffectCallback,
deps: DependencyList,
key: 'effect' | 'layout'
) => {
const [hook, current] = getSlot<HookEffect>(cursor++)
if (isChanged(hook[1], deps)) {
hook[0] = cb
hook[1] = deps
current.hooks[key].push(hook as Required<HookEffect>) // 分别收集到不同队列
}
}执行时机的差异
typescript
const bubble = (fiber: Fiber) => {
if (fiber.isComp) {
if (fiber.hooks) {
side(fiber.hooks.layout) // 同步执行 layout effects
schedule(() => side(fiber.hooks.effect) as undefined) // 异步执行 effects
}
}
}
const side = (effects?: HookEffect[]) => {
effects.forEach((e) => e[2] && e[2]()) // 先执行清理函数
effects.forEach((e) => (e[2] = e[0]())) // 再执行副作用函数
effects.length = 0 // 清空队列
}执行时机对比:
| Hook | 执行时机 | 是否阻塞渲染 | 使用场景 |
|---|---|---|---|
| useLayoutEffect | DOM 更新后同步执行 | 是 | DOM 测量、同步 DOM 操作 |
| useEffect | DOM 更新后异步执行 | 否 | 数据获取、订阅、手动 DOM 操作 |
Q12: Fre 是如何实现 Context 机制的?useContext 的查找逻辑是什么?
A12: 基于 Fiber 树向上查找的 Context 实现
Context 创建
typescript
export const createContext = <T>(value: T): ContextType<T> => {
const C = ({ value, children }: Parameters<ContextType<T>>[0]) => {
const ref = useRef(value)
const subs = useMemo(() => new Set<() => void>(), EMPTY_ARR) // 订阅者集合
if (ref.current !== value) {
ref.current = value
subs.forEach(cb => cb()) // 值变化时通知所有订阅者
}
return children
}
C.initialValue = value
return C as ContextType<T>
}Context 消费
typescript
export const useContext = <T>(ctx: ContextType<T>) => {
const update = useReducer(null, null)[1] as SubscriberCb // 获取更新函数
let subs: Set<SubscriberCb>
useEffect(() => () => subs?.delete(update), EMPTY_ARR) // 组件卸载时取消订阅
const fiber = getBoundary(useFiber(), ctx) // 向上查找 Context Provider
return fiber
? (subs = (fiber.hooks.list[1][0]).add(update), // 订阅更新
(fiber.hooks.list[0][0] as { current: T }).current) // 返回当前值
: ctx.initialValue // 没找到 Provider,返回默认值
}Context 查找机制
typescript
export const getBoundary = (fiber, name) => {
let current = fiber.parent
while (current) {
if (current.type === name) return current // 找到匹配的 Context Provider
current = current.parent
}
return null // 没找到,返回 null
}Context 工作流程:
- Provider 组件存储值和订阅者集合
- Consumer 通过
getBoundary向上查找最近的 Provider - 找到后订阅更新,返回当前值
- Provider 值变化时通知所有订阅者重新渲染
Q13: Fre 的性能优化策略有哪些?如何避免不必要的重新渲染?
A13: 多层次的性能优化策略
1. Fiber 复用和状态保持
typescript
function clone(a: Fiber, b: Fiber) {
b.hooks = a.hooks // 复用 hooks 状态
b.ref = a.ref // 复用 ref
b.node = a.node // 复用 DOM 节点
b.kids = a.kids // 复用子节点引用
a.flag = TAG.REPLACE
b.alternate = a // 建立新旧关联
}2. 组件级别的 Memo 优化
typescript
export const isMemo = (fiber: Fiber) => {
if (
(fiber.type as FC).memo &&
fiber.type === fiber.alternate?.type &&
fiber.alternate?.props
) {
let scu = (fiber.type as FC).shouldUpdate || shouldUpdate
if (!scu(fiber.props, fiber.alternate.props)) {
return true // 跳过重新渲染
}
}
return false
}3. 时间切片避免长时间阻塞
typescript
const reconcile = (fiber?: Fiber) => {
while (fiber && !shouldYield()) { // 定期检查是否需要让出控制权
fiber = capture(fiber) as any
}
return fiber ? reconcile.bind(null, fiber) : null
}4. 精确的依赖比较
typescript
export const isChanged = (a: DependencyList | undefined, b: DependencyList) => {
return (
!a ||
a.length !== b.length ||
b.some((arg, index) => !Object.is(arg, a[index])) // 使用 Object.is 精确比较
)
}5. 批量更新机制
typescript
export const update = (fiber?: Fiber) => {
if (!fiber.dirty) {
fiber.dirty = true // 标记为脏,避免重复调度
schedule(() => reconcile(fiber)) // 批量调度更新
}
}性能优化总结:
- 节点复用:最大化复用现有 fiber 和 DOM 节点
- 智能跳过:通过 memo 和依赖比较避免不必要的计算
- 时间切片:避免长时间阻塞主线程
- 批量处理:合并多个更新操作
- 精确比较:使用 Object.is 进行准确的值比较