本系列文章总共三篇:
课前小问题
- hooks 是如何存储状态的
- 有多个相同的 hooks 时 react 是如何区分的
定义
React hooks api 是在 react 这个库里面定义的,我们以 useState 为例:
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
我们可以发现 hooks 的定义非常简单,只是获取了 dispatch 然后调用 dispatcher 对应的 useState 属性,其它 hooks 也是类似,比如 useEffect 是调用 dispatcher 的 useEffect 属性。
接着我们就需要看看 dispatcher 到底是什么,通过查看 resolveDispatcher 我们发现 dispatcher 指向的是 ReactCurrentDispatcher.current。
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
invariant(
dispatcher !== null,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
return dispatcher;
}
通过全局搜索我们发现 ReactCurrentDispatcher.current 在 ReactFiberHooks.js 这个文件中被赋值,接下来我们就来看看这个文件。
renderWithHooks
前言
经过搜索我们发现 ReactCurrentDispatcher.current在 ReactFiberHooks.js 文件中被频繁赋值,其中最主要被赋值的地方就在 renderWithHooks 方法中,经过搜索我发现 renderWithHooks 在 ReactFiberBeginWork.js 这个文件中被多次调用,如果你之前看过上一篇文档或是对 react 的更新流程的源码比较熟悉的话,你应该知道 ReactFiberBeginWork.js 文件对应着 beginWork 这个方法,在这个方法中会找出要更新的 fiber 对象并执行对应的更新方法。
经过搜索我找到了和 function component 相关的几个方法:updateFunctionComponent 和 mountIndeterminateComponent,这两个都是更新 function component,区别是第一次渲染的时候会调用 mountIndeterminateComponent,因为第一次还无法确定是 function component 还是 class component。
mountIndeterminateComponent:
updateFunctionComponent:
接下来我们就来看看 renderWithHooks 到底做了什么。
流程图
在线地址:www.processon.com/view/link/5…
具体逻辑
通过上面的流程图,我们发现 renderWithHooks 做了如下几件事:
- 通过判断 nextCurrentHook 是否为 null 来判断是否是初次渲染,如果是初次渲染就将 ReactCurrentDispatcher.current 赋值为 HooksDispatcherOnMount 否则赋值为 HooksDispatcherOnUpdate
- 然后调用 function component 得到 children
- 判断是否存在嵌套更新(didScheduleRenderPhaseUpdate),如果存在就继续执行第二步,直到嵌套更新结束或是超过最大嵌套更新层数
- 设置当前 fiber 对象上的 memoizedState 为当前的 hook 对象,以及设置其它属性,并将 effectTag 标记为 sideEffectTag
- 重置全局变量
- 返回 children
HooksDispatcherOnMount
简介
HooksDispatcherOnMount 对象中定义了各个 hooks api 在初次渲染中的实现
流程图
在线地址:www.processon.com/view/link/5…HooksDispatcherOnUpdate
简介
HooksDispatcherOnUpdate 对象中定义了各个 hooks api 在再次渲染中的实现
流程图
在线地址:www.processon.com/view/link/5…
useState
经过前面的讲述此时你应该知道 useState 最终调用的是 ReactCurrentDispatcher.current.useState 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被赋值为 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那我们先来看一下 HooksDispatcherOnMount 中的实现。
mountState
- 首先调用 mountWorkInProgressHook 方法创建 hook 对象
- 判断传入的 initialState 也就是 useState 传入的参数是否是函数,如果是就执行它得到初始 state
- 设置 hook.memoizedState 和 hook.baseState 为 initialState,这里你就可以知道为什么 function component 使用了 hook 之后就可以保存状态了,因为状态保存在 hook 对象上了,而 hook 对象又保存在 fiber 对象的 memoizedState 属性上
- 创建 queue 对象并赋值给 hook.queue,queue 类似于 fiber 对象上面的 updateQueue
- 为将当前 fiber(workInProgress)和 queue 绑定为 dispatchAction 的前两个参数,并赋值给 dispatch
- 返回
[hook.memoizedState, dispatch]
updateState
updateState 内部调用了 updateReducer,updateRecucer 内部做了以下事情:
- 首先调用 mountWorkInProgressHook 方法创建 hook 对象
- 赋值
queue.lastRenderedReducer
为 basicStateReducer - 如果出现重复渲染(即在一次渲染中又调用了一次渲染),我们去 renderPhaseUpdates 中根据 queue 获取 update 然后遍历执行 update 链表获取 newState,然后判断 newState 和 oldState 是否相等,如果不相等就标记更新,最后返回 [newState, dispatch]
- 如果没有出现重复渲染就从 queue 找到最后一个 update,进而找到第一个 udpate,因为是循环链表所以可以通过 last.next 找到 first,然后和第四步一样循环执行 update 链表获取 newState,然后判断 newState 和 oldState 是否相等,如果不相等就标记更新,最后返回 [newState, dispatch]
dispatchAction
dispatchAction 就是 useState 返回的第二个参数
流程图
在线地址:www.processon.com/view/link/5…
具体逻辑
- 首先判断一下是否是处于一个渲染阶段的更新,如果是将 didScheduleRenderPhaseUpdate 设置为 true,这个标志位在 renderWithHooks 中被用于判断是否处于嵌套更新,接着创建一个 update 对象,再创建一个 renderPhaseUpdates Map 对象,并以 queue 为 key update 为 value 存储到 renderPhaseUpdate 中,renderPhaseUpdate 在 updateState 方法中会调用
- 如果不是处于一个渲染阶段的更新,则先计算出 expirationTime 然后创建一个 update 对象,接着将 update 放到 queue.last 这个循环链表中,接着判断一下如果当前 fiber.expirationTime = NoWork,并且 queue.lastRenderedReducer 不为空,我们就可以通过 lastRenderedReducer 计算出新的 state(eagerState),lastRenderedReducer 接受之前的 state(currentState)和 action(就是传入 useState 返回的第二个方法的参数),接着将 lastRenderedReducer 和 eagerState 赋值给 update 的 eagerReducer 和 eagerState,接着判断新的 state (eagerState)和老的 state(currentState)是否相等,如果相等就直接 return 因为没有更新产生,如果不相等那就调用 scheduleWork 进入调度阶段,这个就和上一篇讲的流程连接起来了。
本章解决的问题
- hooks 是如何存储状态的
UseEffect
经过前面的讲述此时你应该知道 useEffect 和 useState 一样,最终调用的是 ReactCurrentDispatcher.current.useEffect 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被赋值为 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那我们先来看一下 HooksDispatcherOnMount 中的实现。
mountEffect
HooksDispatcherOnMount 中 useEffect 指向的是 mountEffect,它又调用了 mountEffectImpl
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
mountEffectImpl
mountEffectImpl 做了以下事情:
- 通过 mountWorkInProgressHook 创建一个 hook 对象
- 将传入的 fiberEffectTag 设置到 sideEffectTag 上,对应到 mountEffect 就是 UpdateEffect | PassiveEffect,最终 sideEffectTag 会被设置到当前 fiber 对象的 effectTag 上(参见 renderWithHooks)
- 最后调用 pushEffect,传入 hookEffectTag(UnmountPassive | MountPassive),create,nextDeps
- 将 pushEffect 的结果赋值给 hook.memoizedState
updateEffect
在更新阶段会将 dispatcher 指向 HooksDispatcherOnUpdate,在 HooksDispatcherOnUpdate 中 useEffect 指向的是 updateEffect,它又调用了 updateEffectImpl。
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
updateEffectImpl
updateEffectImpl 做了以下事情:
- 通过 updateWorkInProgressHook 创建一个 hook 对象
- 判断 currentHook 是否为 null,currentHook 不为 null 说明不是初次渲染,获取 currentHook.memoizedState,也就是上一个 effect 对象,找到该对象的 destory 属性和 deps 属性,判断新的 deps 和老的 deps 是否相等,如果相等就调用 pushEffect 传入 NoHookEffect,表示没有 effect 需要执行,也就不会在 commit 阶段执行 unmount 和 mount,也就是调用 destroy 和 create 方法,然后 return
- 如果 currentHook 等于 null 或是新的 deps 和老的 deps 不相等,将传入的 fiberEffectTag 设置到 sideEffectTag 上(UpdateEffect | PassiveEffect),最终 sideEffectTag 会被设置到当前 fiber 对象的 effectTag 上(参见 renderWithHooks),最后调用 pushEffect,传入 hookEffectTag(UnmountPassive | MountPassive),create,nextDeps,将 pushEffect 的结果赋值给 hook.memoizedState
pushEffect
- 创建一个 effect 对象
- 将 effect 添加到 componentUpdateQueue.lastEffect 上,形成一个循环链表,componentUpdateQueue 会被添加到当前 fiber 对象的 updateQueue 上(参见 renderWithHooks)
- 返回 effect
effect
const effect: Effect = {
tag, // hookEffectTag
create, // useEffect 接收的第一个参数
destroy, // 在 mountEffect 中是 undefined
deps, // useEffect 接收的第二个参数
// Circular
next: (null: any), // 指向下一个 effect
};
commitLayoutEffects
最终生成的 updateQueue 会在 commit 阶段的 commitLayoutEffects 中执行
详情可以看上一篇
commitLayoutEffectOnFiber(commitLifeCycles)
还记得上面 mountEffectImpl 方法会将 UpdateEffect | PassiveEffect 设置到 fiber.effectTag 上,对于有 UpdateEffect 的 fiber 对象在 commitLayoutEffects 中会执行 commitLayoutEffectOnFiber 方法,它对应的就是 commitLifeCycles 方法,在该方法中对于 FunctionComponent 会执行 commitHookEffectList方法,传入 UnmountLayout, MountLayout, finishedWork
commitHookEffectList
在该方法中会对传入的 finishedWork.updateQueue 上面的 effect 对象执行 unmount 和 mount,也就是调用 effect 对象上的 destroy 方法和 create 方法,对应于 useEffect 返回的方法和传入的方法,第一次渲染设置的 destroy 为 undefined 所以第一次渲染 destroy 不会执行
useRef
useRef 和其它 hooks 一样最终调用的是 ReactCurrentDispatcher.current.useRef 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被赋值为 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那我们先来看一下 HooksDispatcherOnMount 中的实现。
mountRef
在 HooksDispatcherOnMount 中 useRef 指向的是 mountRef 方法,我们来看一下它做了什么:
- 通过 mountWorkInProgressHook 方法创建了 hook 对象
- 创建 ref 对象
const ref = { current: initialValue };
初始值就是传入 useRef 的第一个参数 - 设置
hook.memoizedState = ref;
- 返回 ref
updateRef
在 HooksDispatcherOnUpdate 中 useRef 指向的是 updateRef 方法,我们来看一下它做了什么:
- 通过 updateWorkInProgressHook 获取到 hook
- 返回 hook 对象上的 memoizedState
创建 hook
经过上面的几个 hook api 的实现我们发现每个 hook api 都需要先创建一个 hook 对象,而创建 hook 对象针对初次渲染和再次渲染这两个阶段调用的方法有所不同,我们先来看初次渲染。
mountWorkInProgressHook
初次渲染调用的是 mountWorkInProgressHook 方法,我们来看一下它做了什么:
- 创建一个 hook 对象
- 判断 workInProgressHook 是否为空,如果为空就将 workInProgressHook 和 firstWorkInProgressHook 指向新的 hook
- 如果不为空就插入其后(next),然后将 workInProgress 指向新的 hook
- 返回 workInProgress
updateWorkInProgressHook
接下来我们看看再次渲染时调用的 updateWorkInProgressHook 方法:
- 首先判断一下 nextWorkInProgressHook 是否为空,如果不为空说明当前处于渲染阶段触发的重新渲染,因为只有在重新渲染时 renderWithHooks 才会将其设置为 firstWorkInProgressHook,如果为空就将 workInProgressHook 设置为 nextWorkInProgressHook,然后将 nextWorkInProgressHook 设置为 workInProgressHook.next,然后设置 nextCurrentHook
- 如果 nextWorkInProgressHook 为空,我们将 currentHook 设置为 nextCurrentHook,也就是找到上一次渲染的 hook 对象(类似于 fiber里面的 current),然后根据 currentHook 复制一个 newHook,执行 mountWorkInProgressHook 中的第二三步,然后将 nextCurrentHook 指向 currentHook 的 next,这里我们就可以知道为什么多个 hook api 执行的时候 react 是如何一一对应的了,就是通过初次渲染形成的链表去对应的,所以千万要注意前后两次的渲染中 hook 的顺序不能有改变
- 返回 workInProgress
hook
我们来看看 hook 对象到底是个什么东西
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
memoizedState
存储 hook 对象的数据,useState 对应的就是 state,useEffect 对应的就是 effect 对象,useRef 对应的就是 ref 对象
baseState
和 useState 相关,在初次渲染时等于传入的初始 state,后续是每次计算出的新的 state
queue
类似于 fiber 对象的 updateQueue,每次调用 useState 返回的 setSomeState 方法就会创建一个 update 对象放到 queue 中,然后在 render 阶段再遍历 queue 计算出新的 state
const queue = (hook.queue = {
last: null, // 指向最后一个 update,它的 next 指向第一个 update,这是一个循环链表
dispatch: null, // dispatch 方法,用于计算出新的 state
lastRenderedReducer: reducer, // 最后一个 update 的 reducer
lastRenderedState: (initialState: any), // 指向最后一个 update 产生的 state
});
本节解决的问题
- 有多个相同的 hooks 时 react 是如何区分的
Github
包含带注释的源码、demos和流程图
github.com/kwzm/learn-…