React Hooks

Posted on 2020年6月26日周五 技术

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

💡

给一点take away 吧,说一说对我帮助最大的两个心得: initialState 的改变不会影响hooks 返回结果的改变,因为mount 前后的hooks 函数根本就是两套。 多个 useEffect 分别set 一下state,dispatch 一下action 啥的其实不一定会引起多次渲染,因为hook 会用 renderPhaseUpdates 来判断race condition,如果优化够好的话多个 useEffect 和单个的效果应该差不多。

两篇很有用的文章

React Hooks 源码解析(3):useState

在写本文之前,事先阅读了网上了一些文章,关于 Hooks 的源码解析要么过于浅显、要么就不细致,所以本文着重讲解源码,由浅入深,争取一行代码也不放过。那本系列讲解第一个 Hooks 便是 useState,我们将从 useState 的用法开始,再阐述规则、讲解原理,再简单实现,最后源码解析。另外,在本篇开头,再补充一个 Hooks 的概述,前两篇·限于篇幅问题一直没有写一块。 注:距离上篇文章已经过去了两个月,这两个月业务繁忙所以没有什么时间更新该系列的文章,但 react 这两个月却从 16.9 更新到了 16.11,review 了一下这几次的更新都未涉及到 hooks,所以我也直接把源码笔记这块更新到了 16.11。 Hook 是 React 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。其本质上就是一类特殊的函数,它们约定以 use 开头,可以为 Function Component 注入一些功能,赋予 Function Component 一些 Class Component 所具备的能力。 例如,原本我们说 Function Component 无法保存状态,所以我们经常说 Stateless Function Component,但是现在我们借助 useState 这个

Hooks 有什么好处?

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

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

简单来说,一个fiber 对应一个hooks 链表,一个hook 对应state 和update queue。

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

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 相关的代码都做了什么?
💡

首次加载时调用的是mount 系列hooks,根据hooks 代码的调用创建对应的hooks,并为这些被创建的hooks 分别创建queue,为被创建的queue bind dispatch 函数。

具体创建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 完成之后),依次执行一些副作用操作。

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

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

talk about fiber
啥是fiber
💡

React 中的fiber 协调算法的执行单元。

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

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