react源码系列之五,实现简易版useState

202 阅读3分钟

1 工作流程

例如:

function App() {
  const [count, setCount] = useState(0);
  return <span onClick={() => setCount(count => count + 1)}>{count}</span>;
}
ReactDOM.render(<App />, document.getElementById("root"));
  • 状态更新章节提到,当调用ReactDOM.render会产生mount的更新并创建update对象,在点击span时会触发setCount的更新调用dispatchAction方法并创建update对象。随后会重新render,在render阶段会重新执行函数组件本书,重新调用useState重新计算新的state。 image.png

2 函数组件的update对象

  • 函数组件节点的update对象和ClassComponent及HostRoot类型节点的update对象并不相同,在现在这个例子中,我们先简化为如下格式:
const update = {
  action, // 更新执行的函数,即setCount(count => count + 1)中的箭头函数,
          // 也可直接传入新的数据setCount(2),这里简化只实现传入回调函数的情况;
  next: null // 与同一个Hook的其他更新形成链表
}
  • 若是下面这种情况,则会生成3个update并用next连接形成环状链表
function App() {
  const [count, setCount] = useState(0);
  const handleSpanClick = () => {
      setCount((count) => count + 1);
      setCount((count) => count + 1);
      setCount((count) => count + 1);
  };
  return <p onClick={handleSpanClick}>{num}</p>;
}
ReactDOM.render(<App />, document.getElementById("root"));

3 创建update

通过调用dispatchAction方法创建update并连接形成环状链表

function dispatchAction(queue, action) {
  const update = {
    action,
    next: null
  }
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;
  run();//模拟重新触发render
}

4 状态保存

  • 生成的update对象保存在updateQueue上
    • 对于hostComponent,在completeWork阶段会形成一个数组updateQueue:[要改变属性的key,对应的keyValue];
    • 对于ClassComponent和HostRoot则保存在queue的shared.pending属性中形成单向环状链表;
const fiber = {
  memoizedState: null,   // 保存Hooks链表
  stateNode: App
};

5 hook的数据结构

  • fiber的memoizedState保存了hooks的链表(每一个hook对应一个useState),hook的memoizedState保存了该hook的state数据;
hook = {
  // 保存update的queue,即上文介绍的queue
  queue: {
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

6 模拟ReactDOM.render运行

isMount = true; //实际是用current判断是mount还是update
let workInProgressHook = null;//指向当前正在执行的hook
function run() {
  workInProgressHook = fiber.memoizedState; //重置为fiber保存的第一个Hook
  fiber.stateNode();   // 模拟render阶段调用renderWithHooks
  isMount = false;
}

7 计算state

function useState(initialState) {
  let hook;
  if (isMount) {
    //创建hook
    hook = {
      queue: {
        pending: null,
      },
      memoizedState: initialState,//初始化state
      next: null,
    };
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook; //创建第一个hook
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    hook = workInProgressHook;
    //更新下一次调用useState时的hook指针,函数内部多个useState方法从上往下同步依次执行
    //当一个函数组件内部多次调用useState时,只要每次执行函数本身时useState的调用顺序及数量保持一致,
    //那么在更新时始终可以通过workInProgressHook找到当前useState在mount阶段创建的保存在内存中的hook对象。
    workInProgressHook = workInProgressHook.next;
  }
  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    //此hook上存在需要计算的update
    let firstUpdate = hook.queue.pending.next; //hook.queue.pending保存了最后一个update,环状链表
    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending.next); //遍历这条链表
    hook.queue.pending = null; //代表这次update已经计算完成;
  }
  hook.memoizedState = baseState;
  //这里dispatchAction利用bind绑定了第一个参数queue,并返回一个新的函数,该函数在调用setXxx()时接收action回调函数
  return [baseState, dispatchAction.bind(null, hook.queue)];
}

8 模拟运行并调试

function App() {
  const [count, setCount] = useState(0);
  console.log({ isMount }, { count });
  //为简化,不返回jsx对象
  return {
    onClick() {
      setCount((count) => count + 1);
    },
  };
}
window.app = run();

控制台打印如下:

image.png