React (3)—— useState包括手写miniState

110 阅读5分钟

useState

useState 的数据保存在 fiber components 节点上;通过链表的方式保存 hooks;所以 hooks 的顺序不能改变。

function Component() {
  const [count, setCount] = useState(0); // hook 1
  useEffect(() => {}); // hook 2
  const [name, setName] = useState(""); // hook 3
}

miniState 实现

为了更好的理解useState建议有余力的同学可以手写下 miniState。

根据我的建议呢,手写之前需要先了解关于链表的知识点 看我的另一篇文章 前端需要了解的链表

对链表有所了解之后就可以来看这段实现 useState 的代码了。 这里建议将代码 copy 到编辑器中运行打断点来梳理,当对整了解了整个运行过程后,可以自己开始手写实现,在手写的过程中感觉有哪里没有理解在回过头来看代码。

hooks可以理解为两个阶段 mountupdate 分别对应初始化和用户操作,这里用isMount变量来作为区分; useState本质上就是一个函数,同一个函数多次调用如何区分呢,这里主要靠workInprogressHook来作为指针,指向当前 hook;fiber 的memoizedState会记录 hooks 的链表顺序。

let isMount = true;
let workInprogressHook = null;
const fiber = {
  stateNode: App,
  memoizedState: null, // 记录state;
};

miniState 源码实现:

let isMount = true;
let workInprogressHook = null;
const fiber = {
  stateNode: App,
  memoizedState: null, // 记录state;
};

// 模拟hook 调度
const schedule = () => {
  workInprogressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;
  return app;
};

const useState = (initialState) => {
  let hook;

  /**
   *  更新hook链表的流程
   */
  // 初始化阶段构建一个链表,fiber.memoizedState
  // workInprogressHook 作为链表的指针 指向当前hook
  if (isMount) {
    hook = {
      memoizedState: initialState,
      next: null,
      queue: {
        pending: null,
      },
    };
    // 表示第一次执行的第一个hook
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      // 指针指向当前hook
      workInprogressHook.next = hook;
    }
    workInprogressHook = hook;
  }
  // 获取当前hook,改变指针
  else {
    hook = workInprogressHook;
    workInprogressHook = workInprogressHook.next;
  }

  /**
   * 执行steState action
   */
  // 获取上一次的值
  let baseState = hook.memoizedState;

  if (hook.queue.pending) {
    // 第一个action;
    let firstUpdate = hook.queue.pending.next;
    // 循环执行环状链表
    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      // 更新指针
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending.next);

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

function dispatchAction(queue, action) {
  let update = {
    action,
    next: null,
  };
  if (queue.pending === null) {
    //自身与自身链接
    update.next = update;
  } else {
    // 尾 和 首进行链接 u1-> u0
    // update 是 u1 ,  queue.pending.next 是u0
    // u1.next-> u0
    update.next = queue.pending.next;

    //  queue.pending.next 是u0
    //  update 是 u1
    // u0.next -> u1
    queue.pending.next = update;

    // 总结一下  形成环状链表
    // u1.next -> u0 -> u0.next -> u1 -> u1.next
  }
  queue.pending = update;
  // 触发app更新

  setTimeout(() => {
    schedule();
  }, 0);
}

function App() {
  const [num, updateNum] = useState(0);
  const [num1, updateNum2] = useState(0);
  return {
    onclick() {
      updateNum((num) => num + 1);
      updateNum((num) => num + 2);
      updateNum((num) => num + 3);
      updateNum2((num) => num + 10);
    },
  };
}

window.app = schedule();

执行过程时序图:

image-17.png

sequenceDiagram
participant User
participant Schedule
participant App
participant useState
participant dispatchAction
participant Fiber
participant Hooks

    Note over User, Hooks: 初始化阶段
    User->>Schedule: 调用 schedule()
    activate Schedule
    Schedule->>Fiber: workInprogressHook = memoizedState (null)
    Schedule->>App: 执行 fiber.stateNode()
    activate App
    App->>useState: 第一次调用 useState(0)
    activate useState
    useState->>Fiber: 检查 fiber.memoizedState (null)
    useState->>Hooks: 创建 hook1 {memoizedState:0, queue.pending:null}
    useState->>Fiber: fiber.memoizedState = hook1
    useState->>App: 返回 [0, dispatchAction]
    deactivate useState

    App->>useState: 第二次调用 useState(0)
    activate useState
    useState->>Fiber: 检查 fiber.memoizedState (hook1)
    useState->>Hooks: 创建 hook2 {memoizedState:0, queue.pending:null}
    useState->>Hooks: hook1.next = hook2
    useState->>App: 返回 [0, dispatchAction]
    deactivate useState

    App-->>Schedule: 返回 app 对象
    deactivate App
    Schedule->>Schedule: isMount = false
    Schedule-->>User: 返回 app
    deactivate Schedule

    Note over User, Hooks: 用户交互阶段
    User->>App: 调用 onclick()
    activate App
    App->>dispatchAction: updateNum(num=>num+1) (hook1.queue)
    App->>dispatchAction: updateNum(num=>num+2) (hook1.queue)
    App->>dispatchAction: updateNum(num=>num+3) (hook1.queue)
    App->>dispatchAction: updateNum2(num=>num+10) (hook2.queue)
    deactivate App

    Note over User, Hooks: 更新调度阶段
    dispatchAction->>Hooks: 为hook1创建u1,u2,u3
    Hooks->>Hooks: 构建环形链表 u1→u2→u3→u1
    Hooks->>Hooks: hook1.queue.pending = u3
    dispatchAction->>Hooks: 为hook2创建u4
    Hooks->>Hooks: 构建自环 u4→u4
    Hooks->>Hooks: hook2.queue.pending = u4
    dispatchAction->>Schedule: 异步调用 schedule()

    Note over User, Hooks: 更新阶段
    activate Schedule
    Schedule->>Fiber: workInprogressHook = memoizedState (hook1)
    Schedule->>App: 执行 fiber.stateNode()
    activate App

    App->>useState: 第一次调用 useState()
    activate useState
    useState->>Fiber: 获取当前hook (hook1)
    useState->>Hooks: 读取hook1.queue.pending (u3)
    loop 处理hook1的更新队列
        useState->>Hooks: 执行action u1: 0+1=1
        useState->>Hooks: 执行action u2: 1+2=3
        useState->>Hooks: 执行action u3: 3+3=6
    end
    useState->>Hooks: hook1.memoizedState = 6
    deactivate useState

也十分建议配合卡颂老师的文章和视频一起学习 文章链接 视频链接

state 的异步和批量更新

上面的miniState只是模拟了 State 的部分实现,并不能完全反映真实代码中 state 的更新流程,接下来我们介绍异步和批量更新,这个 React 初学者最困惑的部分。

useState 的异步

通过上面的代码可以看出来 useState 数据的更新不是同步的,每次 state 的更新都会触发组件重新Render;

const [state, setState] = useState("1");
const click = () => {
  setSate("18");
  console.log(state); // 1
};

所以在我们改变了 state 的同时没有办法立刻通过获取state来拿到最新的值;这里在某些特殊极端场景下需要配合ref来拿到最新值;

useState 批量更新: 批量更新就是指,短时间内同时更新了多个 state,React 会将他们合并更新,避免多次触发 Redner;

const [state, setState] = useState("1");
const [state1, setState1] = useState("2");
const click = () => {
  setSate("18");
  setState1("3");
};

这里的批量更新在 React 17 和 18 版本中有很大的差异需要注意

React17 中批量更新在异步方法中是不会生效的,包括 promise 下面都会触发两次Render

const [state, setState] = useState("1");
const [state1, setState1] = useState("2");
const click = () => {
  setTimeout(() => {
    setSate("18");
    setState1("3");
  }, 0);
};
const [list, setList] = useState(1);
const [list2, setList2] = useState(2);
const fun = () => {
  return new Promise((reslove) => {
    reslove("sccuess");
  });
};

const onClick = () => {
  fun().then((res) => {
    setList(list + 1);
    setList2(list2 + 1);
  });
};

React18中,对于这种场景做了很多支持,批量更新全部覆盖,上面的场景都会只render一次; 需要注意的是,需要使用React提供的全新apicreateRoot来启用新特性。

import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);

也提供了强制退出批处理的api,这里通常不需要使用(谨慎使用!)

import { flushSync } from 'react-dom';
flushSync(() => setCount(1)); // 强制同步提交

这两种批量更新的模式在React底层中分别对应 legacy模式 concurrent模式