React Hooks的二三事

预告

本文将解答一些常见问题,以确保使用Hook的时候达到人码合一的境界。问题如下:

  • React 是如何把对 Hook 的调用和组件联系起来的?
  • 为什么Hook不能写进if语句里?
  • 怎么做到多次调用同一个setState只有最后一个触发渲染的?
  • React怎么知道Hook在不在函数组件内执行?

1、React 是如何把对 Hook 的调用和组件联系起来的?

这个问题在React文档里有https://react.docschina.org/docs/hooks-faq.html#how-does-react-associate-hook-calls-with-components
讲得很抽象,我们还是从具体例子看吧

例子

  • 一个普通的function Component
export default function () {
  const [name, setName] = useState[''];
  const [value, setValue] = useState[''];

  return (
    <div className="upload-image">
      <TextBox name={name} value={value} />
    </div>
  );
}

每一次发生渲染的时候,这个function都会被执行以得到一个更新后的vnode(虚拟dom树的一个节点)

问题

每次函数的执行都会执行useState方法,这里有两个useState,为什么每次都能得到正确的value和setValue呢?

相关源码如下:

1、mount阶段

  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 = hook.queue = {
      pending: null,
      dispatch: null,
      lastRenderedReducer: basicStateReducer,
      lastRenderedState: initialState
    };
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
  }
  • 中心思想就是通过mountWorkInProgressHook这个函数得到一个hook对象,然后给这个对象设置lastRenderedReducer和lastRenderedState(对应value和setValue),然后return这个元组。
  function mountWorkInProgressHook() {
    var hook = {
      memoizedState: null,
      baseState: null,
      baseQueue: null,
      queue: null,
      next: null
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
    } else {
      // Append to the end of the list
      workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
  }

这里创建了一个hook对象,然后判断一下这个workInProgressHook全局变量是否有值,如果有值就赋给workInProgressHook.next,并且将workInProgressHook置为这个新的hook

mountWorkInProgressHook
  • 总结
    1、在mount阶段,执行useState就是创建一个新对象,保存value、setValue;
    2、workInProgressHook是hook链表的尾节点;
    3、然后把这个新的hook对象作为hook链表里的一个节点插入到workInProgressHook的下一个节点里,并更新workInProgressHook。

2、update阶段

  function updateState(initialState) {
    return updateReducer(basicStateReducer);
  }

function updateReducer(reducer, initialArg, init) {
    var hook = updateWorkInProgressHook();
    var queue = hook.queue;
…… // 一些操作
    var dispatch = queue.dispatch;
    return [hook.memoizedState, dispatch];

}

update阶段也是一样的,需要通过updateWorkInProgressHook来得到hook,这个hook里保存有对应的数据(value、reducer等)

  • updateWorkInProgressHook
  function updateWorkInProgressHook() {
    // This function is used both for updates and for re-renders triggered by a
    // render phase update. It assumes there is either a current hook we can
    // clone, or a work-in-progress hook from a previous render pass that we can
    // use as a base. When we reach the end of the base list, we must switch to
    // the dispatcher used for mounts.

    var nextWorkInProgressHook;

    if (workInProgressHook === null) {
      nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
    } else {
      nextWorkInProgressHook = workInProgressHook.next;
    }

    if (nextWorkInProgressHook !== null) {
      // There's already a work-in-progress. Reuse it.
      workInProgressHook = nextWorkInProgressHook;
      nextWorkInProgressHook = workInProgressHook.next;
      currentHook = nextCurrentHook;
    } else {
……

      var newHook = {
        memoizedState: currentHook.memoizedState,
        baseState: currentHook.baseState,
        baseQueue: currentHook.baseQueue,
        queue: currentHook.queue,
        next: null
      };

      if (workInProgressHook === null) {
        // This is the first hook in the list.
        currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
      } else {
        // Append to the end of the list.
        workInProgressHook = workInProgressHook.next = newHook;
      }
    }

    return workInProgressHook;
  }

只看workInProgressHook,关键代码就是三行

 workInProgressHook = nextWorkInProgressHook;
 nextWorkInProgressHook = workInProgressHook.next;
return workInProgressHook;

这里就是将nextWorkInProgressHook指针next了一下。
指向hook链表的下一个节点

总结

  • 在mount阶段,每次执行useXXXhook,其实就是创建了一个新的hook对象,用以保存这个hook的值和其他状态;这个对象会被添加到一个链表上。
  • workInProgressHook是一个指针,指向hook链表的尾部。
  • 在update阶段,也是通过.next遍历链表,得到当前hook对象来做更新操作

2、为什么Hook不能写进if语句里?

React文档里对于这个问题的描述如下

只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

这个问题和问题1很像。

回答

  • 由问题1得知,因为每一次数组重新渲染是通过遍历hook链表来拿到每一个useXXX对应的那个hook对象的;
  • 如何遍历一个链表,就是curNode=curNode.next;
  • 所以,如果前一次渲染所遍历的那个hook链表和后一个不同,比如使用if之后个数就不一样了,那就不能得到正确的hook对象了。

3、怎么做到多次调用同一个setState只有最后一个触发渲染的?

示例

  const [val, setValue] = useState('');
  useEffect(() => {
    setValue('a');
    setValue('b');
    setValue('c');
    setValue('d');
  }, []);

上面这段代码不会让function Component 执行四次,而是只有一次。

源码

调用setValue的时候,其实在调用dispatchAction(fiber, queue, action)

function dispatchAction<A>(
  componentIdentity: Object,
  queue: UpdateQueue<A>,
  action: A,
) {
  invariant(
    numberOfReRenders < RE_RENDER_LIMIT,
    'Too many re-renders. React limits the number of renders to prevent ' +
      'an infinite loop.',
  );

  if (componentIdentity === currentlyRenderingComponent) {
    // 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.
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    // This means an update has happened after the function component has
    // returned. On the server this is a no-op. In React Fiber, the update
    // would be scheduled for a future render.
  }
}

queue

  • 这里的queue就是上面提到的,每一个hook都有自己的hook对象,然后这个hook对象拥有一个属性叫queue
    var hook = {
      memoizedState: null,
      baseState: null,
      baseQueue: null,
      queue: null,
      next: null
    };

这个queue是一个链表,每一个节点叫update,结构如下。

const update: Update<A> = {
      action,
      next: null,
    };
  • 然后这不是调了四次setValue嘛,每一次调的时候都会新生成update,然后添加到这个queue的尾部


  • 结束了之后,新的一次对于function Component的调用又来了,这时候照常调用了useState,如下。
  const [val, setValue] = useState('');
  useEffect(() => {
    setValue('a');
    setValue('b');
    setValue('c');
    setValue('d');
  }, []);

结合我们之前说过的,这个useState会得到val。这个得到val的过程其实就是如果有更新,就更新这个val值,并返回,以供下面用到它的地方使用,源码如下
updateState其实就是调用updateReducer

function updateReducer(reducer, initialArg, init) {
……
   do {
     ……
          var action = update.action;
          newState = reducer(newState, action);
    
……
      update = update.next;
    } while (update !== null && update !== first);
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
……
  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

这里使用do while一直去遍历这个queue链表,然后计算newState,直到遍历到queue结束为止。
此时得到的newState就是最终的value了。

回答

  • 在每次setState的时候,会创建一个update对象用以储存value,然后把这个update对象塞进这个hook持有的queue链表末尾;
  • 在发生渲染时,调用useState会拿出queue链表遍历来依次调用reducer得到新的value,而这个新的value最终的值是这个链表末尾的那个update节点的值。

4、React怎么知道Hook在不在函数组件内执行?

报错如下

Invalid hook call. Hooks can only be called inside of the body of a function component.


去源码里搜一下这个报错,找到代码如下

  function throwInvalidHookError() {
    {
      {
        throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
      }
    }
  }
……
  var ContextOnlyDispatcher = {
    readContext: readContext,
    useCallback: throwInvalidHookError,
    useContext: throwInvalidHookError,
    useEffect: throwInvalidHookError,
    useImperativeHandle: throwInvalidHookError,
    useLayoutEffect: throwInvalidHookError,
    useMemo: throwInvalidHookError,
    useReducer: throwInvalidHookError,
    useRef: throwInvalidHookError,
    useState: throwInvalidHookError,
    useDebugValue: throwInvalidHookError,
    useResponder: throwInvalidHookError,
    useDeferredValue: throwInvalidHookError,
    useTransition: throwInvalidHookError
  };
……
 function pushDispatcher(root) {
    var prevDispatcher = ReactCurrentDispatcher$1.current;
    ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
……
  }

dispatcher

调试的时候发现的



所有的useXXX在执行的时候,都会执行一次这个方法,例如


  • 这个方法就是获取dispatcher,如果dispatcher=ReactCurrentDispatcher.current没有值就会报错。
    这个ReactCurrentDispatcher对应的源码里的ReactCurrentDispatcher$1
  • 为什么会有这个dispatcher是因为react需要在不同环境下运行(这是一个设计模式,忘了),比如浏览器、RN、服务器等。
    因为React帮我们判断好了当前环境,我们代码只有一份,但是可以跑在不同的环境中。
    React是怎么做到的呢?
dispatcher.useState(initialState);

他调用useState的时候是调的dispatcher上的,这个dispatcher会根据环境不同被有差异地处理过。

ReactCurrentDispatcher$1

说回ReactCurrentDispatcher$1.current
涉及到的代码如下

  function pushDispatcher(root) {
    var prevDispatcher = ReactCurrentDispatcher$1.current;
    ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;

    if (prevDispatcher === null) {
      // The React isomorphic package does not include a default dispatcher.
      // Instead the first renderer will lazily attach one, in order to give
      // nicer error messages.
      return ContextOnlyDispatcher;
    } else {
      return prevDispatcher;
    }
  }

  function popDispatcher(prevDispatcher) {
    ReactCurrentDispatcher$1.current = prevDispatcher;
  }
  • 也就是说,只有在pushDispatcher和popDispatcher里,ReactCurrentDispatcher$1.current才会被赋值
  • 而调用这两个函数只有performConcurrentWorkOnRoot和performSyncWorkOnRoot这两个地方;这已经是组件初始化的地方了。


回答

  • 每一个useXXX都是React通过dispatcher.useXXX这种方式来调用的,为什么是这种方式,是因为React希望用统一的写法来运行在不同的环境中;
  • 如果dispatcher是空,则会报这个错;
  • dispatcher的赋值是在组件初始化的时候赋值的
  • 所以当useState被执行的时候,如果dispatcher没有值,就代表它不在组件内部被调用。

参考:
1、https://mp.weixin.qq.com/s/J0_PLrbVZMRAiwjWK2WDrw
2、https://react.docschina.org/docs/hooks-faq.html#how-does-react-associate-hook-calls-with-components
3、https://react.docschina.org/docs/hooks-rules.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容