React Hooks 小结

Read the latest article and comment on Notion.

所有的理解都基于React V16.11

两篇很有用的文章

React Hooks 源码解析(3):useState

  • Hooks 有什么好处?

    赋予了函数式组件状态,用类组件来管理状态可能有如下缺点:

    • 逻辑分散,处理同一个东西的逻辑可能会被分散在 componentDidMount componentDidUpdate componentDidUnmount 里面。
      特别是 useEffect 把一些副作用操作都统一了。
    • 范式复杂,class 可能把一些原可以简单的组件弄复杂了。
  • 涉及到的数据结构有哪些?

    • 这些数据结构的具体定义和交互是怎么样的?

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3raXW2aU-1644039451266)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/91445c9d-3adb-49af-8b73-8b4e6c04cc8a/Untitled.png)]

      Hook

      export type hook = {
        memorizedState: any,
        
        baseState: any,
        baseUpdate: Update<any, any> | null,
        queue: UpdateQueue<<any, any> | null,
      
        next: Hook | null,
      };
      
      • hooks 通过链表连接在一起。

      • memorizedStatebaseState 的区别,两者的不同之处目前还不得而知,不过每次调用hook 返回的都是memorizedState

      • memorizedStatebaseState 有可能不同吗?

        有可能。

        memorizedState 代表的是上一次hook 返回给组件的 state,而这个 state 有可能是“非法状态”。

        这个“非法状态”会在 updateReducer里面诞生,当遍历 queue 中的 update ,计算新 state 的时候,有可能发生 update.expirationTime < renderExpirationTime 的情况。此时hook 会选择不去执行 update.action ,非法状态就就诞生了。

        当然,为了之后修正当前的这个非法状态,hook 就把 baseStatebaseUpdate 置成了第一次skip 时的 state 和 update,某个时刻重新从 baseStatebaseUpdate 计算真正合法的 state。

        这是为了在资源不足的情况下优先完成高优先级任务。

        不管怎么说,在资源和时间充足的情况下,从 baseStatebaseUpdate 重新追赶后就能得到正确的state。

      • 为什么hook 的queue 有时是循环链表,有时不是?

        参照后面的更新状态callback 具体实现的section。

      Update

      type Update<S, A> = {
        expirationTime: ExpirationTime,
        suspenseConfig: null | SuspenseConfig,
        action: A,
        eagerReducer: ((S, A) => S) | null,
        eagerState: S | null,
        next: Update<S, A> | null,
      
        priority?: ReactPriorityLevel,
      };
      
      • updates 也是通过链表连接在一起的。

      • update 有 expirationTime suspenseConfig priority 各种配置参数。

      • expirationTime 的具体作用?

        不知道。

      • 啥是 suspenseConfig

        不知道。

      • 为啥要有 eagerReducer ,和传入 updateReducer() 中的 reducer 参数互相区分?

        不知道。

      UpdateQueue

      type UpdateQueue<S, A> = {
        last: Update<S, A> | null,
        dispatch: (A => mixed) | null,
        lastRenderedReducer: ((S, A) => S) | null,
        lastRenderedState: S | null,
      };
      
      • dispatch 被封装过,供外界调用使用。所有的hook 都对同一个公共的 dispatch 进行了封装。

      • queue 里面为什么还要特意封装一下 lastRenderedReducerlastRenderedState ?用hook 自带的一些信息不好吗?

        不知道。

      HooksDispatcherOnMount & HooksDispatcherOnUpdate

      const HooksDispatcherOnMount: Dispatcher = {
        readContext,
      
        useCallback: mountCallback,
        useContext: readContext,
        useEffect: mountEffect,
        useImperativeHandle: mountImperativeHandle,
        useLayoutEffect: mountLayoutEffect,
        useMemo: mountMemo,
        useReducer: mountReducer,
        useRef: mountRef,
        useState: mountState,
        useDebugValue: mountDebugValue,
        useResponder: createResponderListener,
      };
      
      const HooksDispatcherOnUpdate: Dispatcher = {
        readContext,
      
        useCallback: updateCallback,
        useContext: readContext,
        useEffect: updateEffect,
        useImperativeHandle: updateImperativeHandle,
        useLayoutEffect: updateLayoutEffect,
        useMemo: updateMemo,
        useReducer: updateReducer,
        useRef: updateRef,
        useState: updateState,
        useDebugValue: updateDebugValue,
        useResponder: createResponderListener,
      };
      

      第一次mount 组件时用mount 系列的hooks 来执行相关的准备、创建操作;之后update 组件用的是update 系列的hooks ,根据之前的状态和操作来返回新的状态。

      renderWithHooks() 里面有一句:

      nextCurrentHook = current !== null ? current.memoizedState: null;
      
      ReactCurrentDispatcher.current = 
        nextCurrentHook === null
          ? HooksDispatcherOnMount
          : HooksDispatcherOnUpdate;
      

      其中 current 是传入 renderWithHooks() 的第一个参数,是一个fiber,若 current === null 则目前没有fiber,是首次加载。

  • 首次加载时,hooks 相关的代码都做了什么?

    • 具体创建hook 的代码是什么逻辑?

      function mountWorkInProgressHook(): Hook {
        const hook: Hook = {
          memoizedState: null,
      
          baseState: null,
          queue: null,
          baseUpdate: null,
      
          next: null,
        };
      
        if (workInProgressHook === null) {
          firstWorkInProgressHook = workInProgressHook = hook;
        } else {
          workInProgressHook = workInProgressHook.next = hook;
        }
        return workInProgressHook;
      }
      

      创建一个hook,并将其设置为全局变量 workInProgressHook ,同时维护一个hooks 链表,每次都把新创建的hook 放在这个链表的最后。

  • useStateuseReducer 系列的hooks 可以返回一个更改hooks 状态的callback,这里的状态更新是咋实现的,有没有race condition?

    useState 其实就是特殊情况下的 useReducer ,两者返回的callback 其实就是首次加载中bind 到 hook.queue.dispatch 上的dispatch 函数。

    • 状态更新就是把传入的action 给添加到queue 的最后,并发出一个请求re-render 的事件。

    • 当然可能存在race condition,这个时候就创建一个key 为queue 的map,缓存一下在re-render 期间发起的action,如果race condition 被引发多次,就把actions 串成一个链表。

      • 除此之外,根据这个文件中的说明,fiber 级别还会有对race condition 的处理,当进行re-render 时,update 链表会分别在current pointer 和work-in-progress pointer 里面被维护,这两个pointer 我理解被存在了不同的fiber 里面。每当有一个新update 事件被添加时,这个update 会被同时添加进这两个pointer or fiber 里,以防止在某些race condition 下被添加的新update 事件丢失。
    • 把action 添加到queue 的操作是咋样的,这个queue 是单链表对吗?

      不是单链表,当 queue.last === null 的时候,被创建出的是一个循环链表。

      至于为什么是一个循环链表,是因为 queue.last === null 这个条件只会在mount 的时候发生,mount or commit 之后的re-render 是要从第一个update 开始的,考虑到:

      • 循环链表的 last.next 就是链表的头节点。
      • 除了第一次re-render 后,之后的更新都是从最后一次update 之后的 update 开始更新了,那么寻求的就是update.next。

      我觉得使用循环链表可以达到以下目的:

      • 少存一个头节点,只有mount or commit 前才需要头节点,这个时候从 last.next 取就好了,其他情况也不需要头节点,此时queue 就不是循环链表了。
      • 范式的统一。
  • react render 的时候是怎么知道此时是mount 还是update?

    根据当前fiber 是否为null 来判断。

  • 在首次加载后,再次调用hooks 发生了什么?

    首次加载之后的hooks 就是update 系列的hooks 了,再拿 useStateuseReducer 来看, useState 会直接调用 updateReducer ,而首次加载后的 useReducer 就是 updateReducer

    updateReducer 被调用时,其会判断当前是否处于re-render 阶段,如果是,按我们上面说的一样,去找key 为queue 的map,取出update 事件链表,计算出新的state。

    如果不是re-render 阶段,那么就遍历queue 里面的update ,计算出新的state。

    不过这里要注意一下特殊情况,会有资源不足而跳过一些update 的情况,参照上文*memorizedStatebaseState 有可能不同吗?* 这个问题。

    • 调用hook 后,具体被操作的hook 是怎么被拿到的?

      function updateWorkInProgressHook(): Hook {
        if (nextWorkInProgressHook !== null) {
          // There's already a work-in-progress. Reuse it.
          workInProgressHook = nextWorkInProgressHook;
          nextWorkInProgressHook = workInProgressHook.next;
      
          currentHook = nextCurrentHook;
          nextCurrentHook = currentHook !== null ? currentHook.next : null;
        } else {
          // Clone from the current hook.
          currentHook = nextCurrentHook;
      
          const newHook: Hook = {
            memoizedState: currentHook.memoizedState,
      
            baseState: currentHook.baseState,
            queue: currentHook.queue,
            baseUpdate: currentHook.baseUpdate,
      
            next: null,
          };
      
          if (workInProgressHook === null) {
            workInProgressHook = firstWorkInProgressHook = newHook;
          } else {
            workInProgressHook = workInProgressHook.next = newHook;
          }
          nextCurrentHook = currentHook.next;
        }
        return workInProgressHook;
      }
      

      先看一看有没有workInProgressHook,如果有的话,直接使用,如果没有,从之前留着的hooks 链表里面拷贝需要的hook 出来,这样的结果就是每次re-render 完之后,都多了一个新版本的hooks 链表,我猜这里可能和fiber 涉及到的调解算法有关。

  • useEffect 的实现?

    function commitHookEffectList(
      unmountTag: number,
      mountTag: number,
      finishedWork: Fiber,
    ) {
      const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQuere: any);
      let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
          if ((effect.tag & unmountTag) !== NoHookEffect) {
            // Unmount
            const destory = effect.destroy;
            effect.destory = undefined;
            if (destory !== undefined) {
              destory();
            }
          }
    	    if ((effect.tag & mountTag) !== NoHookEffect) {
    	      // Mount
            const create = effect.create;
            effect.destory = create();
    	    }
          effect = effect.next;
        } whlie (effect !== firstEffect);
      }
    }
    

    这里可以发现,effect 的链表始终是循环链表,因为effect 一旦mount 了之后数量就是恒定的,永远不会受到其他事件的影响,所以每次从 lastEffect.next 走起就好了。

  • useStateuseEffect 有何不同?

    • useState 是在fiber 里面创建初始状态,然后返回一个callback 让用户来schedule 新一轮的re-render。
    • useEffect 是在fiber 里面创建初始状态,然后在每次fiber commit 之后(render 完成之后),依次执行一些副作用操作。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umykITuK-1644039451267)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7d16a0c5-e596-41a7-b913-d228b5dd19db/Untitled.png)]

  • update 的expirationTime 是何时计算的?有何作用?

    是在创建update 的时候就决定的,用来帮助fiber 确定update 的优先级,fiber 因为其充分利用device 资源的特性,会根据当前的资源来决定是否进行当前的update,如果资源不足,则跳过,记录被跳过的第一个update 为 baseUpdate 待之后又获得资源后从 baseUpdate 重新更新。

  • talk about fiber

    这可能是最通俗的 React Fiber(时间分片) 打开方式

    • 啥是fiber

      解读一:fiber 相当于是react 中的协程。因为协程之间是主动协调任务的,所以其实react 无法对fiber 进行抢占式调度,全凭使用者的自觉,一般fiber 会主动检查是否超过了浏览器给的运行时间,如果超过就保存现场,把控制权还给浏览器(一般来说这个运行时间是16ms,1000ms / 60)。

      解读二:react 中的执行单元,每执行完一个fiber 就检查一下剩余时间。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NzUEiDLE-1644039451268)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3fc0f219-6bea-4400-a886-60e57ae173be/Untitled.png)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值