react-实现简洁版useState

loading 2024年01月20日 59次浏览

1. 工作原理

先从最简单的Hooks:useState看起。

它的工作原理很好理解:

  1. 通过一些途径产生更新,更新会造成组件render

  2. 组件render时useState返回的num为更新后的结果

function App() {
  const [num, setNum] = useState(0);

  return <p onClick={() => setNum(num => num + 1)}>{num}</p>;
}

其中步骤1的更新可以分为mount和update:

  • 调用ReactDOM.render会产生mount的更新,更新内容为useState的initialValue

  • 点击p标签触发setNum会产生一次update的更新,更新内容为num => num + 1

接下来讲解这两个步骤如何实现。

2. 函数式组件中的Update

更新该怎么理解,已经在前一篇博客讲得很清楚了,不管是Update本身还是储存它的Update queue

但是存在一个问题,不同于ClassComponent的实例可以存储数据,对于FunctionComponent,queue存储在哪里呢?

答案是:FunctionComponent对应的fiber中。

我们使用如下精简的fiber结构:

// App组件对应的fiber对象
const fiber = {
  // 保存该FunctionComponent对应的Hooks链表
  memorizedState: null,
  // 指向App函数
  stateNode: App
};

3. Hook数据结构

接下来我们关注fiber.memorizedState这一链表中保存的Hook的数据结构。

Hook与update类似,都通过链表连接。不过Hook是无环的单向链表。

hook = {
  // 保存update的queue,即上文介绍的queue
  queue: {
    pending: null
  },
  // 保存hook对应的state
  memorizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

注意区分fiber里的memorizedState和hook里的memorizedState

所以实际上的结构是这样的:

每个useState对应一个hook对象。

调用const [num, setNum] = useState(0)setNum产生的update保存在useState对应的hook.queue中。

4. 模拟React调度更新流程

在setNum函数的末尾,我们会触发schedule()模拟React调度更新流程。

我们用isMount变量指代是mount还是update。

// 首次render时是mount
isMount = true;

function schedule() {
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
}

在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。

workInProgressHook = workInProgressHook.next;

这样,只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。

5. 模拟useState

先来看大体代码,还是比较好理解的,留意对照着上面的hook数据结构看即可。

function useState(initialState) {
  // 当前useState使用的hook会被赋值该该变量
  let hook;

  if (isMount) {
    // ...mount时需要生成hook对象
  } else {
    // ...update时从workInProgressHook中取出该useState对应的hook
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    // ...根据queue.pending中保存的update更新state
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}

5.1 获取到hook对象

完善上面大体代码中的mount时生成hook和update时获取hook的代码:

if (isMount) {
  // mount时为该useState生成hook
  hook = {
    queue: {
      pending: null
    },
    memoizedState: initialState,
    next: null
  }

  // 将hook插入fiber.memoizedState链表末尾
  if (!fiber.memoizedState) {
    fiber.memoizedState = hook;
  } else {
    workInProgressHook.next = hook;
  }
  // 移动workInProgressHook指针
  workInProgressHook = hook;
} else {
  // update时找到对应hook
  hook = workInProgressHook;
  // 移动workInProgressHook指针
  workInProgressHook = workInProgressHook.next;
}

5.2 根据Update更新State

接下来再完善如果hook.queue.pending不为空(有更新)的情况下,如何根据queue中的Update更新State:

// update执行前的初始state
let baseState = hook.memoizedState;

if (hook.queue.pending) {
  // 获取update环状单向链表中第一个update
  let firstUpdate = hook.queue.pending.next;

  do {
    // 执行update action
    const action = firstUpdate.action;
    baseState = action(baseState);
    firstUpdate = firstUpdate.next;

    // 最后一个update执行完后跳出循环
  } while (firstUpdate !== hook.queue.pending.next);

  // 清空queue.pending
  hook.queue.pending = null;
}

// 将update action执行完后的state作为memoizedState
hook.memoizedState = baseState;

6. 总结

这样一来就实现了一个简洁版的useState,但是和正式的useState相比,这个实现出来的有以下不足:

  • React Hooks没有使用isMount变量,而是在不同时机使用不同的dispatcher。换言之,mount时的useState与update时的useState不是同一个函数。

  • React Hooks有中途跳过更新的优化手段。

  • React Hooks有batchedUpdates,当在click中触发三次updateNum,精简React会触发三次更新,而React只会触发一次。

  • React Hooks的update有优先级概念,可以跳过优先级低的update。