react hooks 源码分析 --- useState

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是怎么实现的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,289评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,968评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,336评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,718评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,734评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,240评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,631评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,599评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,139评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,166评论 3 345
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,286评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,917评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,604评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,075评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,205评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,814评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,351评论 2 365