React hooks 源码全方位解析
本篇博客对React17的Hooks源码进行了剖析, 目的是理解React的Hooks设计
1. useEffect
用法
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
对应的生命周期: Mount
, Update
, WillUnmount
在deps
发生改变的时候执行effectCallback
, 如果依赖为空, 那么Effect
会在Mount
的时候执行一次
如果想在Unmount
卸载阶段进行卸载监听和订阅, 需要返回一个匿名函数
Warning: 如果没有在WillUnmount
阶段对监听和订阅进行卸载, 一直存在可能会造成内存泄露
import {useEffect} from "react";
useEffect(() => {
document.addEventListener("mousemove", handleMouseMove)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
}
}, [])
源码实现:
1.1 入口定义
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
1.2 resolveDispatch()
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// ...
return ((dispatcher: any): Dispatcher);
}
1.3 ReactCurrentDispatcher.current
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher)
}
1.4 源码在react-reconciler中的ReactFiberHooks
2. useState
2.1 fiber设计
在function component
中利用fiber
保存了hooks
列表
// TODO: first hook's action
type Fiber = {|
// 记录function component的hooks列表, 单链表结构
memoizedState: any
|}
2.2 dispatchAction执行update
import {useState} from "react";
const [num, setNum] = useState(0)
调用setNum时, 实际上是调用了dispatchAction.bind()
在dispatchAction
创建了update
的环状单链表updateQueue
// 环状单向链表
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
3 useState and useReducer
3.1 理解
首先需要明白的是, useState
和useReducer
这俩个hooks
是Redux
的创始人Dan
加入React核心团队后带来的变化
所以在理解这俩个hooks的设计理念的时候, 可以多想想Redux中dispatch
和action
的概念, 方便理解这俩个Hooks
function useState(initialState) {
// resolveDispatcher读取ReactCurrentDispatcher.current获取当前的dispatcher
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
在mounted
的时候俩者中的lastRenderedReducer
不同
useState
是basicStateReducer
useReducer
是我们自己传入的Reducer
俩个Hook在更新state的时候逻辑和updateQueue的更新逻辑是差不多的,循环更新函数,计算出新的state
Q1: 为什么采用了环状单链表的结构?
在调度阶段取得第一个initialState
, 然后循环调用updateQueue
,
执行update
保存的action
更新函数, 将计算后的state
返回
这时候需要和第一个update
比较判断来确保所有的update
都执行结束
4. useEffect and useLayoutEffect
4.1 useEffect
当我们修改了数据需要update
, React
调用mutation
之前会对副作用函数进行flush
执行flushPassiveEffects
--> 然后执行flushPassiveEffectsImp
在flushPassiveEffectsImp
中会清除所有useEffect的销毁函数
var unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
// 清楚所有useEffect的销毁函数
for (var i = 0; i < unmountEffects.length; i += 2) {
var _effect = unmountEffects[i];
var fiber = unmountEffects[i + 1];
var destroy = _effect.destroy;
_effect.destroy = undefined;
{
fiber.effectTag &= ~PassiveUnmountPendingDev;
var alternate = fiber.alternate;
if (alternate !== null) {
alternate.effectTag &= ~PassiveUnmountPendingDev;
}
}
if (typeof destroy === 'function') {
{
setCurrentFiber(fiber);
{
invokeGuardedCallback(null, destroy, null);
}
if (hasCaughtError()) {
if (!(fiber !== null)) {
{
throw Error( "Should be working on an effect." );
}
}
var error = clearCaughtError();
captureCommitPhaseError(fiber, error);
}
resetCurrentFiber();
}
}
} // Second pass: Create new passive effects.
按照全部销毁
--> 全部执行
的顺序,确保ref的引用正确
注意: useEffect的更新是在layout阶段之后异步执行, 源码体现在这里
// 根据不同的fiber类型进行处理 --> commitFiberToLayout
function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
{
// useLayoutEffect --> 这里直接commit了,执行useLayout
commitHookEffectListMount(Layout | HasEffect, finishedWork);
}
// effect function --> 销毁和回调 --> 先进行了调度, 存在了俩个数组中, 并没有执行 --> 在结束后统一异步执行
schedulePassiveEffects(finishedWork);
return;
}
}
}
4.2 useEffect和useLayout的区别:
useEffect
在commitFiberToLayout
阶段(Layout阶段)开启调度, 把回调函数和销毁函数保存, 在Layout阶段结束后统一异步
调用useLayoutEffect
在Layout
阶段同步
执行, 直接commit
-->commitHookEffectListMount
React在执行
5. useRef
总述: useRef --> 通往 mutable 的通道
5.1 是什么
从6.不同Hook的dispatcher
可以看到useRef同样分为俩个函数mountRef
和updateRef
function mountRef<T>(initialValue: T): {|current: T|} {
// 获取当前hook
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) {
// do something
} else {
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
}
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}
可以看到useRef
本质上就是一个包含current
的对象
5.2 如何工作
机制描述 --> 通过topic
来确定mutation
操作
React
在commit
的阶段时进行mutation
操作, 根据不同的effectTag
来对DOM进行操作
effectTag
是一个单纯的二进制, 通过位操作符的原理来进行比对时的优化, 不同的操作类型是这样的
本质上是一种 topic and mutation
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b00000000000000000000000;
export const PerformedWork = /* */ 0b00000000000000000000001;
// 这里的Tag标识了要对DOM进行何种mutation操作
// You can change the rest (and add more).
export const Placement = /* */ 0b00000000000000000000010;
export const Update = /* */ 0b00000000000000000000100;
export const PlacementAndUpdate = /* */ Placement | Update;
export const Deletion = /* */ 0b00000000000000000001000;
export const ChildDeletion = /* */ 0b00000000000000000010000;
所以,对于HostComponent
、ClassComponent
如果包含ref
操作,那么也会赋值相应的effectTag
1. render阶段
1.1 标记topic
在render阶段通过markRef来对含有ref
的fiber
标记effectTag
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.flags |= Ref;
if (enableSuspenseLayoutEffectSemantics) {
workInProgress.flags |= RefStatic;
}
}
}
1.2 何时赋值?
在commitLayout阶段对ref进行赋值commitAttachRef
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 获取ref属性对应的Component实例
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// 赋值ref
if (typeof ref === 'function') {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
总结下组件对应fiber被赋值Ref effectTag需要满足的条件:
fiber
类型为HostComponent
、ClassComponent
、ScopeComponent
(这种情况我们不讨论)mount阶段存在ref属性
update阶段ref属性改变
2. commit阶段
2.1 移除之前的ref指向
function commitMutationEffects(root, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentFiber(nextEffect);
var effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 如果是Ref
if (effectTag & Ref) {
var current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
} // The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
}
上面可以看到, 如果是Ref, 执行了commitDetachRef
方法, 这个方法清除了ref
的current
指向
function commitDetachRef(current) {
var currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
currentRef(null);
} else {
// 移除之前的ref
currentRef.current = null;
}
}
}
2.2 根据topic
确定mutation
类型
function commitMutationEffects(root, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentFiber(nextEffect);
var effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 如果是Ref
if (effectTag & Ref) {
var current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
} // The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
// 在确定了effectTag进行的mutation操作之后, 需要进行处理, 处理逻辑在下面的switch中
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
}
在确定了effectTag进行的mutation操作之后, 需要进行处理, 处理逻辑在下面的switch中
// 这里是位操作符, React中的mutation对应的topic都是二进制(都是2的倍数), 如果你不太理解你可以把它当成 逻辑运算符 与
var primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement:
{
commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate:
{
// Placement
commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement; // Update
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
break;
}
case Hydrating:
{
nextEffect.effectTag &= ~Hydrating;
break;
}
case HydratingAndUpdate:
{
nextEffect.effectTag &= ~Hydrating; // Update
var _current2 = nextEffect.alternate;
commitWork(_current2, nextEffect);
break;
}
case Update:
{
var _current3 = nextEffect.alternate;
commitWork(_current3, nextEffect);
break;
}
case Deletion:
{
commitDeletion(root, nextEffect);
break;
}
}
2.3执行mutation
操作
在上文的switch
结构中确定了mutation
类型, 如果是Deletion
会执行commitDeletion
操作
在commitDeletion
——unmountHostComponents
——commitUnmount
——ClassComponent | HostComponent
类型case中调用的safelyDetachRef
方法负责执行类似commitDetachRef
的操作。
function safelyDetachRef(current: Fiber) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, refError);
}
} else {
ref.current = null;
}
}
}
6. 不同 Hook的 dispatcher
有这么三个不同的dispatcher来判断是什么时候执行的更新
// 已经在Hook中,如果有嵌套的Hook throw 一个 Error
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
// 挂载时的dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
// 更新时的dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
在执行的时候通过给ReactCurrentDispatcher
的current
属性赋值来确定处于什么状态