基于 React 18 讲解 Hooks 原理

13,357 阅读4分钟

本文使用 React 源码版本:18.2.0

整体代码运行的 debug 过程,有录制视频教程,可以配合观看:视频地址

主要讲解常用 react hook 的内部运行机制,状态保存逻辑,以及不同 hook 对象在 Fiber 节点上的挂载方式。通过本文,可以了解到为什么不能中途改变 hook 的使用顺序,以及为什么要使用环状链表保存 effect 和 update 对象等常见 hook 问题。

前置知识点

Fiber 架构

react 16.18.0 版本引入 fiber 架构,实现异步可中断更新。先把 vdom 树转成 fiber 链表,然后再渲染 fiber。主要是解决之前由于直接递归遍历 vdom,不可中断,导致当 vdom 比较大的,频繁调用耗时 dom api 容易产生性能问题。

下面是 fiber 树的示意图:

  • reconcile 阶段将 vdom 转换成 fiber,确定节点操作,并创建用到的 DOM
  • commit 阶段执行实际 DOM 操作

Fiber 数据结构

代码地址

主要分下面几块:

  • 节点基础信息的描述
  • 描述与其它 fiber 节点连接的属性
  • 状态更新相关的信息
  • 优先级调度相关

这边和 hook 关联比较大的主要是 memoizedState 和 updateQueue 属性。函数组件会将内部用到的所有的 hook 通过单向链表的形式,保存在组件对应 fiber 节点的 memoizedState 属性上。updateQueue 是 useEffect 产生的 effect 连接成的环状单向链表。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;						// Fiber对应组件的类型 Function/Class/Host...
  this.key = key;						// key属性
  this.elementType = null;	// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
  this.type = null;					// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
  this.stateNode = null;		// Fiber对应的真实DOM节点

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;		// 指向父级Fiber节点
  this.child = null;		// 指向子Fiber节点
  this.sibling = null;	// 指向右边第一个兄弟Fiber节点
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性 —— 保存本次更新造成的状态改变相关信息
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;		// class 组件 Fiber 节点上的多个 Update 会组成链表并被包含在 fiber.updateQueue 中。 函数组件则是存储 useEffect 的 effect 的环状链表。
  this.memoizedState = null;	// hook 组成单向链表挂载的位置
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

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

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

Hook 数据结构

代码位置

hook 的 memoizedState 存的是当前 hook 自己的值。

const hook: Hook = {
  memoizedState: null,	// 当前需要保存的值

  baseState: null,
  baseQueue: null,	// 由于之前某些高优先级任务导致更新中断,baseQueue 记录的就是尚未处理的最后一个 update
  queue: null,	// 内部存储调用 setValue 产生的 update 更新信息,是个环状单向链表

  next: null,		// 下一个hook
};

不同类型hookmemoizedState保存不同类型数据,具体如下:

  • useState:对于const [state, updateState] = useState(initialState)memoizedState保存state的值

  • useEffectmemoizedState保存包含useEffect回调函数依赖项等的链表数据结构effecteffect链表同时会保存在fiber.updateQueue

  • useRef:对于useRef(1)memoizedState保存{current: 1}

  • useMemo:对于useMemo(callback, [depA])memoizedState保存[callback(), depA]

  • useCallback:对于useCallback(callback, [depA])memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果。

示例

可以从截图中看到,代码中使用的 useState 和 useRef 两个 hook 通过 next 连接成链表。另外 useState 的 hook 对象的 queue 中存储了调用 setValue 时用到的函数。

function App() {
  const [value, setValue] = useState(0);
  const ref = useRef();
  ref.current = "some value";

  return (
    <div className="App">
      <h1>目前值:{value}</h1>
      <div>
        <button onClick={() => { 
          setValue(v => v + 1)
        }}>增加</button>
      </div>
    </div>
  );
}

Hooks 链表创建过程

每个 useXxx 的 hooks 都有 mountXxx 和 updateXxx 两个阶段。链表只创建一次,在 mountXxx 当中,后面都是 update。

mountXxx 阶段代码:HooksDispatcherOnMountInDEV 代码地址

以 useState 为例,mount 时会进入 HooksDispatcherOnMountInDEVuseState方法,最终执行 mountState

HooksDispatcherOnMountInDEV = {
    ...
    useState: function (initialState) {
      currentHookNameInDev = 'useState';
      mountHookTypesDev();
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

      try {
        return mountState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
    ...
  };

mountState 内部会创建当前 hook 的 hook 对象,不同 useXXX 的差异主要就在 mountXXX 函数里面,每种 hooks api 都有不同的使用 hook.memorizedState 数据的逻辑,后面会介绍几个重点的。

mountWorkInProgressHook 是个通用方法,所有 hook 都会执行**,通过它新建 hook 对象,如果前面没有hook 对象,就将该 hook 挂到当前 fiber 节点的 memoizedState上面,否则接到前一个 hook 对象的 next 上,构成单向链表。**

为什么不能在循环、条件或嵌套函数中调用 Hooks?

同样的问题是“为什么不能改变 hook 的执行顺序?”

通过上面介绍已经知道各个 hook 在 mount 时会以链表的形式挂到 fiber.memoizedState上。

update 时会进入到 HooksDispatcherOnUpdateInDEV,执行不同 hook 的 updateXxx 方法。

updateXxx 阶段代码:HooksDispatcherOnUpdateInDEV 代码地址

HooksDispatcherOnUpdateInDEV = {
    ...
    useState<S>(
      initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
      currentHookNameInDev = 'useState';
      updateHookTypesDev();
      const prevDispatcher = ReactCurrentDispatcher.current;
      ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
      try {
        return updateState(initialState);
      } finally {
        ReactCurrentDispatcher.current = prevDispatcher;
      }
    },
    ...
  };

最终会通过 updateWorkInProgressHook方法获取当前 hook 的对象,获取方式就是从当前 fiber.memoizedState上依次获取,遍历的是 mount 阶段创建的链表,故不能改变 hook 的执行顺序,否则会拿错。(updateWorkInProgressHook 也是个通用方法,updateXXX 都是走到这个地方)

// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;	// 当前在执行的 hook 对象
let workInProgressHook: Hook | null = null;

...

function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      // 刚开始更新,从 fiber.memoizedState 获取第一个 hook 对象
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 如果不是,则获取链表中的下一个 hook
    nextCurrentHook = currentHook.next;
  }
  ...
  return workInProgressHook;
}

具体 hook

useRef

代码位置:mountRefupdateRef

  • mount 时:把传进来的 value 包装成一个含有 current 属性的对象,然后放在 memorizedState 属性上。
  • update 时:直接返回,没做特殊处理

对于设置了 ref 的节点,什么时候 ref 值会更新?

组件在 commit 阶段的 mutation 阶段执行 DOM 操作,所以对应 ref 的更新也是发生在 mutation 阶段。

useCallback

代码位置:mountCallbackupdateCallback

  • mount 时:在 memorizedState 上放了一个数组,第一个元素是传入的回调函数,第二个是传入的 deps。
  • update 时:更新的时候把之前的那个 memorizedState 取出来,和新传入的 deps 做下对比,如果没变,那就返回之前的回调函数,否则返回新传入的函数。

比对是依赖项是否一致的时候,用的是Object.is

Object.is() 与 === 不相同。差别是它们对待有符号的零和 NaN 不同,例如,=== 运算符(也包括 == 运算符)将数字 -0 和 +0 视为相等,而将 Number.NaNNaN 视为不相等。

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // is() 用的是 Object.is,只是多了些兼容代码
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

useMemo

代码位置:mountMemoupdateMemo

和 useCallback 大同小异。

  • mount 时:在 memorizedState 上放了个数组,第一个元素是传入函数的执行结果,第二个元素是 deps。
  • update 时:取出之前的 memorizedState,和新传入的 deps 做下对比,如果没变,就返回之前的值。如果变了,创建一个新的数组放在 memorizedState,第一个元素是新传入函数的执行结果,第二个元素是 deps。

useEffect

代码位置:mountEffectImplupdateEffectImpl

useLayoutEffect 在 mount 和 update 这块和 useEffect 差不多,就不展开讲了。

mount 时和 update 时涉及的主要方法都是 pushEffect,update 时判断依赖是否变化的原理和useCallback 一致。像上面提到的 memoizedState 存的是创建的 effect 对象的环状链表。

pushEffect 的作用:是创建 effect 对象,并将组件内的 effect 对象串成环状单向链表,放到fiber.updateQueue上面。即 effect 除了保存在 fiber.memoizedState 对应的 hook 中,还会保存在 fiber 的 updateQueue 中

function pushEffect(tag, create, destroy, deps) {
  // 创建 effect 对象
  var effect = {
    tag: tag,	// effect的类型,区分是 useEffect 还是 useLayoutEffect
    create: create,	// 传入use(Layout)Effect函数的第一个参数,即回调函数
    destroy: destroy,	// 销毁函数
    deps: deps,	// 依赖项
    // Circular
    next: null
  };
  // 获取 fiber 的 updateQueue
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    // 如果前面没有 effect,则将componentUpdateQueue.lastEffect指针指向effect环状链表的最后一个
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 如果前面已经有 effect,将当前生成的 effect 插入链表尾部
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // 把最后收集到的 effect 放到 lastEffect 上面
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

function createFunctionComponentUpdateQueue() {
  return {
    lastEffect: null,
    stores: null
  };
}

hook 内部的 effect 主要是作为上次更新的 effect,为本次创建 effect 对象提供参照(对比依赖项数组),updateQueue 的 effect 链表会作为最终被执行的主体,带到 commit 阶段处理。即 fiber.updateQueue 会在本次更新的 commit 阶段中被处理,其中 useEffect 是异步调度的,而 useLayoutEffect 的 effect 会在 commit 的 layout 阶段同步处理。等到 commit 阶段完成,更新应用到页面上之后,开始处理 useEffect 产生的 effect,简单说:

  • useEffect 是异步调度,等页面渲染完成后再去执行,不会阻塞页面渲染。
  • uselayoutEffect 是在 commit 阶段新的 DOM 准备完成,但还未渲染到屏幕前,同步执行。

为什么如果不把依赖放到 deps,useEffect 回调执行的时候拿的会是旧值?

updateEffectImpl 的逻辑可以看出来,effect 对象只有在 deps 变化的时候才会重新生成,也就保证了,如果不把依赖的数据放到 deps 里面,用的 effect.create还是上次更新时的回调,函数内部用到的依赖自然就还是上次更新时的。即不是 useEffect 特意将回调函数内部用到的依赖存下来,而是因为,用的回调函数就是上一次的,自然也是从上一次的上下文中取依赖值,除非把依赖加到 deps 中,重新获取回调函数。

依照这个处理方式也就能了解到:对于拿对象里面的值的情况,如果对象放在组件外部,或者是通过 useRef 存储,即使没有把对象放到 deps 当中,也能拿到最新的值,因为 effect.create 拿的只是对象的引用,只要对象的引用本身没变就行。

useState

代码位置:mountStateupdateState

  • mount 时:将初始值存放在memoizedState 中,queue.pending用来存调用 setValue(即 dispath)时创建的最后一个 update ,是个环状链表,最终返回一个数组,包含初始值和一个由dispatchState创建的函数。

为什么要是环状链表?—— 在获取头部或者插入尾部的时候避免不必要的遍历操作

(上面提到的 fiber.updateQueue 、 useEffect 创建的 hook 对象中的 memoizedState 存的 effect 环状链表,以及 useState 的 queue.pending 上的 update 对象的环状链表,都是这个原因)

方便定位到链表的第一个元素。updateQueue 指向它的最后一个 update,updateQueue.next 指向它的第一个update。

若不使用环状链表,updateQueue 指向最后一个元素,需要遍历才能获取链表首部。即使将updateQueue指向第一个元素,那么新增update时仍然要遍历到尾部才能将新增的接入链表。

function mountState(initialState) {
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,	// update 形成的环状链表
    interleaved: null,		// 存储最后的插入的 update 
    lanes: NoLanes,
    dispatch: null,		// setValue 函数
    lastRenderedReducer: basicStateReducer,	// 上一次render时使用的reducer
    lastRenderedState: initialState		// 上一次render时的state
  };
  hook.queue = queue;
  var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}
  • update 时:可以看到,其实调用的是 updateReducer,只是 reducer 是固定好的,作用就是用来直接执行 setValue(即 dispath) 函数传进来的 action,即 useState 其实是对 useReducer 的一个封装,只是 reducer 函数是预置好的。

updateReducer 主要工作

  • 将 baseQueue 和 pendingQueue 首尾合并形成新的链表

  • baseQueue 为之前因为某些原因导致更新中断从而剩下的 update 链表,pendingQueue 则是本次产生的 update链表。会把 baseQueue 接在 pendingQueue 前面。

  • 从 baseQueue.next 开始遍历整个链表执行 update,每次循环产生的 newState,作为下一次的参数,直到遍历完整个链表。即整个合并的链表是先执行上一次更新后再执行新的更新,以此保证更新的先后顺序

  • 最后更新 hook 上的参数,返回 state 和 dispatch。

function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook();
  // hook.queue.pending 指向update环转链表的最后一个update,即链表尾部
  var queue = hook.queue;

  queue.lastRenderedReducer = reducer;
  var current = currentHook; // The last rebase update that is NOT part of the base state.

  // 由于之前某些高优先级任务导致更新中断,baseQueue 记录的就是尚未处理的最后一个 update
  var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
  // 当前 update 链表最后一个 update
  var pendingQueue = queue.pending;

  if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    if (baseQueue !== null) {
      // 合并 baseQueue 和 pendingQueue,baseQueue 排在 pendingQueue 前面
      var baseFirst = baseQueue.next;
      var pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  // 合并后的 update 链表不为空时开始循环整个 update 链表计算新 state
  if (baseQueue !== null) {
    // We have a queue to process.
    var first = baseQueue.next;
    var newState = current.baseState;	// useState hook当前的state
    var newBaseState = null;
    var newBaseQueueFirst = null;
    var newBaseQueueLast = null;
    var update = first;

    do {
      var updateLane = update.lane;

      ...
      
      if (update.hasEagerState) {
        // If this update is a state update (not a reducer) and was processed eagerly,
        // we can use the eagerly computed state
        newState = update.eagerState;
      } else {
        // 取得当前的update的action,可能是函数也可能是具体的值
        var action = update.action;
        newState = reducer(newState, action);
      }
      
      update = update.next;
    } while (update !== null && update !== first);

    ...

    // 把最终得倒的状态更新到 hook上
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  } 

  ...

  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

dispath 调用时做了什么事情?

主要是执行 dispatchSetState函数,创建本次更新的 update 对象,计算本地更新后的新值,存储到 update.eagerState中,并把该 update 和之前该 hook 已经产生的 update 连成环状链表。

  • 创建 update 对象:
var update = {
  lane: lane,
  action: action,	// 执行的具体数据操作
  hasEagerState: false,
  eagerState: null,		// 依据当前 state 和 action 计算出来的新 state
  next: null			//指向下一个update的指针
};
  • 构建 update 环状链表:如果前面没有 update,则直接自己连自己,如果有update,则将自己插入到原本最后一个 update 与 第一个 update 之间,并将自己赋值给存储最后一个 update 的 queue.interleaved

dispatchSetState的作用:

function dispatchSetState(fiber, queue, action) {

  // 创建 update 
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null
  };
  
  // 是否在渲染阶段更新
  if (isRenderPhaseUpdate(fiber)) {
    // 将 update 存到 queue.pending 当中
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    ...
    var lastRenderedReducer = queue.lastRenderedReducer;
    ...
    
    // 计算当前 reducer 下生成的 state
    var currentState = queue.lastRenderedState;
    var eagerState = lastRenderedReducer(currentState, action); 
  
    // Stash the eagerly computed state, and the reducer used to compute
    // it, on the update object. If the reducer hasn't changed by the
    // time we enter the render phase, then the eager state can be used
    // without calling the reducer again.
    update.hasEagerState = true;
    update.eagerState = eagerState;
    
    // 将新增的 update 插入 update 链表尾部并返回 root 节点
    var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  
    if (root !== null) {
      var eventTime = requestEventTime();
  
      // 执行调度方法,实现更新
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
  
  markUpdateInDevTools(fiber, lane);
}


// 将新增的 update 插入 update 链表尾部
function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
  var interleaved = queue.interleaved;

  if (interleaved === null) {
    // This is the first update. Create a circular list.
    update.next = update; // At the end of the current render, this queue's interleaved updates will
    // be transferred to the pending queue.

    pushConcurrentUpdateQueue(queue);
  } else {
    update.next = interleaved.next;
    interleaved.next = update;
  }

  queue.interleaved = update;
  return markUpdateLaneFromFiberToRoot(fiber, lane);
}

// 将 update 存到 queue.pending 当中
function enqueueRenderPhaseUpdate(queue, update) {
  // This is a render phase update. Stash it in a lazily-created map of
  // queue -> linked list of updates. After this render pass, we'll restart
  // and apply the stashed updates on top of the work-in-progress hook.
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  var 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;
}

简单实现

详细讲解原文地址:极简 hook 实现

涵盖了dispath、创建 update、形成 update 环状链表、更新时遍历整个 update 链表、通过 action 计算新 state 的大概逻辑。

let workInProgressHook;
let isMount = true;	// 是mount还是update。

const fiber = {
  memoizedState: null,	// 保存该FunctionComponent对应的Hooks链表
  stateNode: App
};

function schedule() {
  /* 
   更新前将workInProgressHook重置为fiber保存的第一个Hook,
   workInProgressHook变量指向当前正在工作的hook,
   在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。
   这样,只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。
  */
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  const app = fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
  return app;
}

function dispatchAction(queue, action) {
  // 创建update
  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;

  // 模拟React开始调度更新
  schedule();
}

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

  if (isMount) {
    // mount时为该useState生成hook
    hook = {
      // 保存update的queue,即上文介绍的queue
      queue: {
        pending: null	// 始终指向最后一个插入的 update,是一个环状单向链表
      },
      // 保存hook对应的state
      memoizedState: initialState,
       // 与下一个Hook连接形成单向无环链表
      next: null
    }
    // 将hook插入fiber.memoizedState链表末尾
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;	// 移动workInProgressHook指针
  } else {
    // update时从workInProgressHook中取出该useState对应的hook
    hook = workInProgressHook;	 // update时找到对应hook
    workInProgressHook = workInProgressHook.next;	// 移动workInProgressHook指针
  }

  let baseState = hook.memoizedState;	// update执行前的初始state
  if (hook.queue.pending) {
    // 根据queue.pending中保存的update更新state
    let firstUpdate = hook.queue.pending.next;	// 获取update环状单向链表中第一个update

    do {
      // 执行update action
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
      // 最后一个update执行完后跳出循环
    } while (firstUpdate !== hook.queue.pending)
      hook.queue.pending = null;	// 清空queue.pending
  }
  hook.memoizedState = baseState;	// 将update action执行完后的state作为memoizedState

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

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

  console.log(`${isMount ? 'mount' : 'update'} num: `, num);

  return {
    click() {
      updateNum(num => num + 1);
    }
  }
}

window.app = schedule();

自定义 hook

自定义 hook 和直接在组件内使用自定义 hook 中用到的 hook,形成的fiber.memoizedState hook 链表结构一致,没啥特殊的,不做过多描述。

参考