useState源码解析

1,854 阅读3分钟

最基础的useState用法

function Counter() {
    var [ count, setCount ] = useState(0)
    return (
      <div>
        <div>{count}</div>
        <button onClick={() => {setCount(count + 1)}}>点击</button>
      </div>
    )
}

基于useState的用法,自己实现一个useState

// 把state存储在外面
var _state
function useState(initialValue) {
    // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
    var state = _state || initialValue
    function setState(newState) {
        state = newState
        render()
    }
    return [state, setState]
}

这样就模拟实现了一个基础的useState

当我们的代码执行到了useState的时候,他到底做了什么呢?

源码路径 /packages/react-reconciler/src/ReactFiberHooks.js

function baseStateReducer<S>(state: S, action:BaicStateAction<S>):S {
  return typeof action === 'function' ? action(state) : action
}

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any),
  )
}

可见useState不过就是个语法糖,本质其实就是useReducer,那么useReducer具体做了什么呢?

useReducer

最开始两句代码是每个Hooks都会做的统一代码:

currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
  • currentlyRenderingFiber
  • createWorkInProgressHook

这里分两种情况:第一次渲染和更新,如果workInProgressHook.queue存在则为更新,否则是第一次渲染

第一次渲染

第一次渲染主要就是初始化操作

// There is no existing queue, so this is the initial render.
if (reducer === basicStateReducer) {
  // Special case for `useState`.
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
} else if (initialAction !== undefined && initialAction !== null) {
  initialState = reducer(initialState, initialAction);
}
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;

这里初始化initialState,并且记录在workInProgressHook.memoizedStateworkInProgressHook.baseState

然后创建queue对象

queue = workInProgeressHook.queue = {
    last: null,
    dispatch: null,
}

可以看到queue的结构非常简单,只有一个last指针和dispatchdispatch是用来记录更新state的方法的,接下去我们就要创建dispatch方法了

const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
  null,
  currentlyRenderingFiber,
  queue,
): any)

可以看到这个dispatch就是dispatchAction绑定了对应的Fiberqueue。最后return

return [workInProgressHook.memoizedState, dispatch];

return的结果对应了我们const [state, updateState] = useState('default')的用法

更新

分两种情况,是否是reRender,所谓reRender就是说在当前更新周期中又产生了新的更新,就继续执行这些更新直到当前渲染周期中没有更新为止

他们基本的操作是一致的,就是根据renderupdate.action来创建新的state,并赋值给Hook.memoizedState以及Hook.baseState.

注意这里,对于非reRender的况,我们会对每个更新判断其优先级,如果不是当前整体更新优先级内得到更新会跳过,第一个跳过的Update会变成新的baseUpadate他记录了在之后所有的Update,即便是优先级比他高的,因为在他被执行的时候,需要保证后续的更新要在他更新之后的基础上再次执行,因此结果可能会不一样。

dispatchAction

首先看这个判断:

if (
  fiber === currentlyRenderingFiber ||
  (alternate !== null && alternate === currentlyRenderingFiber)
)

这其实就是判断这个更新是否是在渲染过程中产生的currentlyRenderingFiber只有在FunctionalComponent更新的过程中才会被设置,在离开更新的时候设置为null,所以只要存在并与产生更新的Fiber相等,说明这个更新时在当前渲染中产生的,则这是一次reRender

所有更新过程中只产生的更新记录在renderPhaseUpdates这个Map上,以每个Hookqueuekey

对于不是更新过程中生成的更新,则直接在queue上执行操作就行了,注意在最后会发起一次scheduleWork的调度。

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
  invariant(
    numberOfReRenders < RE_RENDER_LIMIT,
    'Too many re-renders. React limits the number of renders to prevent ' +
      'an infinite loop.',
  );

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we will restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      expirationTime: renderExpirationTime,
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);
    const update: Update<A> = {
      expirationTime,
      action,
      next: null,
    };
    flushPassiveEffects();
    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;
    scheduleWork(fiber, expirationTime);
  }
}