从源码剖析React Hooks之useReducer、useState

803 阅读8分钟

前言

React16.8引入hooks以来,我们可以在函数组件中完成之前类组件才独有的状态、生命周期等功能,加之函数组件性能优于类组件,大家开始大量使用函数组件代替原来的类组件。而这正是得益hooks中的useStateuseReduceruseEffect的使用,本文主要介绍useStateuseReducer,两者比较相似都是实现状态的功能,后者主要是运用于一些场景比较复杂的情况。在源码中,useState其实就是内置reducer函数的useReducer,本文基于React18.2带大家进行深入的了解。

源码分析

基本使用

import React from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return count++;
    case 'decrement':
      return count--;
    default:
      throw new Error();
  }
}
function PersionInfo() {
  const [count, dispatch] = useReducer(reducer, 0);
  
  return (
    <>
      {count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

上述代码是关于useReducer的基本使用,reducer是一个纯函数,我们通过dispatch来分发不同的动作借此来控制状态count的改变,进而引起页面的改变,完成页面的更新。

我们知道我们可以在一个函数组件中使用多种hook,而每种hook又可以使用多个,而且每个hook又可以多次使用setState或着dispatch方法来改变状态,React又是怎么管理每个hooks,每个hook又是怎么管理每一次更新,以及React要求不能在判断条件中使用hooks又是为什么,所有答案都会在源码中揭晓,我们一一探究。

实现状态共享

import { useState, useReducer, useEffect } from 'react';

我们通过上述的方式引入hooks,那我们引入的这个useReducer又是什么呢,我们点开这个这个文件,发现我们调用的useReducer实际上是ReactCurrentDispatcher.current这个变量提供的方法。那React为什么要去重新定义这样的一个值去间接实现呢?

// src/react/src/ReactHooks.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';

function resolveDispatcher() {
  // 默认值是null
  return ReactCurrentDispatcher.current;
}

/**
 * useReducer
 * @param {*} reducer 处理函数,用于根据老状态和动作计算新状态
 * @param {*} initialArg 初始状态
 * @returns
 */
export function useReducer(reducer, initialArg) {
  const dispatch = resolveDispatcher();

  return dispatch.useReducer(reducer, initialArg);
}

hooks实际上是在src/react-reconciler文件中配合fiber实现的,React必定是在这里做了状态共享,有意思的是,React在这里使用了一个十分'俏皮'的变量名

__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(秘密的,内部的,不要使用否则你会被解雇),初听这个变量着实是给我吓到了。而这个变量也是React实现hooks的关键变量。

// src/react/src/React.js
import { useReducer } from './ReactHooks';
import ReactSharedInternals from './ReactSharedInternals';

export {
  ...,
  ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
};
// src/react/src/ReactSharedInternals.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';

const ReactSharedInternals = {
  ReactCurrentDispatcher,
};

export default ReactSharedInternals;

React在上面两个文件中将这个内部变量共享出去,这个内部变量配合Fiber参与React调度过程,在不同的阶段执行不同的操作,主要是分为下面的挂载过程更新过程

挂载阶段

由上一小节我们知道,我们实际上调用的hooks来自于ReactCurrentDispatcher.current上面挂载的函数,显然这个函数在不同阶段是不同的,

// src/react-reconciler/src/ReactFiberHooks.js
/**
 * 渲染组件函数
 * @param {*} current 老fiber
 * @param {*} workInProcess 新fiber
 * @param {*} Component 组件定义
 * @param {*} props 组件属性
 * @returns 虚拟DOM
 */
export function renderWithHooks(current, workInProgress, Component, props) {
  currentlyRenderingFiber = workInProgress; // Function组件对应的fiber
  // 需要在函数组件执行之前给ReactCurrentDispatcher.current赋值

  if (current !== null && current.memoizedState !== null) {
    // 存在老fiber且有hook链表,走更新逻辑
    ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    ReactCurrentDispatcher.current = HooksDispatcherOnMount;
  }

  const children = Component(props);
  ...
  return children;
}

const HooksDispatcherOnMount = {
  useReducer: mountReducer,
  useState: mountState,
  ...
};
const HooksDispatcherOnUpdate = {
  useReducer: updateReducer,
  useState: updateState,
};

实际上我们在挂载阶段调用的useReducer最终是指向了mountReducer这个函数,我们借助这个函数观察一下hooks的结构。

function mountReducer(reducer, initialArg) {
  const hook = mountWorkInProgressHook();
  // 这里注意甄别memoizedState,函数组件Fiber的memoizedState指向hooks链表,而hook的memoizedState指向状态
  hook.memoizedState = initialArg;
  const queue = {
    pending: null,
    dispatch: null,
  };
  hook.queue = queue;

  const dispatch = (queue.dispatch = dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ));

  return [hook.memoizedState, dispatch];
}

这个主要是做了四件事情:

  • 通过mountWorkInProgressHook方法创建了一个hook结构,其中包括memoizedState用来保存状态值,queue表示这个hook更新队列,next表示该指向下一个hook
  • 保存初始值,useReducer允许输入一个初始值;
  • 初始化该hook的更新队列,更新队列包括一个pendingdispatch方法;
  • 绑定dispatch方法,该方法的主要作用是,创建一个更新,加到任务队列中,再就是引导React的更新。

到了这里,我们通过分析挂载阶段各个步骤来分析hook的结构。

function mountState(initialState) {
  return mountReducer(baseStateReducer, initialState);
}
function baseStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

同时,我们mountState简单复用了mountReducer

其实真正的源码中作了相同状态的识别,我们主要是懂原理,这里不作为重点只是简单的做了一个复用。

mountWorkInProgressHook

/**
 * 挂载构建中的hook
 */
function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null, // hook的状态
    queue: null, // 存放本hook的更新队列,queue.pending = update 的循环链表
    next: null, // 指向下一把hook,一个函数里里面可能会有多个hook,它们会组成一个单向链表
  };

  if (workInProgressHook === null) {
    // 当前函数对应的fiber的状态等于第一个hook对象,currentlyRenderingFiber指的当前函数组件的fiber
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

从上面的函数我们可以分析到,我们每次调用mountWorkInProgressHook函数都会生成一个hook,而这些hooks是以一个单向链表的方式挂在函数组件的fiber上的,其中currentlyRenderingFiber.memoizedState指向第一个hookworkInProgressHook指向最后一个hookhook之间通过next链接。我们简单画出下面的过程图。

执行如下代码,

const [a, setA] = useState();
const [b, dispatchB] = useReducer(reducer1);
const [c, dispatchC] = useReducer(reducer2);
const [d, setD] = useState();

生成如下的hooks结构

通过该结构我们也可以知道为什么不能在判断使用hooks, 正是这种链表结构,如果条件不成立,就会导致这种顺序错误,React也会抛错。

更新阶段

我们在上面阶段把每个hook按照顺序组成了一个单向链表,同时有两个指针方便我们取到链表的头尾,当进入到更新阶段,即运行如下代码:

dispatchB({action: 1});
dispatchB({action: 2});
dispatchB({action: 3});

由上一小节知道,我们调用dispatch方法实际上就是调用dispatchReducerAction方法,我们看看该方法主要是完成了哪些事情。

dispatchReducerAction

// src/react-reconciler/src/ReactFiberHooks.js
/**
 * 执行派发动作的方法,他要更新状态,并且让界面重新更新
 * @param {*} fiber function对应的fiber
 * @param {*} queue hook对应的更新队列
 * @param {*} ation 派发的动作
 */
function dispatchReducerAction(fiber, queue, action) {
  // 在每个hook里会存放一个更新队列,更新队列是一个更新对象的循环链表update1.next = update2.next = update1
  const update = {
    action, // {action: 1} 派发的动作
    next: null, // 指向下一个更新对象或者第一更新对象
  };

  // 将update对象添加到循环链表中
  const last = queue.last;
  if (last === null) {
    // 链表为空,将当前更新作为第一个,并保持循环
    update.next = update;
  } else {
    const first = last.next;
    if (first !== null) {
      // 在最新的update对象后面插入新的update对象
      update.next = first;
    }
    last.next = update;
  }
  // 将表头保持在最新的update对象上
  queue.pending = update;

  // 调度更新
  scheduleUpdateOnFiber();
}

对于每个单独的hook来说,他的每次更新都会生成一个update对象,这个对象会包括一个action派发动作和一个next指向下一个更新的指针,最后生成一个如图所示的环形链表hook的队列指向这个最后一个更新。

到这里我们在更新前的所有的准备工作已经完成了,我们有一个关于hooks的单向链表,上面会按照顺序依次记载着我们使用过的hook。在每个hook上面又会挂上每一次更新的环形链表,这样的结构方便我们拿到每一次的更新。当我们进入更新阶段的时候,即当我们函数再次运行到useReducer的时候,react就会帮我们调用updateReducer来计算每一次更新结果,并返回最新的状态。

updateReducer

下面我们根据源码来看看react的整个计算过程。

function updateReducer(reducer) {
  // 根据上一次hook创建一个新的hook,这里有属性的复用。
  const hook = {
    memoizedState: currentHook.memoizedState,
    queue: currentHook.queue,
    next: null,
  };
  // 获取新的hook的更新队列
  const queue = hook.queue;
  // 获取老的hook
  const current = currentHook;
  // 获取将要生效的更新队列
  const pendingQueue = queue.pending;
  // 初始化一个新的状态,取值为当前状态
  let newState = current.memoizedState;
  if (pendingQueue !== null) {
    queue.pending = null; // 重置更新队列
    const firstUpdate = pendingQueue.next; // 取出第一更新
    let update = firstUpdate;
    do {
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== firstUpdate);
  }

  hook.memoizedState = newState;
  return [hook.memoizedState, queue.dispatch];
}

我们看到在更新阶段主要是做了下面几件事:

  • 创建了一个新的hook,这里复用了之前的hook的属性,(这里的复用可能是比较奇怪,源码里面还进行了其他的指针改变的操作,这里我们关注点不在这里,所以做了一个简化);
  • 取出更新队列queue,和当前的状态值memoizedState
  • 如果存在更新队列,条件成立,断开hook的更新队列,完成如下图的操作;

  • 循环操作,根据条件和你传入的reducer计算每一次更新,得到的计算结果newState做为下一次的计算的入参,直到循环结束,返回最后的计算结果和dispatch方法,完成单个hook的更新。
//useState其实就是一个内置了reducer的useReducer
function baseStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}
function updateState() {
  return updateReducer(baseStateReducer);
}

总结

  • react通过单向链表的方式记录使用的每一个hook,fiber的memoizedState指针指向第一个hookworkInProgressHook指向最后一个hook。其中有指针指向第一个是因为方便从头计算,指向尾是因为方便向后添加新的hook。也是这种结构的原因导致不能在判断条件里面使用hook
  • 每个hook的更新都是一个环形链表,计算更新时,会依次计算每一个更新。
  • useState其实是内置了一个baseStateReduceruseReducer,可以是看成一个阉割版的useReducer