React源码解析之hook原理

·  阅读 1453

源码结构

在渲染函数组件的时候会调用renderWithHooks方法:

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  // 1
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  ...

 // 2
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  ...
  
 // 3
 // 根据当前workInprogress树上的Fiber在current树上是否有对应的Fiber节点
 // 来判断是创建hook,还是更新hook
 ReactCurrentDispatcher.current =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;
  
  // 4
  // 调用函数组件
  let children = Component(props, secondArg);

  ...

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  // 5
  // 重置hook链表指针
  currentHook = null;
  workInProgressHook = null;

  ...

  return children;
}

可以看到这个函数中:

  1. nextRenderLanes(当前需要渲染的优先级集合)赋值给了renderLanes,将workInProgress(函数组件对应的Fiber对象)赋值给了currentlyRenderingFiberrenderLanescurrentlyRenderingFiber是全局变量,在后面调用hook函数时使用。
  2. workInProgress的三个属性重置了:memoizedState,updateQueue,NoLanes
  3. 根据当前节点在current树上是否有相对应的节点判断是创建还是更新,以此来给ReactCurrentDispatcher.current赋值:
const HooksDispatcherOnMount: Dispatcher = {
  ...
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

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

创建阶段赋值为HooksDispatcherOnMount,更新阶段赋值为HooksDispatcherOnUpdate,可以看到这两个对象中保存的都是hook函数,key都是一样的,只是在不同阶段所调用的hook函数是不一样的。

  1. 调用函数组件,此时会调用hook函数,返回React element对象
  2. 重置hook链表指针:currentHook,workInProgressHook,关于这两个指针十分重要,下面会详细分析。

组件挂载

下面我们则详细分析第4部分的代码:调用函数组件。

例如,我们现在有这样一个函数组件:

export default function Demo () {
  const [num, setNum] = useState('value1');
  const [num1, setNum1] = useState('value2');

  const onTapBtn = () => {
    setNum(num + 1);
  }

  return (
    <button onClick={onTapBtn} >Demo:{num}</button>
  );
}

调用函数组件后,首先会执行useState('value1'),那么我们来看下useState的源码:

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  ...
  return ((dispatcher: any): Dispatcher);
}

可以看到useState中首先会获取dispatcher,也就是我们上面所提到的第3部分的代码,将保存了hook函数的对象赋值给了ReactCurrentDispatcher.current,在useState函数中调用了resolveDispatcher方法获取了ReactCurrentDispatcher.current,随后调用了ReactCurrentDispatcher.current中的useState方法并传入了初始化的state

dispatcher.useState的函数内部,又调用了mountState函数:

function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  ...
}

mountState中调用了mountWorkInProgressHook函数,我们来看一下mountWorkInProgressHook函数做了什么:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

首先创建了一个hook对象,然后判断workInProgressHook是否为null,因为每次调用完函数组件,都会把workInProgressHook置为null,也就是我们刚开始分析的源码结构的第5部分。此时又是第一次调用hook函数,workInProgressHook肯定为null,所以会将新建的hook对象赋值给workInProgressHook,然后将workInProgressHook挂载到currentlyRenderingFiber(源码结构的第1部分:函数组件对应的Fiber对象)memoizedState属性上。

最后返回workInProgressHook,记住此时workInProgressHook指向的是useState('value1')所创建的hook对象。

mountWorkInProgressHook函数执行完成后,继续向下执行:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null, // 需要更新的state
    interleaved: null,
    lanes: NoLanes, // 更新的lane优先级
    dispatch: null, // 发起更新state的函数
    lastRenderedReducer: basicStateReducer, // 上一次计算state的函数
    lastRenderedState: (initialState: any), // 上一次更新的state
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

先判断了initialState,也就是我们使用useState时所传入的参数,如果是函数类型的,则会调用,获取到返回值。然后将值赋值给了hook对象上的baseStatememoizedState属性。

然后创建了一个队列对象,接着将dispatchAction函数通过bind方法传入函数组件对应的fiber对象和队列对象,挂载到队列对象的dispatch属性上。

最后返回一个集合,第一个元素是state的值,第二个元素是更新state函数(dispatchAction)。

此时useState('value1')调用完毕。

接着调用useState('value2'),流程也是与useState('value1')是一样的,不同点在于workInProgressHook的指向,我们来看一下:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

首先也是会为useState('value2')创建一个hook对象,继续执行,此时workInProgressHook指向的是useState('value1')创建的hook对象,则会将useState('value2')创建的对象挂载到useState('value1')创建的hook对象下的next属性上,形成一个hook对象链表

在挂载阶段hook创建到此也完成了。

组件更新

还是以之前的例子来讲,现在我们点击Button,调用了setNum去更新state,上面讲过更新state的函数其实就是dispatchAction,我们来看一下源码:

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
 ...

  const eventTime = requestEventTime(); // 获取更新触发时的时间
  const lane = requestUpdateLane(fiber); // 获取更新优先级

  // 创建更新对象
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 获取workInProgress树上对应的fiber
  const alternate = fiber.alternate;
  
  // currentlyRenderingFiber:是在render阶段调用函数组件前将函数组件对应的workInprogress的fiber赋值给了它,在调用函数组件后重置为null
  // 如果fiber === currentlyRenderingFiber ,则说明该函数组件正在执行更新任务,将需要更新的数据添加到更新链表中即可,不用产生更新任务进行调度
  if (
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    ...
  } else {
    if (isInterleavedUpdate(fiber, lane)) {
     ...
    } else {
      // 将创建对象连接形成一个环形链表
      const pending = queue.pending;
      if (pending === null) {
        // This is the first update. Create a circular list.
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      queue.pending = update;
    }

    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        ...
        try {
          const currentState: S = (queue.lastRenderedState: any);
          // 获取最新的state
          const eagerState = lastRenderedReducer(currentState, action);
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          // 判断上一次更新的值与本次更新的值是否相同,不同则执行更新,相同直接返回不会执行更新
          if (is(eagerState, currentState)) {
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
   

 ...
}

可以看到dispatchAction中主要是创建了一个更新对象,将更新对象挂载到queuepending属性上,那有的同学可能有疑惑了,fiberqueue哪来的?还记的我们分析函数组件挂载的时候,最后将dispatchAction函数通过bind方法传入函数组件对应的fiber对象和队列对象:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  ...
  const queue = (hook.queue = {
    pending: null, // 需要更新的state
    interleaved: null,
    lanes: NoLanes, // 更新的lane优先级
    dispatch: null, // 发起更新state的函数
    lastRenderedReducer: basicStateReducer, // 上一次计算state的函数
    lastRenderedState: (initialState: any), // 上一次更新的state
  });
  
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

所以我们在更新state时,能够获取到函数组件对应的fiber对象和queue对象。

将更新对象挂载到queuepending属性上前,会将更新对象形成一个环形链表:

update1  update1.next -> update2  update2.next -> update1

之后调用lastRenderedReducer函数获取最新的state值:

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

lastRenderedReducer实际上就是basicStateReducer函数,在它内部对action也就是我们调用更新函数传入的值,判断它是不是函数,是函数则会传入上一次的值并调用该函数获取到最新的值,如果不是函数,则直接返回新传入的值。

然后将最新的state,赋值给更新对象的eagerState属性上。

接着对eagerState(本次更新的state)currentState(上一次更新的state)使用Object.is进行对比,如果相等,则会return不会进行更新,反之,则会进行更新。

当进行更新时,则又会调用renderWidthHooks函数。

ReactCurrentDispatcher.current赋值为HooksDispatcherOnUpdate

然后调用函数组件,这是函数组件内部的hook函数又会执行一遍,我们来看看此时又是如何执行的。

首先还是会调用useState('value1'),和挂载一样,但是不同的是,不再调用mountState函数,而是调用updateState函数:

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

updateState内部调用了updateReducer函数:

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
 ...
}

updateReducer内部首先调用了updateWorkInProgressHook去获取对应的hook对象:

function updateWorkInProgressHook(): Hook {

  // currentlyRenderingFiber是workInprogress树上的Fiber
  // currentlyRenderingFiber.alternate则是对应在current树上的Fiber
  // nextCurrentHook的值则是current树上的Fiber的hook链表
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // nextWorkInProgressHook的值则是workInProgress树上的Fiber的hook链表
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }


  // 将currentHook作为current树上的Fiber的hook链表的指针
  // 将currentHook作为workInProgressHook作为对应workInProgress树上的Fiber的hook链表的指针
  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

我们知道每次调用完函数组件后都会将currentHookworkInProgressHook都重置为null。

  1. 会将nextCurrentHook赋值为current树上函数组件对应fiber上的memoizedState(hook对象链接)属性,也可以理解为,将nextCurrentHook指向为current树上函数组件的hook链表的第一个hook对象。
  2. nextWorkInProgressHook赋值为workInProgress树上函数组件对应fiber上的memoizedState(hook对象链接)属性,但是在每次调用函数组件前,都会将函数组件在workInProgress树上对应fiber的memoizedState置为null,所以nextWorkInProgressHook第一次总是为null。
  3. nextWorkInProgressHook为null,则会根据currentHook创建一个新的hook对象,然后将新的对象赋值给workInProgressHook,并挂载在函数组件在workInProgress树上对应fiber的memoizedState属性上。
  4. nextCurrentHook赋值给currentHook
  5. 返回workInProgressHook

通过这个函数我们发现有两个指针:currentHookworkInProgressHook

  1. currentHook:指向的是current树上函数组件对应的hook链表上的hook对象
  2. workInProgressHook:指向的是workInProgress树上函数组件对应的hook链表上的hook对象

并且它们两个指针指向的hook对象是一一对应的:此时currentHook指向的是current树上函数组件对应的hook链表上的第一个对象useState('value1'),workInProgressHook指向的是根据currentHook指向的hook对象创建出来的新的hook对象,也是useState('value1')

updateWorkInProgressHook调用完成后,通过currentHook可以拿到上一次的state值,通过遍历更新对象形成的环形链表计算出最终的state。

最后也会返回一个集合,第一个元素是最新state值,第二个元素是更新函数。

为什么需要将hook函数放在函数组件的顶层

举个例子:

const [num, setNum] = useState('value1'); // useState1
const [num2, setNum2] = useState('value2'); // useState2

现在创建完成后的hook链表是这样:

useState1 -> useState2

我们现在将useState1放在if里面,让它在更新阶段不执行:

let isMount = false;

// 只在创建阶段执行,更新阶段不执行
if(!isMount) {
    const [num, setNum] = useState('value1'); // useState1
    isMount = true;
}
const [num2, setNum2] = useState('value2'); // useState2

然后我们更新useState2的值:

setNum2('update');

在更新阶段,会根据current上的hook链表创建workInprogress的hook链表,虽然useState1不会执行,但是在执行useState2的时候,会去获取current上的hook链表的第一个对象useState1-hook,以此创建useState2在workInprogress的hook链表上的hook对象,所以此时useState2的hook对象是useState1-hook,但是更新操作是在useState2上发起的,useState1并不会更新,所以会获取useState1上一次更新的值:value1,然后返回给useState2,最终useState2得到更新的值则为:value1

hook链表:

curent树上的hook链表:         useState1 hook -> useState2 hook
workInprogress树上的hook链表: useState1 hook

之所以结果会是这样,是因为在创建时,会根据hook函数书写的顺序创建hook链表,在更新时,使用指针从hook链表中依次取出对应的hook对象进行执行,将hook函数放入了if中相当于将后面的hook函数的执行提前,与hook链表中所对应的hook对象的位置就错乱了,则会造成无法预期的结果。

总结

  1. 在挂载函数组件时,会根据hook函数书写的位置创建一个hooks链表,挂载在函数组件对应的fiber的memoizedState属性上。
  2. 在更新函数时,使用currentHook指针依次取出函数组件对应在current树上fiber的hooks链表中的hook对象,然后根据currentHook来创建或复用,以此创建workInprogress树上函数组件的hooks链表,并且会使用workInprogressHook指针取出hook对象。workInprogressHookcurrentHook是一一对应的。
  3. 使用两个指针:currentHookworkInprogressHook的目的是,使用currentHook可以获取上一次hook函数更新的状态,协助本次更新,比如:
    • 使用了useEffect,上一次传入的依赖数组为:[a=1],本次更新依赖数组为[a=2], 两次对比不同则需要执行回调函数。
    • 使用了useState,本次调用更新函数传入一个函数,我们知道回调函数会将上一次的值作为参数传入,那么这个参数从哪里来的?是的,从curent树上的hook链表中hook函数对应的hook对象上取出来的。
分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改