hook源码解析之useState链表存储

129 阅读4分钟

常见问题

  1. 为什么不能在循环,条件,嵌套函数中使用hooks
  2. 为什么fiber.updateQueue,hook.queue存储是环装链表
  3. 为什么React不建议使用componentWillxxx的生命周期

名词解释

diff算法

fiber将在reconcile阶段生成,当首次将vdom(深度优先遍历)生成fiber之后,再次触发更新将使用diff算法生成新的fiber。

diff算法原理

对比current fiber和vdom,生成workInProcess Fiber树

  1. 第一阶段遍历vdom看节点是否可以复用,如果可以复用打成更新的effectTag,新建更新fiber,如果节点不能复用,结束第一阶段,如果vdom都遍历完成可以复用之后,将多余旧fiber链表打成删除的effectTag。
  2. 遍历旧的fiber链表生成map对象,遍历vdom,查看节点是否存在map对象中,如果存在就移动过来,打effectTag。
  3. 遍历完vdom之后,将多余旧fiber链表打成删除的effectTag。

fiber

fiber架构,实现异步中断更新。将vdom转为fiber链表,然后在渲染fiber。主要用于之前需要递归遍历vdom,不能中断,当vdom较大时,存在性能问题。(每个ReactElment对应一个fiber)

  • reconcile(调和)阶段将vdom采用前序遍历的方式转为fiber,确定节点操作,并打effectTag
  • 在commit阶段执行实际dom操作
  • react基于fiber的渲染流程render(reconcile+schedule)+commit阶段

数据结构

function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // 用于标记组件类型
  this.key = key; // 唯一的key值,用于判断fiber是否可以复用
  this.elementType = null;
  this.type = null; // 描述对应的组件,自定义组件'App',对于默认元素'div','span'
  this.stateNode = null; // 对应组件的dom

  // Fiber
  this.return = null; // 父
  this.child = null; // 子
  this.sibling = null; // 兄弟
  this.index = 0;

  this.ref = null;
  this.refCleanup = null;

  this.pendingProps = pendingProps; // 初始化props
  this.memoizedProps = null; // 更新之后的props
  
  // class 组件Fiber节点上的多个Update组成链表,函数组件的useEffect的effect组件的环状单向链表
  this.updateQueue = null; 
  this.memoizedState = null; // hook组成的单向链表挂载的位置
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags; // 标记fiber更新的状态,比如是新建,更新,删除等
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  // 调度优先级
  this.lanes = NoLanes; 
  this.childLanes = NoLanes;

  // 指向改fiber在另一次更新的fiber
  this.alternate = null;

  .....
}

Hook

fiber中的memoizedState存的当前的hooks

const hook: Hook = {
    memoizedState: null, // 保存不同数据类型的数据(useEffect,useCallback,useMemo,useState,useRef)
    baseState: null,
    baseQueue: null, // 上次中断,记录的就是尚未处理的最后一个update
    queue: null, // 记录update的函数,setValue
    next: null, // 下一个hook
 };

第一步:初始化hook(mountWorkInProgressHook)

  1. 当workInProgressHook为null时,创建hook对象,赋值给fiber.memoizedState和workInProgressWork
  2. 否则改变workInProgressHook指针,将hook链接 第二步:更新hook(updateWorkInProgressHook)
  3. 当前执行的hook,workInProgressHook,取fiber.memoizedState
  4. workInProgressHook.next
function mountWorkInProgressHook() {
  let hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (!workInProgressHook) {
    workInProgressHook = fiber.memoizedState = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

function updateWorkInProgressHook() {
  let hook = workInProgressHook;
  if (!workInProgressHook) {
    workInProgressHook = fiber.memoizedState;
  } else {
    workInProgressHook = workInProgressHook.next;
  }

  return hook;
}

useState源码步骤

mountState

  1. 执行mountWorkInProgressHook函数,创建hook
  2. 给hook中的memoizedState,baseState,queue,queue.dispath属性赋值
  3. 返回[hook.memoizedState,dispath];
function mountState(initState) {
  let hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState =
    typeof initState === "function" ? initState() : initState;

  let queue = {
    pending: null, // 存储update的环状单项链表
    dispath: null,
    lastRenderedState: null,
    lastRenderedReducer: null,
  };

  let dispath = (queue.dispath = dispathSetState.bind(null, fiber, queue));

  hook.queue = queue;

  return [hook.memoizedState, dispath];
}


updateState

  1. 调用updateReducer函数,以下为改函数中的步骤
  2. 执行updateWorkInProgressWork函数,获取当前执行hook
  3. 将hook.baseQueue插入queue.pending头部
  4. 遍历baseQueue的生成最新的state,赋值给hook.memoizedState
  5. 返回[hook.memoizedState,dispath];
function basicStateReducer(state, action) {
  return typeof action === "function" ? action(state) : action;
}


function updateReducer(reducer, initialArg) {
  let hook = updateWorkInProgressHook();

  let baseQueue = hook.baseQueue;
  let queue = hook.queue;
  let pendingQueue = queue.pending;

  if (baseQueue) {
    const firstQueue = baseQueue.next;
    baseQueue.next = pendingQueue.next;
    pendingQueue.next = firstQueue;
  }

  baseQueue = hook.baseQueue = pendingQueue;
  queue.pending = null;

  if (baseQueue) {
    let first = baseQueue.next;
    let update = first;
    let newState = hook.baseState;

    do {
      newState = reducer(newState, update.action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.baseQueue = null;
    hook.memoizedState = hook.baseState = newState;
  }

  return [hook.memoizedState, hook.queue.dispath];
}

function updateState(initState) {
  return updateReducer(basicStateReducer, initState);
}

dispathSetState

  1. 创建update对象
  2. 插入到queue.pending的尾部,收尾相连的环状列表
  3. 执行调度
function schedule(fiber) {
  workInProgressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;

  return app;
}

function dispathSetState(fiber, queue, action) {
  let update = {
    action,
    next: null,
  };

  if (queue.pending) {
    update.next = queue.pending.next;
    queue.pending = queue.pending.next = update;
  } else {
    queue.pending = update.next = update;
  }

  schedule(fiber);
}

demo代码 该代码中仅实现usestate的链表存储,并没有实现批量更新及异步中断更新等功能。

知识点

react遍历vdom采用的深度优先遍历(根左右)

react更新

react更新分为两个阶段render+commit

render 阶段

创建fiber对象,生成effectList

commit阶段

  1. before motation (操作dom之前)
  2. motation(操作dom)

ref值更新

  1. layout (dom更新之后)

useLayoutEffect

运算符

  • |=:两个二进制位数都为0,则为0,否则为1
  • &=:两个二进数位数都为1,则为1,否则为0
  • ^=:两个二进数位数的值都相同,则为0,否则为1

参考文档

zhuanlan.zhihu.com/p/553744711 juejin.cn/post/711910… zhuanlan.zhihu.com/p/63739227 xie.infoq.cn/article/977…