美文网首页
react hooks 源码分析 --- useState

react hooks 源码分析 --- useState

作者: crabApple1 | 来源:发表于2020-03-17 15:49 被阅读0次

1. react hooks简介

react hooks 是react 16.8.0 的新增特性,它可以让你在不编写class的情况下使用state以及其他的一些react特性。

  在过去的react版本中,如果我们想要使用状态管理或者想要在render之后去做一些事情,我们必须使用class组件才能办到。但是现在hooks的出现,使得函数组件也同样可以做到。
  hooks实际上是一些以use开头来明名的函数,它就像钩子一样,把函数组件不具备的特性钩进来,使得函数组件也同样可以使用这些特性。
  话不多说,下面我们就开始看一下第一个hook的api。

2.useState 使用规则

function User(props) {
  let [count, setCount] = useState(0); // 这里的count,setCount类似于class组件里的state,setState,我们要改变count这个状态的值,只需要调用setCount这个函数就可以了,它接受一个参数,就是你要更改的值。
  let [name, setName] = useState('Mary'); // 你可以在组件内部多次调用useState来创建多个状态变量

  return <div>
    <div>当前计数: count</div>
    <button onClick={() => { setCount(count+1); }}>count+1</button>
  </div>
}

  useState使得我们可以在函数组件里使用状态,它接受一个参数,就是当前状态的初始值。返回两个变量,第一个变量就是我们的状态变量,第二个就是改变这个状态的函数,类似于class组件里的state和setState。
注意,这里useState返回的是一个数组,所以变量的名字是我们自己任意取的。

useState class state
可以在组件内部多次使用,创建多个状态变量 只有一个state对象
set函数,传进来的参数完全覆盖该状态值 合并state

3.源码分析

  当前react版本为16.9.0。打开源码,我们首先从react.js文件入手,找到useState的源码。

import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState, // 在这里
  useResponder,
} from './ReactHooks'; // 所以我们要找的源码在这个文件里面

  我们在进到ReactHooks.js文件里看一下

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

  从上述代码可以看出,我们的useState函数是挂到dispatcher对象上面的,那dispatcher对象到底是什么呢,我们再进到resolveDispatcher函数里看一下。
  dispatcher对象被赋值为ReactCurrentDispatcher.current,我们在进一步看一下ReactCurrentDispatcher是什么。

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';

const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher), // current是Dispatcher类型的
};

export default ReactCurrentDispatcher;

  dispatcher我们看到ReactCurrentDispatcher.current被初始化为null,似乎到这里我们什么也没找到。
  但我们找到了一条线索,那就是useState其实是挂载ReactCurrentDispatcher.current对象上面的,所以我们只要找到它被赋值的地方就可以了。
  但这部分的内容,实际上属于fiber调度的范畴,所以我们就简单提一下,不做过多阐述,实际上真正赋值的地方是在render阶段.

reactFiberHooks.js的renderWithHooks函数中。 文件路径
ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount  // 组件挂载阶段
        : HooksDispatcherOnUpdate; // 组件更新阶段

  上面代码,当nextCurrentHook为空的时候,被赋值为HooksDispatcherOnMount,不为空的时候被赋值为HooksDispatcherOnUpdate,意思就是说,当组件第一次render,也就是挂载的时候,我们的hook api是在HooksDispatcherOnMount这个对象上的,非首次渲染是在HooksDispatcherOnUpdate对象上的。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  ...
  useState: mountState,
  ...
};
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  ...
  useState: updateState,
  ...
};

  所以我们需要分两个分支来看源码。

3.1 mountState

  首先我们需要知道,在组件里,多次调用useState,或者其他hook,那react怎么知道我们当前是哪一个hook呢。其实在react内部,所有的hook api第一次被调用的时候都会先创建一个hook对象,来保存相应的hook信息。然后,这个hook对象,会被加到一个链表上,这样我们每次渲染的时候,只要从这个链表上面依次的去取hook对象,就知道了当前是哪一个hook了。
下面我们就看一下这个hook对象的具体格式。

const hook: Hook = {
    memoizedState: null, // 缓存当前state的值
    baseState: null, // 初始化initState,以及每次dispatch之后的newState
    queue: null, // update quene
    baseUpdate: null, //基于哪一个hook进行更新,循环update quene的起点
    next: null, // 指向下一个hook
};

对于useState来说,memoizedState属性上保存的就是当前hook对应状态变量当前的值,也就是我们获取到的状态变量的值。那这个quene上面保存的是什么呢,我们稍后在解释。
  言归正传,我们开始将mountState函数。组件首次渲染的源码,就是mountState这个函数。也就是说首次渲染时useState的源码就是mountState。
那么我们来看看它的实现。

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook(); // 第一步:创建新的hook对象并加到链上,返回workInProgressHook
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState; // 第二步:获取初始值并初始化hook对象
  const queue = (hook.queue = { // 第三步:创建更新队列(update quene),并初始化
    last: null, // 最后一次的update对象
    dispatch: null, // 更新函数
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any), // 前面最后一次更新的state值,更新的值有可能是函数,函数计算需要用到前一个state的值
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind( // 第四步
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber), // 绑定当前fiber和quene
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

第一步,创建hook对象,并将该hook对象加到hook链的末尾。

我们来看一下代码。

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {  // 创建hook对象
    memoizedState: null,
 
    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) { // 如果是组件内部的第一个hook
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else { // 不是第一个hook对象,就直接把新创建的hook对象加到hook链表的末尾
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

第二步:初始化hook对象的状态值,也就是我们传进来的initState的值。
第三步:创建更新队列,这个队列是更新状态值的时候用的。
第四步:绑定dispatchAction函数。我们可以看到最后一行返回的就是这个函数。也就是说这个函数,其实就是我们改变状态用的函数,就相当于是setState函数。这里它先做了一个绑定当前quene和fiber对象的动作,就是为了在调用setState的时候,知道该更改的是那一个状态的值。
  至此,我们就看完了mountState函数。
下面这张图,是我自己画的简易版useState源码的流程图。


useState源码流程图

  那么到这里,我们已经走完了组件首次渲染调用useState时的逻辑。现在,我们已经拿到了我们的状态变量state,那么我们就可以改变这个状态了,也就是调用set函数,这里为了说明方便,我们就直接说setState函数了(实际上你可以随意取名字)。

3.2 dispatchAction

前面已经说过dispatchAction就是我们更改状态值时调用的函数。

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A, // 2
) {
...
if(){
  rerender逻辑
}else{
  const update: Update<S, A> = { // 第一步
      expirationTime,
      suspenseConfig,
      action, // 2
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // 第二步:将update加到quene上,更新quene的last为当前update,注意quene是一个环形链表
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update; // 环形链
    } else {
      const first = last.next; // 这个last.next是指向第一个update,因为quene是一个环形链表
      if (first !== null) {
        // Still circular.
        update.next = first; // 使quene变成环形链表
      }
      last.next = update; // 将update加到quene上。
    }
    queue.last = update; // 更新quene的last为当前update
}
...

省略无关代码,我们可以看到实际上,dispatchAction这个函数主要做了两件事情。
第一件就是创建了一个update对象,这个对象上面保存了本次更新的相关信息,包括新的状态值action。
第二件,就是将所有的update对象串成了一个环形链表,保存在我们hook对象的queue属性上面。所以我们就知道了queue这个属性的意义,它是保存所有更新行为的地方。
在这里我们可以看到,我们要更改的状态值并没有真的改变,只是被缓存起来了。那么真正改变状态值的地方在哪呢?答案就是在下一次render时,函数组件里的useState又一次被调用了,这个时候才是真的更新state的时机。

3.3 updateState

这里就是我们组件更新时,调用useState时真正走的逻辑了。

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
  reducer: (S, A) => S, // 对于useState来说就是basicStateReducer
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook(); // 获取当前正在工作的hook,Q1
  const queue = hook.queue; // 更新队列
 // The last update in the entire queue
  const last = queue.last; // 最后一次的update对象
  // The last update that is part of the base state.
  const baseUpdate = hook.baseUpdate; // 上一轮更新的最后一次更新对象
  const baseState = hook.baseState; // 上一次的action,现在是初始值

  // Find the first unprocessed update.
  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      // For the first update, the queue is a circular linked list where
      // `queue.last.next = queue.first`. Once the first update commits, and
      // the `baseUpdate` is no longer empty, we can unravel the list.
      last.next = null; // 因为quene是一个环形链表,所以这里要置空
    } 
    first = baseUpdate.next; // 第一次是用的last.next作为第一个需要更新的update,第二次之后就是基于上一次的baseUpdate来开始了(baseUpdate就是上一次的最后一个更新)
  } else {
    first = last !== null ? last.next : null; // last.next是第一个update
  }
  if (first !== null) { // 没有更新,则不需要执行,直接返回
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do { // 循环链表,执行每一次更新
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        ...
      } else { // 正常逻辑
        // This update does have sufficient priority.
        // Process this update.
        if (update.eagerReducer === reducer) { // 如果是useState,他的reducer就是basicStateReducer
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);

    if (!didSkip) { // 不跳过,就更新baseUpdate和baseState
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }
    ...
    hook.memoizedState = newState; // 更新hook对象
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;

    queue.lastRenderedState = newState;
  }

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

updateState做的事情,实际上就是拿到更新队列,循环队列,并根据每一个update对象对当前hook进行状态更新。最后返回最终的结果。

这是我在学习useState源码时的自问自答

1、怎么循环hook对象的,在哪里操作的
    (1)从当前fiber对象的memoizedState属性保存着当前组件的第一个hook对象
    (2)在每次执行updateState的时候,首先需要获取当前工作中的hook,就是在这里循环的hook
    (3)hook链是一个环形链吗?不是,是单向链表
        在mount阶段,workInProgressHook.next = null,update阶段最后一个hook的next依然是null
        是不是说当前fiber对象的memoizedState一直都是第一个hook (462行)
2.Q:更新函数绑定当前hook的地方在哪
  A:在dispatchAcion.bind的地方,绑定了fiber和quene
3.Q:更新state时,怎么定位到第一个需要执行的update的
  A:基于baseUpdate来开始更新
4.Q:renderWithHooks为什么第一次没有执行 FunctionComponent这个分支?
  A:renderWithHooks是在组件更新阶段执行的FunctionComponent
5.Q:useState可以放对象吗?
  A:可以,但是如果setState里的对象还是同一个就不会触发重新渲染

第一次正式的写技术文章,作文水平有限,希望可以帮到大家。

参考
[掘金]» useState源码解析

Youmeng博客 » 阅读源码后,来讲讲React Hooks是怎么实现的

相关文章

网友评论

      本文标题:react hooks 源码分析 --- useState

      本文链接:https://www.haomeiwen.com/subject/cdizehtx.html