React Hooks的实现和数据结构是怎样的呢?

255 阅读10分钟

1. 极简Hooks实现

function App() {
  const [num, updateNum] = useState(0);

  return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}
  1. 通过一些途径产生更新,更新会造成组件render--updateNum;
  2. 组件render时useState返回的num为更新后的结果;

其中步骤1的更新可以分为mount和update:

  1. 调用ReactDOM.render会产生mount的更新,更新内容为useState的initialValue(即0)。
  2. 点击p标签触发updateNum会产生一次update的更新,更新内容为num => num + 1。

1.1 更新是什么

通过一些途径产生更新,更新会造成组件render

const update = {
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

1.2 update的数据结构

加入有多个update,如何组合起来

// 之前
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;

// 之后
return <p onClick={() => {
  updateNum(num => num + 1);
  updateNum(num => num + 1);
  updateNum(num => num + 1);
}}>{num}</p>;

通过环形单向链表 调用updateNum实际上是dispatchAction.bind(null, hook.queue)

function dispatchAction(queue, action) {
  // 创建update
  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;

  // 模拟React开始调度更新
  schedule();
}

demo:

当产生第一个update(我们叫他u0),此时queue.pending === null。

update.next = update;即u0.next = u0,他会和自己首尾相连形成单向环状链表。

然后queue.pending = update;即queue.pending = u0

queue.pending = u0 ----->
                ^       |
                |       |
                ---------

当产生第二个update(我们叫他u1),update.next = queue.pending.next;,此时queue.pending.next === u0, 即u1.next = u0。

queue.pending.next = update;,即u0.next = u1。

然后queue.pending = update;即queue.pending = u1

queue.pending = u1 ---> u0   
                ^       |
                |       |
                ---------

queue.pending始终指向最后一个插入的update

1.3 状态如何保存

更新产生的update对象会保存在queue中。不同于ClassComponent的实例可以存储数据,对于FunctionComponent,queue存储对应的fiber中。

// App组件对应的fiber对象
const fiber = {
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null,
  // 指向App函数
  stateNode: App
};

1.4 Hooks数据结构

hook = {
  // 保存update的queue,即上文介绍的queue
  queue: {
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

Q:update与hook的关系

A:每个useState对应一个hook对象。 调用const[num,updateNum] = useState(0);时updateNum(即上文介绍的dispatchAction)产生的update保存在useState对应的hook.queue中。

1.5 模拟react调度更新流程

1.实现通过操作产生更新,更新造成组件render

function dispatchAction(queue, action) {
  // ...创建update
  
  // ...环状单向链表操作

  // 模拟React开始调度更新
  schedule();
}

// 模拟调度
// 首次render时是mount
isMount = true;

function schedule() {
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
}

// 每当遇到下一个useState,我们移动workInProgressHook的指针
workInProgressHook = workInProgressHook.next;
// 保证了每次组件render时useState的调用顺序及数量保持一致
// 可以通过workInProgressHook找到当前useState对应的hook对象。

1.6 计算state

2.组件render时,useState返回的值为更新后的结果,即一个完整的useState

function useState(initialState) {
  // 当前useState使用的hook会被赋值该该变量
  let hook;

  if (isMount) {
    hook = {
      queue: {
        pending: null
      },
      memoizedState: initialState,
      next: null
    }

    // 将hook插入fiber.memoizedState链表末尾
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    // 移动workInProgressHook指针
    workInProgressHook = hook;
  } else {
    // update时找到对应hook
    hook = workInProgressHook;
    // 移动workInProgressHook指针
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    // 获取update环状单向链表中第一个update
    let firstUpdate = hook.queue.pending.next;
  
    do {
      // 执行update action
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
  
      // 最后一个update执行完后跳出循环
    } while (firstUpdate !== hook.queue.pending.next)
  
    // 清空queue.pending
    hook.queue.pending = null;
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}

2. Hooks数据结构

2.1 dispatcher

上文中,useState使用isMount区分mount和update

在真实的Hooks中,组件mount时的hook与update时的hook来源于不同的对象,这类对象在源码中被称为dispatcher

// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  // ...省略
};

// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  // ...省略
};

mount时调用的hook和update时调用的hook其实是两个不同的函数。

在FunctionComponent render前,会根据FunctionComponent对应fiber的以下条件区分mount与update

current === null || current.memoizedState === null

并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatcher的current属性

ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;  

2.2 dispatch异常场景

useEffect(() => {
  useState(0);
})

实际上,ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常

export const ContextOnlyDispatcher: Dispatcher = {
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  // ...省略

2.3 Hook数据结构

const hook: Hook = {
  memoizedState: null,

  baseState: null,
  baseQueue: null,
  queue: null,

  next: null,
};

除了memoizedState,其余与updateQueue一致

  • useState:对于const [state, updateState] = useState(initialState),memoizedState保存state的值
  • useReducer:对于const [state, dispatch] = useReducer(reducer, {});,memoizedState保存state的值
  • useEffect:memoizedState保存包含useEffect回调函数、依赖项等的链表数据结构effect。effect链表同时会保存在fiber.updateQueue中
  • useRef:对于useRef(1),memoizedState保存{current: 1}
  • useMemo:对于useMemo(callback, [depA]),memoizedState保存[callback(), depA]
  • useCallback:对于useCallback(callback, [depA]),memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果

有些hook是没有memoizedState的,比如:

  • useContext

2. useState和useReducer

useState和useReducer是Redux作者加入React后的一个核心贡献:将Redux的思想带入到React里

本质来说,useState只是预置了reducer的useReducer

2.1 概览

function App() {
  const [state, dispatch] = useReducer(reducer, {a: 1});

  const [num, updateNum] = useState(0);
  
  return (
    <div>
      <button onClick={() => dispatch({type: 'a'})}>{state.a}</button>  
      <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
    </div>
  )
}
  • 声明阶段即App调用时,会依次执行useReducer与useState方法
  • 调用阶段即点击按钮后,dispatch或updateNum被调用时

2.2 声明阶段

当FunctionComponent进入render阶段的beginWork时,会调用renderWithHooks方法

该方法内部会执行FunctionComponent对应函数(即fiber.type)

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

2.2.1 mount时

mount时,useReducer会调用mountReducer,useState会调用mountState

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 创建并返回当前的hook
  const hook = mountWorkInProgressHook();

  // ...赋值初始state

  // 创建queue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

  // ...创建dispatch
  return [hook.memoizedState, dispatch];
}

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 创建并返回当前的hook
  const hook = mountWorkInProgressHook();

  // ...赋值初始state

  // 创建queue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });

  // ...创建dispatch
  return [hook.memoizedState, dispatch];
}

其中,mountWorkInProgressHook对应创建并返回对应的Hook,以上两个hooks的区别:

queue参数的lastRenderedReducer字段

const queue = (hook.queue = {
  pending: null,
  // 保存dispatchAction.bind()的值
  dispatch: null,
  // 上一次render时使用的reducer
  lastRenderedReducer: reducer,
  // 上一次render时的state
  lastRenderedState: (initialState: any),
});

useReducer的lastRenderedReducer为传入的reducer参数。useState的lastRenderedReducer为basicStateReducer,basicStateReducer如下:

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

2.2.2 update时

在update时,useReducer和useState调用的是同一个函数 updateReducer

// 找到对应的hook,根据update计算该hook的新state并返回
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取当前hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  
  queue.lastRenderedReducer = reducer;

  // ...同update与updateQueue类似的更新逻辑

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

2.3 调用阶段

调用阶段会执行dispatchAction,此时该FunctionComponent对应的fiber以及hook.queue已经通过调用bind方法预先作为参数传入

// 创建update,将update加入queue.pending中,并开启调度。

function dispatchAction(fiber, queue, action) {

  // ...创建update
  var update = {
    eventTime: eventTime,
    lane: lane,
    suspenseConfig: suspenseConfig,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  }; 

  // ...将update加入queue.pending
  
  var alternate = fiber.alternate;

  if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
    // render阶段触发的更新
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      // ...fiber的updateQueue为空,优化路径
    }

    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

4. useEffect

参考commit阶段时useEffect工作流

在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList。

4.1 flushPassiveEffectsImpl

flushPassiveEffects内部会设置优先级,并执行flushPassiveEffectsImpl flushPassiveEffectsImpl主要做三件事:

  • 调用该useEffect在上一次render时的销毁函数;
  • 调用该useEffect在本次render时的回调函数;
  • 如果存在同步任务,不需要等待下次事件循环的宏任务,提前执行;

这里主要关注前两件事:

4.1.1 销毁函数的执行

useEffect的执行需要保证所有组件useEffect的销毁函数必须都执行完后才能执行任意一个组件的useEffect的回调函数。

这是因为多个组件间可能共用同一个ref。

如果不是按照“全部销毁”再“全部执行”的顺序,那么在某个组件useEffect的销毁函数中修改的ref.current可能影响另一个组件useEffect的回调函数中的同一个ref的current属性。

在useLayoutEffect中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。

所以,会遍历并执行所有useEffect的销毁函数

// pendingPassiveHookEffectsUnmount中保存了所有需要执行销毁的useEffect
const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (let i = 0; i < unmountEffects.length; i += 2) {
    const effect = ((unmountEffects[i]: any): HookEffect);
    const fiber = ((unmountEffects[i + 1]: any): Fiber);
    const destroy = effect.destroy;
    effect.destroy = undefined;

    if (typeof destroy === 'function') {
      // 销毁函数存在则执行
      try {
        destroy();
      } catch (error) {
        captureCommitPhaseError(fiber, error);
      }
    }
  }

其中pendingPassiveHookEffectsUnmount数组的索引i保存需要销毁的effect,i+1保存该effect对应的fiber

4.1.2 回调函数的执行

遍历数组,执行对应effect的回调函数

// pendingPassiveHookEffectsMount中保存了所有需要执行回调的useEffect
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
  const effect = ((mountEffects[i]: any): HookEffect);
  const fiber = ((mountEffects[i + 1]: any): Fiber);
  
  try {
    const create = effect.create;
   effect.destroy = create();
  } catch (error) {
    captureCommitPhaseError(fiber, error);
  }
}

5. useRef

ref是reference(引用)的缩写。在React中,我们习惯用ref保存DOM。

对于useRef(1),memoizedState保存{current: 1}

5.1 useRef的两个状态

在mount和update时对应了两个dispatcher

function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = mountWorkInProgressHook();
  // 创建ref
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = updateWorkInProgressHook();
  // 返回保存的数据
  return hook.memoizedState;
}

useRef仅仅是返回一个包含current属性的对象,可以看React.createRef,证明了ref在mount时只有current属性

export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}

5.2 Ref工作流程

在React中,HostComponent、ClassComponent、ForwardRef可以赋值ref属性。

// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />

其中,ForwardRef只是将ref作为第二个参数传递下去, 不会进入ref的工作流程。

因为HostComponent在commit阶段的mutation阶段执行DOM操作。

所以,对应ref的更新也是发生在mutation阶段。

同时,mutation阶段执行DOM操作的依据为effectTag。

所以,对于HostComponent、ClassComponent如果包含ref操作,那么也会赋值相应的effectTag。

// ...
export const Placement = /*                    */ 0b0000000000000010;
export const Update = /*                       */ 0b0000000000000100;
export const Deletion = /*                     */ 0b0000000000001000;
export const Ref = /*                          */ 0b0000000010000000;
// ...

所以,ref的工作流程可以分为两部分:

  1. render阶段为含有ref属性的fiber添加Ref effectTag
  2. commit阶段为包含Ref effectTag的fiber执行对应操作

5.3 render阶段

在render阶段的beginWork与completeWork中有个同名方法markRef用于为含有ref属性的fiber增加RefeffectTag

// beginWork的markRef
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.effectTag |= Ref;
  }
}
// completeWork的markRef
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

在beginWork中,如下两处调用了markRef:

注意 ClassComponent 即使 shouldComponentUpdate 为false该组件也会调用markRef

在completeWork中,如下两处调用了markRef:


总结下组件对应fiber被赋值Ref effectTag需要满足的条件:

  • fiber类型为HostComponent、ClassComponent
  • 对于mount,workInProgress.ref !== null,即存在ref属性
  • 对于update,current.ref !== workInProgress.ref,即ref属性改变

5.4 commit阶段

在commit阶段的mutation阶段中,对于ref属性改变的情况,需要先移除之前的ref。

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;
    // ...

    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        // 移除之前的ref
        commitDetachRef(current);
      }
    }
    // ...
  }
  // ...
  
  function commitDetachRef(current: Fiber) {
    const currentRef = current.ref;
    if (currentRef !== null) {
      if (typeof currentRef === 'function') {
        // function类型ref,调用他,传参为null
        currentRef(null);
      } else {
        // 对象类型ref,current赋值为null
        currentRef.current = null;
      }
    }
  }

接下来进入ref的赋值阶段,commitLayoutEffect会执行commitAttachRef(赋值ref)

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;
    }
  }
}

6. useMemo与useCallback

6.1 mount

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 计算value
  const nextValue = nextCreate();
  // 将value与deps保存在hook.memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将value与deps保存在hook.memoizedState
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

6.2 update

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 返回当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 未变化
        return prevState[0];
      }
    }
  }
  // 变化,重新计算value
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 返回当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 未变化
        return prevState[0];
      }
    }
  }

  // 变化,将新的callback作为value
  hook.memoizedState = [callback, nextDeps];
  return callback;

对于update,这两个hook的唯一区别也是回调函数本身还是回调函数的执行结果作为value。