# 深入 react 技术栈(二)

# 前言

近期在工作过程中遇到了 react 状态管理的问题,后面查阅了一些文章,发现关于 useState 的原理解析比较少。所以这篇文章从讲解原理出发,到简单实现,并列出代码示例。希望能给大家带来一些帮助,今后遇到状态管理的问题都能迎刃而解。

温馨提示:使用 react 时,了解源码(react 源码比 vue 源码复杂)内容并不是必要条件。本文偏探索 react 内部如何运作比较多,所以这里适用对象包括:对 react 内部运行机制感兴趣的同学和熟练使用 Hook 的同学。这里本人非常提倡大家能在日常工作使用更多的 Hook 来减少代码实现或者逻辑解耦

class 组件经常会使人们难以理解,并且大大增加我们的代码量。使用 Hooks 不仅解决了这两个问题,它还有其他优点,其中包括:

  • 易于构建和重用部分状态逻辑
  • 更容易将复杂的组件拆分为多个小模块
  • 避免了生命周期混乱的情况
  • 更容易添加状态类型
  • 更有利于代码进行单元测试

因此,这很容易让我们产生疑问,为什么 Hooks 这么强大?

# useState 的原理

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

看一个官方示例:

import React, { useState } from "react";

function Example() {
  // 声明一个 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

温馨提示:函数组件没有实例,也没有状态。它的渲染只是一个执行函数获取返回值然后渲染的过程。

组件中 useState 会返回当前状态更新状态的方法。初始状态只会在函数第一次执行的时候赋值。每当状态改变之后,视图会重新渲染。其中,假如 Hooks 中存在多个 useState 的时候,我们可以尝试用数组保存这些状态。

下面,我们试着自己动手实现一个 useState 。

# useState 的实现

# 初步实现

// 把状态放在最外面,方便记录 state 的变更
let state;

const useState = (initialValue) => {
  state = state || initialValue;
  // 更新 state 的值
  const updateState = (newValue) => {
    state = newValue;
    render();
  };

  return [state, updateState];
};

// render 方法
const render = () => {
  ReactDom.render(<App />, document.getElementById("root"));
};

这里我们实现了对一个 state 的存储和记忆。实际在日常开发中往往要管理多个 state 。这里我们想到可以用一个数组,去储存所有的 state

# 加强版实现

现在我们需要优化一下 useState ,解决不能同时管理多个 state 的问题(这里没有考虑组件与组件之间 state 变量命名相同的情况)。

// 存放所有状态值
let stateArr = [];
// 记录索引
let index = 0;

const useState = (initialValue) => {
  // 记录当前操作的索引
  const currentIndex = index;
  stateArr[index] = stateArr[index] || initialValue;
  const updateState = (newValue) => {
    stateArr[currentIndex] = newValue;
    render();
  };

  // 避免重复操作当前值
  index += 1;
  return [stateArr[currentIndex], updateState];
};

const render = () => {
  // 每一次 render 都需要重置索引
  index = 0;
  ReactDom.render(<App />, document.getElementById("root"));
};

总结:

  1. 打开页面初次渲染,useState 执行会依次将对应的 updateState 方法绑定到对应索引位置,然后将初始值存入 stateArr 中。
  2. 用户操作,触发 updateState 更新多个索引下的 state 。
  3. 重新渲染,依次执行 useState ,只不过当前的 stateArr 中已经存了最后一次 state 更新之后的值,这里传入 useState 方法的值不在是最开始的初始值,而是最后一次更新后的 state 值。

我们的简单实现就到这里。其实这里的 useState 和真实的 react 中的 useState 还存在很大的区别。

温馨提示:刚才方法里的 state 对应着 react 中的 memoizedState ,真正的 react 存储所有状态的也不是数组结构,而是链表。至于为什么要用链表 (opens new window)结构,大家感兴趣的可以研究一下 react fiber (opens new window) 的实现。react fiber 是 react 核心算法(调度机制)的重新实现。一种致力于提升对动画、布局和手势等领域适用性的架构。

注意:hook 与 FunctionComponent fiber 都存在 memoizedState 属性,不要混淆他们的概念。fiber.memoizedState:FunctionComponent 对应 fiber 保存的 Hooks 链表。hook.memoizedState:Hooks 链表中保存的单一 hook 对应的数据。

那么,react 中是怎样实现 useState 的呢?

# 探索 react 内部

# 定义 Hook

这里看到 ReactFiberHooks.new.js (opens new window) 文件里面,Hook 初始化的几个属性。

为了避免 react 源码可能因 github.com 网络原因加载缓慢问题,这里源码链接采用 Gitee 中链接代替。

export type Hook = {|
  // 指向当前渲染节点 Fiber,上一次完整更新之后的最终状态值
  memoizedState: any,
  // 初始化 state,以及每次 dispatch 之后新的 state
  baseState: any,
  // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  baseQueue: Update<any, any> | null,
  // 缓存的更新队列,存储多次更新行为
  queue: UpdateQueue<any, any> | null,
  // 指向下一个 hook,通过 next 串联每一个 hook
  next: Hook | null,
|};

Update 是一个带着标识符的对象,它是用来标识 react 当前需要更新的内容。有多少个 Update,就表示 react 接下来需要更新多少内容。比如:render 函数调用,或者改变 state 值方法调用的时候,都会创建 Update 更新对象。Update 对象彼此通过 next 相互连接,形成一个单向链表的数据结构。而 UpdateQueue 其实就是用于保存记录 Update 的一个队列。

# 初始化 state

# 1. Dispatch

HooksDispatcherOnMount (opens new window) 是首次加载时会执行的 Dispatch。

温馨提示:Dispatch 可以理解为改变内部 state 的方法之一。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  // 省略其他的 hook
  ...
  useState: mountState,
  ...
};

根据上面的代码,首次加载时,useState 会调用 mountState (opens new window)

# 2. MountState

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 创建并返回当前的hook
  const hook = mountWorkInProgressHook();
  ...
  hook.memoizedState = hook.baseState = initialState;
  // 创建 queue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

回忆下,这里的 mountState 方法和我们刚才实现的加强版 useState 对比一下,是不是有内味儿了。mountWorkInProgressHook 方法会创建并返回对应的 hook。需要注意一下的是 queue 里 lastRenderedReducer 的值是 basicStateReducer (opens new window)。也就是说,上一次 render 使用的 reducer 是 basicStateReducer。

# 3. BasicStateReducer

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

温馨提示:basicStateReducer 会接受两个参数 state 和 action。然后会返回 action(state) 或者 action。个人理解是我们通过传入具体的值修改状态时(例如:setCount(520)),typeof action === 'function'不成立,直接返回 action。

我们传入的 action 是一个具体的值:

当传入 Setter 的是一个 Reducer 函数的时候:

# 更新 state

HooksDispatcherOnUpdate (opens new window) 是更新时会执行的 Dispatch。这里的 useState 会执行 updateState。

# 1. UpdateState

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

这里 updateState 最终会返回 updateReducer (opens new window)。并且传入 basicStateReducer 作为第一个参数。

# 2. UpdateReducer

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取当前hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  ...

  queue.lastRenderedReducer = reducer;

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

还记得 mountState 方法的 hook 是 mountWorkInProgressHook,而现在 updateReducer 使用的是 updateWorkInProgressHook。可能原因是:

  • mountState 只在初始化时调用一次。
  • updateReducer 可能在事件回调、副作用里触发更新和 render 阶段触发更新各种情况重复调用,需要条件判断,区别对待。

整体流程:先找到对应的 hook,然后根据 update 计算出该 hook 的 state 值,最后返回。

# 调用阶段 dispatchAction

最后一个比较大的模块,说一说 dispatchAction (opens new window) 的实现。刚才在 mountState 也看到了 dispatchAction 方法,也就是在 useState 调用的时候会执行它。

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  ...
  // 创建 update
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 将 update 追加到队列的末尾
  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;

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // render 阶段触发的更新
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // fiber 的更新队列为空时的处理
      if (lastRenderedReducer !== null) {
        ...
      }
    }
    ...
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
  ...
}

整体流程:先创建 update,然后将 update 追加到队列的末尾,最后开启任务调度。

currentlyRenderingFiber 指当前的 render 阶段,触发 update 时,通过 bind 方法保证 fiber 和 currentlyRenderingFiber 全等,这代表本次 update 发生于 FunctionComponent 对应 fiber 的 render 阶段。

fiber.lanes 保存 fiber 上存在的任务调度中 update 的优先级。

fiber.lanes === NoLanes 说明现在 fiber 上没有 update。

hook 上可能存在多个不同优先级的 update,最终 state 的值由多个 update 共同决定。当 fiber 上不存在 update 时,此时创建的 update 为该 hook 上第一个 update,初始化 state 时也只依赖该 update,所以这里可以通过条件判断避免很多不必要的计算。也就是如果计算出的 state 与该 hook 之前保存的 state 一致,那么完全不需要开启一次调度。即使计算出的 state 与该 hook 之前保存的 state 不一致,在初始化和更新 state 值时也可以直接使用 dispatchAction 已经计算出的 state。

# 总结

一句话:state 初始化时创建 hook。触发 dispatch 时按顺序插入 update。updateState 的时候再按顺序触发 reducer。

大家现在有没有对 useState 有了新的认识了呢?

最后,文章中没有讲到(但值得拓展)的地方:

  • react 新的调度算法(fiber)是如何实现的?
  • 为什么要用链表结构?
  • useEffect 是怎么玩的?

# 参考文章

# 感谢

  • 文中如有错误,欢迎在评论区批评指正。
  • 图片来源网络,版权如有侵犯请联系本人删除。
  • 如果本文对你有帮助,就点个 Star (opens new window) 支持下吧!感谢阅读。