一句话概括:由于 hooks 的存储结构是链表结构。 每次重新渲染组件,会调用上一次 hooks 链表的初始值,赋值给新创建的 hooks 链表, 如果在循环、条件、嵌套函数中使用 hooks,会导致赋值错误。

注意:hooks 链表存储顺序,不等同于 hooks 的调用的时机顺序

链表存储在源码中的体现

每次调用 hooks 都会生成 hook 对象,其结构如下

  1. export type Hook = {|
  2. memoizedState: any,
  3. baseState: any,
  4. baseQueue: Update<any, any> | null,
  5. queue: UpdateQueue<any, any> | null,
  6. next: Hook | null,
  7. |};

着重关注memoizedStatenext属性,不同hookshook对象的memoizedState保存着不同对象。

  • useStatehook.memoizedState = state;
  • useEffecthook.memoizedState = effect
  • useMemohook.memoizedState = [nextValue, nextDeps];
  • useCallbackhook.memoizedState = [callback, nextDeps];
  • useRefhook.memoizedState = ref;

一般我们会在函数组件中多次使用hooks,这会产生多个hook对象。

组件挂载时

这些对象通过next属性连接形成链表,连接是在挂载时形成的,主要函数是 mountWorkInProgressHook

  1. function mountWorkInProgressHook() {
  2. const hook = {
  3. memoizedState: null,
  4. baseState: null,
  5. baseQueue: null,
  6. queue: null,
  7. next: null,
  8. };
  9. if (workInProgressHook === null) {
  10. // This is the first hook in the list
  11. // 设置一个链表头
  12. currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  13. } else {
  14. // Append to the end of the list
  15. // 将其他 hook 连接到 next
  16. workInProgressHook = workInProgressHook.next = hook;
  17. }
  18. return workInProgressHook;
  19. }

挂载时,hooks 都会调用该函数生成 hook 对象并连接。

  1. 如果 workInProgressHook === null,说明是hook链中的第一个,将其赋值给currentlyRenderingFiber.memoizedState并更新workInProgressHook,这说明函数组件的FibermemoizedState保存着第一个hook的引用。
  2. workInProgressHook !== null,则将新生成的hook赋值给上个 hooknext 属性并更新workInProgressHook

    组件更新时

    更新时,hooks 都会调用 updateWorkInProgressHook 函数生成新的hook 对象并连接形成hook 链以供下次更新时使用。

    1. function updateWorkInProgressHook(): Hook {
    2. let nextCurrentHook: null | Hook;
    3. if (currentHook === null) { // currentHook === null 说明当前是hook链的第一个
    4. // current 为上次更新的组件对应的Fiber
    5. const current = currentlyRenderingFiber.alternate;
    6. if (current !== null) {
    7. // current.memoizedState保存着hook链的第一个hook
    8. nextCurrentHook = current.memoizedState;
    9. } else {
    10. nextCurrentHook = null;
    11. }
    12. } else {
    13. // 取出 currentHook.next,即上次更新中hook链的下一个hook
    14. nextCurrentHook = currentHook.next;
    15. }
    16. // 此时nextCurrentHook代表着本hooks在上次更新中的hook对象
    17. let nextWorkInProgressHook: null | Hook;
    18. if (workInProgressHook === null) { //workInProgressHook === null 说明当前是hook链的第一个
    19. nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
    20. } else {
    21. nextWorkInProgressHook = workInProgressHook.next;
    22. }
    23. // 此时nextWorkInProgressHook绝大多数时候为null,因为 renderWithHooks 函数在执行函数组件前会将
    24. // currentlyRenderingFiber.memoizedState置为null,workInProgressHook.next也为null
    25. if (nextWorkInProgressHook !== null) { // 忽略这条分支
    26. // There's already a work-in-progress. Reuse it.
    27. workInProgressHook = nextWorkInProgressHook;
    28. nextWorkInProgressHook = workInProgressHook.next;
    29. currentHook = nextCurrentHook;
    30. } else {
    31. // 绝大多数时候会进入此分支
    32. // currentHook即上次更新中相应顺序的hook对象
    33. currentHook = nextCurrentHook;
    34. // 复用上次更新的hook对象的属性,生成新的hook对象
    35. const newHook: Hook = {
    36. memoizedState: currentHook.memoizedState,
    37. baseState: currentHook.baseState,
    38. baseQueue: currentHook.baseQueue,
    39. queue: currentHook.queue,
    40. next: null,
    41. };
    42. //形成新的 hook 链,和挂载时一样
    43. if (workInProgressHook === null) {
    44. // This is the first hook in the list.
    45. currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    46. } else {
    47. // Append to the end of the list.
    48. workInProgressHook = workInProgressHook.next = newHook;
    49. }
    50. }
    51. return workInProgressHook;
    52. }

    画图理解

    假如有这样一个不规范的组件。
    挂载时 props.id存在,但更新时 props.id不存在 ```jsx import { useState, useMemo, useEffect, useCallback } from ‘react’

const HooksDemo = (props) => { const [count, setCount] = useState(0);

const memoObj = useMemo(() => { return { name: ‘konsoue’ } }, [])

// useEffect 被嵌套了 if (props.id) { useEffect(() => { console.log(‘useEffect’); }, []) }

const handler = useCallback(() => console.log(‘useCallback’), [])

return (

{memoObj.name}
) }

export default HooksDemo ``` React Hooks 为什么只在顶层使用 - 图1

参考资料

《为什么React Hooks会有两条使用规则》