我们在函数式组件中会调用各种hook
,这些hook
或多或少都会使用官方的api
,如useState,useReducer,useEffect
等,这些官方的api
便于我们操作状态,处理副作用,做缓存,做优化等等
上节提到,我们全局有一个hook
的注册机,每次调用这些api
其实最终都会通过注册机的分发走到真正的处理函数上
在渲染逻辑跟更新逻辑中,注册机上挂载的hook
都是不相同的,所以对于每一个hook api
,都有渲染
跟更新
两个函数
useState
在渲染时调用的是mountState
函数,在更新时调用的是updateState
const State = {
basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return isFunction(action) ? (action as Function)(state) : action
},
mountState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
if (isFunction(initialState)) {
initialState = (initialState as Function)()
}
return Reducer.mountReducer(State.basicStateReducer, initialState)
},
updateState<S>(_initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
return Reducer.updateReducer(State.basicStateReducer)
},
}
可以看到,这两个函数只是在Reducer
上又封装了一层,所以实现的关键还在reducer
上
在reducer
中,也存在着mountReducer
与updateReducer
函数,先从mountReducer
分析起
在渲染阶段,我们先通过mountWorkInProgressHook
创建了一个新的Hook
对象,在创建的过程中,会把其插入当前hook
队列的末尾。
由于每个Hook
对象都是一个独立的更新单元,它有着自己的state
和updateQueue
,所以我们也会初始化掉这些变量,最后返回一个dispathAction
的闭包
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseUpdate: null,
queue: null,
next: null,
}
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook // 第一次
} else {
workInProgressHook = workInProgressHook.next = hook // 插入链表中
}
return workInProgressHook
}
mountReducer<S, I, A>(reducer: (s: S, a: A) => S, initialArg: I, init?: (i: I) => S): [S, Dispatch<A>] {
const hook: Hook = mountWorkInProgressHook()
let initialState: S = null
if (init !== undefined) {
initialState = init(initialArg)
} else {
initialState = initialArg as any
}
hook.memoizedState = hook.baseState = initialState
const queue = hook.queue = {
last: null,
dispatch: null,
eagerReducer: reducer,
eagerState: initialState,
}
const dispatch: Dispatch<A> = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue)
return [hook.memoizedState, dispatch]
}
function dispatchAction<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
const { alternate } = fiber
...
}
useState
和useReducer
的第二个返回值都是dispatchAction
的一个带参版,前两个参数已经在mountReducer
中挂载上。这个函数接受一个action
,我们将其封装成一个Update
对象
const currentTime = requestCurrentTime()
const expirationTime = computeExpirationTimeForFiber(currentTime, fiber)
const update: Update<S, A> = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
}
之后,我们将其放到任务队列的队尾,这里的updateQueue
不同于ClassComponent
,采用的是环形链表
const { last } = queue
if (last === null) {
update.next = update // 第一个update,创建环形链表
} else {
const first = last.next
if (first !== null) {
update.next = first
}
last.next = update
}
queue.last = update
react
在这个函数的最后,做了一层优化。如在setTimeout
等一些异步情况下触发了一个dispatch
,由于在下一个事件循环,当前的fiber
或许不在工作中,此时可以提前计算出State
,减轻update
时的负担
// 当前工作队列为空,在进入render阶段前提前计算下一个state,update时可以根据eagerReducer直接返回eagerState
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
const { eagerReducer } = queue
if (eagerReducer !== null) {
const currentState: S = queue.eagerState
const eagerState = eagerReducer(currentState, action)
// 存储提前计算的结果,如果在更新阶段reducer没有发生变化,可以直接使用eager state,不需要重新调用eager reducer在调用一遍
update.eagerReducer = eagerReducer
update.eagerState = eagerState
if (Object.is(eagerState, currentState)) {
return
}
}
}
在mount
阶段我们会生成一条Hook
队列。放在Fiber
的memoizedState
上,当更新时,我们会依次取出当前队列头部的Hook
,由于我们在编码时已经约束了hook
的调用条件,所以取出时的顺序与我们mount
插入时的顺序一定是一样的,调用updateWorkInProgressHook
获取到当前的Hook
对象
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook) {
workInProgressHook = nextWorkInProgressHook
nextWorkInProgressHook = workInProgressHook.next
currentHook = nextCurrentHook
nextCurrentHook = currentHook !== null ? currentHook.next : null
} else {
currentHook = nextCurrentHook
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
}
if (workInProgressHook === null) {
workInProgressHook = firstWorkInProgressHook = newHook // 第一次
} else {
workInProgressHook = workInProgressHook.next = newHook // 插入链表中
}
nextCurrentHook = currentHook.next
}
return workInProgressHook
}
同样按照fiber
的思路,update
时统一使用workInProgressHook
,如果没有workInProgressHook
,会参照currentHook
赋值一份Hook
updateReducer<S, A>(reducer: (s: S, a: A) => S): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook()
const { queue } = hook
...
}
获取到hook
后,整个updateReducer
的部分就很简单,类似于ClassComponent
由于整个任务队列是个环形链表,所以先把环形链表解开
let first: Update<S, A> = null
if (baseUpdate !== null) {
if (last !== null) { // 从一次停顿的地方开始,为了防止无限循环,需把环形链表解开
last.next = null
}
first = baseUpdate.next
} else {
first = last !== null ? last.next : null
}
随后,从队列头开始遍历,找到优先级大于当前更新优先级的update
,依次调用它们,直到队列尾,这个有个关键,前面提到,我们在dispatchAction
做了一层优化,提前进行了state
的计算,所以,这里通过eagerReducer
是否与传入的reducer
相等来判断是否需要获取计算结果
if (first !== null) {
let newState: S = baseState
let prevUpdate: Update<S, A> = baseUpdate
let update: Update<S, A> = first
let newBaseState: S = null
let newBaseUpdate: Update<S, A> = null
let didSkip: boolean = false
do {
const updateExpirationTime: ExpirationTime = update.expirationTime
if (updateExpirationTime < renderExpirationTime) {
if (!didSkip) { // 优先级低需要跳过,如果是第一个跳过的Update,需要记录下来
didSkip = true
newBaseState = newState
newBaseUpdate = prevUpdate
}
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime
}
} else {
if (update.eagerReducer === reducer) { // 直接使用提前计算的结果
newState = update.eagerState
} else {
const { action } = update
newState = reducer(newState, action)
}
}
prevUpdate = update
update = update.next
} while (update !== null && update !== first)
if (!didSkip) {
newBaseState = newState
newBaseUpdate = prevUpdate
}
if (!Object.is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate()
}
hook.memoizedState = newState
hook.baseUpdate = newBaseUpdate
hook.baseState = newBaseState
queue.eagerReducer = reducer
queue.eagerState = newState
}
const { dispatch } = queue
return [hook.memoizedState, dispatch]
最后,返回计算出的state
和dispatch
pureComponent
和memo
都采取的是浅比较,如果props
的引用不同,都会触发重新渲染,所以react
的基本优化法则,就是在传递Props
引用类型时要提前定义,防止无关渲染
然而在FunctionComponent
中,由于每次更新都会重新执行,所以在FunctionComponent
中声明的一些函数,引用类型值每次渲染都会不同,带来很多无关渲染,增大性能损耗,所以,官方出了两个用来做优化的hook
,useCallback
与useMemo
它的实现非常简单,经由hook
存储一份值,在update
时拿出之前的值,进行比较,相等返回老的,不相等则返回新的
const Callback = {
mountCallback<T>(callback: T, deps?: any[]): T {
const hook = mountWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
hook.memoizedState = [callback, nextDeps]
return callback
},
updateCallback<T>(callback: T, deps?: any[]): T {
const hook = updateWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
const prevState = hook.memoizedState
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: any[] | null = prevState[1]
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]
}
}
}
hook.memoizedState = [callback, nextDeps]
return callback
},
}
mount
时将需要优化的callback
和nextDeps
放到hook
的memoizedState
,
在update
时,将当前的deps
与之前的prevDeps
作比较,如果相等,返回缓存的值,反之,则使用新的callback
function areHookInputsEqual(nextDeps: any[], prevDeps: any[] | null): boolean {
if (prevDeps === null) {
return false
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue
}
return false
}
return true
}
areHookInputsEqual
这个函数依次遍历数组的每一项,进行浅比较
useMemo
与useCallback
的实现大致相同,唯一的区别是useCallback
会对第一个参数进行整个缓存,而useMemo
会执行第一个参数,对其生成的值进行缓存
const Memo = {
mountMemo<T>(nextCreate: () => T, deps?: any[] | null): T {
const hook = mountWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
const nextValue = nextCreate()
hook.memoizedState = [nextValue, nextDeps]
return nextValue
},
updateMemo<T>(nextCreate: () => T, deps?: any[] | null): T {
const hook = updateWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
const prevState = hook.memoizedState
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: any[] | null = prevState[1]
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]
}
}
}
const nextValue = nextCreate()
hook.memoizedState = [nextValue, nextDeps]
return nextValue
},
}
useRef
相当于FunctionComponent
里的一块引用区域,无论如何渲染更新,它的值都保持最新,它的实现可能是useXXX
里面最简单的一个,保存一个引用,存到hook
中
const Ref = {
mountRef<T>(initialValue: T): { current: T } {
const hook = mountWorkInProgressHook()
const ref = { current: initialValue }
hook.memoizedState = ref
return ref
},
updateRef<T>(_initialValue: T): { current: T } {
const hook = updateWorkInProgressHook()
return hook.memoizedState
},
}