React 源码阅读 - Hooks1

575 阅读15分钟

React 16.8 中新加入了 Hook 特性,如果你还没有用过 Hook,可以参考官方文档先进行了解,不在此赘述了。

Hook 可以让我们在不编写 class 组件的情况下使用状态等 React 特性。纯函数组件是没有状态的,也不能执行生命周期函数,所以使用场景比较有限,而 class 组件什么功能都有,只不过比较重,这其实并不太符合 React 的理念,所以 Hooks 就诞生了。

Hooks 原意为钩子,对于纯函数组件来说,如果需要什么状态或是什么副作用功能,那么我们就用钩子把他们钩进来。

从一个极简 Hooks 开始

实现一个不到100行代码的极简 useState Hook

首先看一个例子:

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

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

这个组件做的事情比较简单:

  1. 组件初始化挂载(mount),useState 会用初始值进行渲染,如果是更新(update),则会使用之前已经生成好的 num 的值;
  2. 点击 p 标签触发 setNumnum => num + 1 将被执行,这也是一个更新(update)过程。

Hook 的数据结构

先说明一下,一个 hook 对象和一个 useState 对应,所以我们使用 useState 的时候就相当于创建出了一个 hook 对象:

hook = {
  // 保存 hook 的更新队列
  queue: {
    pending: null
  },
  // 保存 hook 对应的 state
  memoizedState: initialState,
  // 与下一个 Hook 连接形成单向无环链表
  next: null
}

解释一下对象中的几个字段:

  • queue 是一个队列,我们每调用一次 setNum 就会在这个队列中加入一个新的 update
  • memoizedState 用来存储状态的值
  • next 是一个单向无环链表,指向下一个 hook,上面的例子只创建了一个 hook,实际项目中可能有很多个

更新(update)如何串联

代码里面每次对状态进行 set 操作都会形成一个更新(update),每一个更新(update)通过一个环形单向链表结构进行串联。

单个 update 数据结构如下:

const update = {
  // 更新执行的函数
  action,
  // 与同一个 Hook 的其他更新形成链表
  next: null
}

调用 setNum 实际上调用的是下面这个函数:

// 调用方式
dispatchAction.bind(null, hook.queue)

// 函数实现
function dispatchAction(queue, action) {
  // 创建 update
  const update = {
    action,
    next: null
  };

  // 环状单向链表操作
  if (queue.pending === null) { // 当前 hook 的更新队列中还没有正在处理的更新
    update.next = update; // 把当前更新的 next 指向当前更新,形成环
  } else { // 当前 hook 的更新队列中有正在处理的更新
    update.next = queue.pending.next; // 将当前更新的 next 指针指向当前更新队列中正在处理的更新的 next
    queue.pending.next = update; // 将当前更新队列中正在处理的更新的 next 指向当前更新,形成环
  }
  queue.pending = update; // 把新创建的更新作为当前 hook 的更新队列中正在处理的更新

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

这个单向环形链表不太好理解,将前面的例子改造并分析一下:

增加几个更新过程,形成新的例子:

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

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

执行第一个 setNum 的时候,queue.pending === null 成立,执行 update.next = updatequeue.pending = update,此时数据结构如下图所示:

update1.png

执行第二个 setNum 的时候,queue.pending === null 不成立,执行 update.next = queue.pending.nextqueue.pending.next = update,此时数据结构如下图所示:

update2.png

这样的结构有个好处就是在有多个 update 的情况下, queue.pending 始终指向最后一个插入的 update,遍历 update 的时候 queue.pending.next 指向第一个插入的 update

状态如何保存

class 组件的实例可以存储数据,但是函数组件并不能这样干,函数组件会将状态都存入到对应的 fiber 中。

精简后的结构如下:

// App 组件对应的 fiber 对象
const fiber = {
  // 保存该函数对应的 Hooks 链表
  memoizedState: null,
  // 指向 App 函数,App 函数执行之后渲染返回的结果是 DOM 节点
  // 所以这个属性也可以理解为保存的是该 fiber 对应的 DOM 节点
  stateNode: App
};

调度更新

前面的 dispatchAction 函数中,调用了一个 schedule 函数,这就是用于调度更新的函数。下面是实现思路:

  1. isMount 变量(全局变量)来代指 mount 还是 update
  2. 通过 workInProgressHook 变量(全局变量)指向当前正在工作的 hook
  3. 执行 fiberstateNode 方法,触发组件重新渲染,生成新的 DOM 节点。
  4. 组件首次 rendermount,以后再触发的更新为 update
  5. 在组件 render 时,每当遇到下一个 useState,我们移动 workInProgressHook 的指针。
// 首次 render 时是 mount
let isMount = true;
function schedule() {
  // 更新前将 workInProgressHook 重置为 fiber 保存的第一个 Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件 render
  fiber.stateNode();
  // 组件首次 render 为 mount,以后再触发的更新为 update
  isMount = false;
}
// 这是一个 Hook
let workInProgressHook;
// 用于切换 workInProgressHook 的指针,实际在 useState 函数内部有条件地调用执行
workInProgressHook = workInProgressHook.next;

每个不同的 Hook 都是在被串在一个链表中,前面说了一个 useState 创建一个 Hook, 所以组件每次渲染的时候 useState 的调用顺序及数量保持一致,就可以通过 workInProgressHook 找到当前 useState 对应的 Hook 对象。

useState 实现

直接上代码:

function useState(initialState) {
  let hook;

  if (isMount) { // 首次挂载
    // 初始化 hook
    hook = {
      queue: {
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    if (!fiber.memoizedState) {
      // 当前 fiber 没有存 hook,就把当前 hook 存进去
      fiber.memoizedState = hook;
    } else {
      // 当前 fiber 已经存 hook 了
      // 就把当前 hook 存到 hook 链表的 next 节点中(一个函数组件中定义了多个 hook 的情况)
      workInProgressHook.next = hook;
    }
    // 切换工作 hook 为当前 hook
    workInProgressHook = hook;
  } else { // 非首次挂载(更新的时候触发此逻辑)
    // 缓存工作 hook(作为上一个的 hook)
    hook = workInProgressHook;
    // 切换工作 hook
    workInProgressHook = workInProgressHook.next;
  }

  // 更新执行前的初始状态
  let baseState = hook.memoizedState;
  if (hook.queue.pending) { // 不为空代表存在更新
    // 取出第一个更新
    let firstUpdate = hook.queue.pending.next;

    // 执行更新队列中的所有更新
    do {
      // 要执行的动作,比如:setNum 中的 num => num + 1
      const action = firstUpdate.action;
      // 入参为初始状态,执行动作输出新状态,此处不是函数可能会有问题,精简版未支持
      baseState = action(baseState);
      // 切换到下一个更新
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending.next)

    // 更新队列执行完后清空
    hook.queue.pending = null;
  }
  // 更新 hook 的状态值
  hook.memoizedState = baseState;

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

完整代码

通过调用 App 返回的 click 方法模拟组件 click 的行为。调用 window.app.click() 模拟组件点击事件。

let workInProgressHook;
let isMount = true;

const fiber = {
  memoizedState: null,
  stateNode: App
};

function schedule() {
  workInProgressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;
  return app;
}

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;

  schedule();
}

function useState(initialState) {
  let hook;

  if (isMount) {
    hook = {
      queue: {
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next;

    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending)

      hook.queue.pending = null;
  }
  hook.memoizedState = baseState;

  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();

与 React 的差异

毕竟是精简版的 Hooks,和 React 相比还是少了很多功能:

  1. React Hooks 没有使用 isMount 变量,而是在不同时机使用不同的 dispatcher。换言之,mount 时的 useStateupdate 时的 useState 不是同一个函数。
  2. React Hooks 有中途跳过 更新 的优化手段。
  3. React HooksbatchedUpdates,当在 click 中触发三次 setNum,如果 setNum 的参数是数字等非函数类型精简版会触发三次更新,而 React 只会触发一次,另外精简版也不支持 setNum 的参数为非函数。
  4. React Hooksupdate优先级 概念,可以跳过不高优先的 update

源码

Hooks 从哪儿来

还是从 useState 入手来看源码,

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function App() {
  const [count, setCount] = useState(0); // 断点打到这儿进行调试

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

可以看到进入到了 hooks 定义的地方:

// path: packages/react/src/ReactHooks.js

// 省略一些代码

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

// 省略一些代码

可以看到 resolveDispatcher 函数,我们使用的 hooks 都是从这个函数返回的,不过当我们去看这个函数的定义的时候会惊奇的发现,就这:

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;

所以,这些 hooks 都是哪儿来的呢?

我来找一下给 ReactCurrentDispatcher.current 赋值的地方:

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  // 省略一些代码

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  if (__DEV__) {
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    // 是 mount 还是 update 在此判断
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

  // 执行函数组件
  let children = Component(props, secondArg);
  
  // 省略一些代码

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  // 省略一些代码
  
  return children;
}

从上面代码可以看出 mountupdate 时调用的是不同的 hook

HooksDispatcherOnMountHooksDispatcherOnUpdate 生产模式下主要还是用这俩,其定义如下:

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useOpaqueIdentifier: mountOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};
// 省略一些代码

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};
// 省略一些代码

最后执行了 ReactCurrentDispatcher.current = ContextOnlyDispatcher;ContextOnlyDispatcher 的定义如下:

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
export const ContextOnlyDispatcher: Dispatcher = {
  readContext,

  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
  useDeferredValue: throwInvalidHookError,
  useTransition: throwInvalidHookError,
  useMutableSource: throwInvalidHookError,
  useOpaqueIdentifier: throwInvalidHookError,

  unstable_isNewReconciler: enableNewReconciler,
};

从这里可以看出,执行函数组件后,将 hook 都指向了 throwInvalidHookError,就是抛出错误的函数,这时再调用 hook 就会报错,比如下面这种情况:

useEffect(() => {
  useState(0);
});

下面再来看一下 renderWithHooks 函数是怎么调进来的,从源码上看主要是两个地方调用他:

  • updateFunctionComponent 更新函数组件
  • updateForwardRef 更新 forwardRef 包裹的函数组件,一般和 useImperativeHandle 一起使用,用于暴露组件内部方法供外部调用

这两个函数都是在 beginWork 函数的 switch ... case 中调用的。

下图是 hooks 的大致调用过程:

hooks1.png

至此我们解决了 hooks 是从哪儿来的问题了,下面来看一下 hooks 都是具体是什么。

Hook 的数据结构

这里指单个 hook 的数据结构,比如:const [count, setCount] = useState(0); 定义的一个状态就是这里说的单个 hook

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
|};
  • memoizedState 保存着单一 hook 的对应数据(与 fiber 的 memorizedState 不一样,fiber 那个保存的是函数组件的 Hooks 链表)
  • queue 更新队列
  • next 下一个 hook
  • baseState 初始 state
  • baseQueue 初始 queue 队列

不同类型 hookmemoizedState 会有所不同:

  • useState 对于 const [state, updateState] = useState(initialState)memoizedState 保存 state 的值
  • useReducer 对于 const [state, dispatch] = useReducer(reducer, {});memoizedState 保存 state 的值
  • useEffect memoizedState 保存包含 useEffect回调函数依赖项 等的链表数据结构 effect
  • 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 函数的执行结果
  • useContext 没有 memoizedState

下图是一个完整的数据结构:

hooks.png

useState 与 useReducer

由前文可知调用 hook 的时候分为两个场景,一个是 mount,一个是 update

mount

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 创建并返回当前 hook
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // 赋值初始 state
  hook.memoizedState = hook.baseState = initialState;
  // 创建 queue
  const queue = (hook.queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // 创建 dispatch
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 创建并返回当前的 hook
  const hook = mountWorkInProgressHook();
  // 赋值初始 state
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  hook.memoizedState = hook.baseState = initialState;

  // 创建 queue
  const queue = (hook.queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  // 创建 dispatch
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

// 创建并返回对应 hook
function mountWorkInProgressHook(): Hook {
  // 新建一个 hook 对象
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  // 把新建的 hook 对象挂到链表上
  if (workInProgressHook === null) {
    // 还没有 hook 链表,就将 fiber 的 memoizedState 属性指向 hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 如果已经有链表了,就挂在当前链表的 next 上
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

可以看出 mount 场景下,useStateuseReducer 做的事情基本上是差不多的,都是先创建 hook,然后赋值输出状态,创建更新队列,最后创建 dispatch 并返回。

值得注意的是 queue,他们两个 hookqueue 有细微的差异,在 lastRenderedReducer 属性:

const queue = (hook.queue = {
  // 更新队列中正在处理的更新
  pending: null,
  interleaved: null,
  lanes: NoLanes,
  // 保存 dispatchAction.bind() 的值
  dispatch: null,
  // useState 是 lastRenderedReducer: basicStateReducer,
  // 上一次 render 时使用的 reducer
  lastRenderedReducer: reducer,
  // 上一次 render 时的 state
  lastRenderedState: (initialState: any),
});

// 下面典型案例执行的关键代码
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

可见,useStatereducer 参数为 basicStateReduceruseReducer

update

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
type Update<S, A> = {|
  lane: Lane,
  action: A, // 修改动作
  eagerReducer: ((S, A) => S) | null, // 下一个 reducer
  eagerState: S | null, // 下一次的 state
  next: Update<S, A>, // 下一个 update
|};

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取当前 hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  invariant(
    queue !== null,
    'Should have a queue. This is likely a bug in React. Please file an issue.',
  );

  queue.lastRenderedReducer = reducer;

  // 中间有一大堆链表操作,执行了 hook 的 queue 中的 update
  
  // 省略一些代码
  // 下面典型案例执行的关键代码
  const action = update.action;
  newState = reducer(newState, action);
  // 省略一些代码

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

很明显 useStateuseReducerupdate 场景下调用的是同一个函数 updateReducer

updateReducer 的流程比较复杂,尤其里面有一大堆难懂的链表操作,整个流程可以概括为一句话:找到对应的 hook,根据 update 计算该 hook 的新 state 并返回。

有一个点值得注意:

mount 时获取当前 hook 使用的是 mountWorkInProgressHook,而 update 时使用的是 updateWorkInProgressHook,这两个函数有一些不一样,这里的原因是:

  • mount 时可以确定是调用 ReactDOM.render 或相关初始化 API 产生的更新,只会执行一次。
  • update 可能是在事件回调或副作用中触发的更新或者是 render阶段 触发的更新,为了避免组件无限循环更新,后者需要区别对待。

dispatchAction

mountupdate 场景之后都调用了 dispatchAction 函数

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // 省略一些代码

  // 创建 update 对象
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // render阶段触发的更新
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
    // 把新 update 加入更新队列,并生成环形链表
    const pending = queue.pending;
    if (pending === null) {
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
  } else {
    if (isInterleavedUpdate(fiber, lane)) {
      const interleaved = queue.interleaved;
      if (interleaved === null) {
        update.next = update;
        pushInterleavedQueue(queue);
      } else {
        update.next = interleaved.next;
        interleaved.next = update;
      }
      queue.interleaved = update;
    } else {
      const pending = queue.pending;
      if (pending === null) {
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      queue.pending = update;
    }

    // 省略一些代码
    // 进行调度
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);

    // 省略一些代码
  }

  // 省略一些代码
}

典型使用场景

状态多次设置只有最后一次生效:

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

  const handleClick = () => {
    setNum(1);
    setNum(2);
    setNum(3);
  };

  return <p onClick={handleClick}>{num}</p>
}
function App() {
  const [num, setNum] = useState(0);

  const handleClick = () => {
    setNum(num + 1);
    setNum(num + 1);
    setNum(num + 1);
  };

  return <p onClick={handleClick}>{num}</p>
}

这两种情况都只有最后一次生效:前面一个是因为在 update 队列被遍历执行的过程中如果更新函数里面传入的只是个值的话前面执行了会直接把前面的值覆盖;后面一个是因为 状态 num 在每次执行 setNum 的时候并不会里面在该函数组件中更新,所以这三次 setNumnum 都是同一个值。

状态设置的时候传入函数,每次都生效:

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

  const handleClick = () => {
    setNum(preVal => preVal + 1);
    setNum(preVal => preVal + 1);
    setNum(preVal => preVal + 1);
  };

  return <p onClick={handleClick}>{num}</p>
}

这种情况每次执行 setNum 都是生效的,因为我们传入的是一个函数,函数在处理的时候会把每一次 update 队列的最新状态拿到,这在 updateReducer 函数中也是有体现的。

总结

  1. Hooks 是挂在对应 FibermemoizedState 上的;
  2. 一个函数组件中的多个 Hook 相互之间由一个链表串起来的,所以每次组件渲染的时候要保证顺序及数量一致才能让 workInProgressHook 找到当前 useState 对应的 Hook 对象,这也会 useState 不能放在 if 等条件中的原因;
  3. 某个 Hook 的更新可以被多次调用,每次调用都会产生一个 update 对象,而这些对象是以一个环形链表的数据结构存在 Hookqueue.pending 中的, queue.pending 始终指向最后一个插入的 update 对象,遍历 update 的时候 queue.pending.next 指向第一个插入的 update 对象;
  4. Hook 更新的时候调用 dispatchAction.bind(null, fiber, queue) 函数,其在执行的时候保持了对 fiberhook 的引用,从而不会被 js 引擎的垃圾回收销毁,所以函数组件才能保存住状态;
  5. Hook 的更新函数被调用之后会触发组件重新渲染,重新渲染的时候会按顺序依次执行所有的更新(hook 的 queue 里面的 update 对象);
  6. useState 可以理解为 useReducer 的一个特例;
  7. 我们通常认为,useReducer(reducer, initialState) 的传参为初始化参数,在以后的调用中都不可变,但是在 updateReducer 方法中,可以看到 lastRenderedReducer 在每次调用时都会重新赋值也就是说, reducer 参数是随时可变的。