# React Hooks在SD-WAN项目中实践

# 前言

React Hooks是React16新出的基于函数式组件的一组新的api,其不同于之前class组件的内层嵌套方式,利用hooks进行钩子方式的对数据进行了组件间的流向组织,sdwan项目中都是基于函数式组件的封装,本文为sdwan项目中的react hooks的应用实践

# 目录

  • 添加警告规则弹窗组件实践
  • React Hooks源码解读
  • React Fiber数据结构分析

# 探索案例

# 添加警告规则弹窗组件实践

addRule

[组件目录]

  • components
    • addRule.jsx
    • RuleList.jsx
  • index.jsx
  • index.less

[目录描述] addRule是点击弹窗后弹出的主体组件

[源码分析] addRule是添加规则的弹窗,其中在告警规则一栏中,需要对列表中的行进行加减操作,这里最先想到的就是利用useState进行数据的管理,但其实useState是useReducer的语法糖,后续源码中会分析,我们看到使用了useState后可以将所有状态抽离到顶部,后续凡是需要使用trNum或setTrNum的便可以直接使用,这样就省去了在setState中的设置以及对相应this的绑定问题,使得数据的操作更加纯粹而且明晰

const AddRule = (props) => {
  const { children, title } = props;
  ......

  const [trNum, setTrNum] = useState(1);

  const trLoop = (n) => {
    let arr = [];
    for(let i=0; i< n; i++) {
      arr.push(
        <tr>
          <td>
            <Select
              placeholder='请选择'
              defaultValue='0'
              style={{width:'120px'}}
            >
                {options.params.map((d) => (
                    <Select.Option value={d.status} key={d.status}>
                    {d.text}
                    </Select.Option>
                ))}
            </Select>
          </td>
          <td>
            <Select
              placeholder='请选择'
              defaultValue='0'
            >
                {options.compare.map((d) => (
                    <Select.Option value={d.status} key={d.status}>
                    {d.text}
                    </Select.Option>
                ))}
            </Select>
          </td>
          <td>
            <Select
              placeholder='请选择'
              defaultValue={currentType}
              onChange={val => setTypeValue(val)}
            >
                {options.type.map((d) => (
                    <Select.Option value={d.status} key={d.status}>
                    {d.text}
                    </Select.Option>
                ))}
            </Select>
          </td>
          <td>
            { typeValue == options.type[1].status 
            ? 
              <span style={{display: 'inline-flex', verticalAlign: 'middle', lineHeight: '32px', width: '120px'}}>
                <Input placeholder=""/>dBm
              </span>
            : 
              <Select
                placeholder='请选择'
                defaultValue='0'
                style={{width:'120px'}}
              >
                  {options.params.map((d) => (
                      <Select.Option value={d.status} key={d.status}>
                      {d.text}
                      </Select.Option>
                  ))}
              </Select>
            }
          </td>
          <td>
            <PlusOutlined style={{color: '#1890ff'}} onClick={()=>setTrNum(trNum + 1)}/>
          </td>
          <td>
            <CloseOutlined style={{color: '#ff4d4f'}} onClick={()=> trNum>1 && setTrNum(trNum - 1)}/>
          </td>
        </tr>
      )
    };
    return arr;
  };

  ......

  return (
    <>
      <span onClick={showModelHandler}>{children}</span>
      <Modal
        title={title}
        visible={visible}
        onCancel={hideModelHandler}
        onOk={handleOk}
        maskClosable={false}
        destroyOnClose
      >
        <Form form={form} layout="vertical">
          ......
          <Form.Item name="告警规则" label="告警规则">
              <div style={{width: '100%', backgroundColor: '#ececec', padding: '10px'}}>
                <span>
                  符合以下&nbsp;<Select
                    placeholder='请选择'
                    defaultValue='0'
                    style={{width: '120px'}}
                  >
                      {options.rule.map((d) => (
                          <Select.Option value={d.status} key={d.status}>
                          {d.text}
                          </Select.Option>
                      ))}
                  </Select>&nbsp;条件:
                </span>
                <div 
                  style={{
                    border: '1px solid #ccc',
                    width: '100%', 
                    background: '#fff', 
                    marginTop: '10px', 
                    padding: '4px'
                  }}
                >
                  <table >
                    <tbody >
                      { trLoop(trNum) }
                    </tbody>
                  </table>
                </div>
              </div>
          </Form.Item>
          ......
        </Form>
      </Modal>
    </>
  );
};

# React Hooks源码解读

hooks01

[组件目录]

  • packages
    • react
      • src
        • ReactHooks.js

这里仅仅是做了一个名称的导出包括:

  • useContext
  • useState
  • useReducer
  • useRef
  • useEffect
  • useLayoutEffect
  • useCallback
  • useMemo
  • useImperativeHandles
  • useDebugValue
  • useTransition
  • useDeferredValue
  • useOpaqueIdentifier
  • useMutableSource

这里真正的源码是放在了packages/react-reconciler/src/ReactFiberHooks.js里,可以看出其利用的仍然是React的核心数据结构Fiber的调度作用

hooks02

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;

  if (__DEV__) {
    hookTypesDev =
      current !== null
        ? ((current._debugHookTypes: any): Array<HookType>)
        : null;
    hookTypesUpdateIndexDev = -1;
    // Used for hot reloading:
    ignorePreviousDependencies =
      current !== null && current.type !== workInProgress.type;
  }

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

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;

  // didScheduleRenderPhaseUpdate = false;

  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because memoizedState === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)

  // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so memoizedState would be null during updates and mounts.
  if (__DEV__) {
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      // This dispatcher handles an edge case where a component is updating,
      // but no stateful hooks have been used.
      // We want to match the production code behavior (which will use HooksDispatcherOnMount),
      // but with the extra DEV validation to ensure hooks ordering hasn't changed.
      // This dispatcher does that.
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

  let children = Component(props, secondArg);

  // Check if there was a render phase update
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      invariant(
        numberOfReRenders < RE_RENDER_LIMIT,
        'Too many re-renders. React limits the number of renders to prevent ' +
          'an infinite loop.',
      );

      numberOfReRenders += 1;
      if (__DEV__) {
        // Even when hot reloading, allow dependencies to stabilize
        // after first render to prevent infinite render phase updates.
        ignorePreviousDependencies = false;
      }

      // Start over from the beginning of the list
      currentHook = null;
      workInProgressHook = null;

      workInProgress.updateQueue = null;

      if (__DEV__) {
        // Also validate hook order for cascading updates.
        hookTypesUpdateIndexDev = -1;
      }

      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnRerenderInDEV
        : HooksDispatcherOnRerender;

      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }

  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there's no re-entrancy.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  if (__DEV__) {
    workInProgress._debugHookTypes = hookTypesDev;
  }

  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;

  if (__DEV__) {
    currentHookNameInDev = null;
    hookTypesDev = null;
    hookTypesUpdateIndexDev = -1;
  }

  didScheduleRenderPhaseUpdate = false;

  invariant(
    !didRenderTooFewHooks,
    'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',
  );

  return children;
}

从中抽离出核心的hooks渲染,其他的具体的use方法可以在其上进行扩展,可以看出其实质是是基于Fiber的workInProgress的全局变量的更改与调度,其中包含记录当前hook状态的memoizedState以及需要更新的队列updateQueue,hooks的队列通过memoizedState及next构成了一个链表,整个hook的核心是基于Dispatcher的切换hook的调用,这里就涉及到Fiber的整个数据结构,在下一节中进行描述

# React Fiber数据结构分析

hooks03

hooks04

[组件目录]

  • packages
    • react-reconciler
      • src
        • ReactFiber.js

简单来说React的Fiber数据结构是维护了一个如下的数据格式:

Fiber = {
    // 标识 fiber 类型的标签,详情参看下述 WorkTag
    tag: WorkTag,

    // 指向父节点
    return: Fiber | null,

    // 指向子节点
    child: Fiber | null,

    // 指向兄弟节点
    sibling: Fiber | null,

    // 在开始执行时设置 props 值
    pendingProps: any,

    // 在结束时设置的 props 值
    memoizedProps: any,

    // 当前 state
    memoizedState: any,

    // Effect 类型,详情查看以下 effectTag
    effectTag: SideEffectTag,

    // effect 节点指针,指向下一个 effect
    nextEffect: Fiber | null,

    // effect list 是单向链表,第一个 effect
    firstEffect: Fiber | null,

    // effect list 是单向链表,最后一个 effect
    lastEffect: Fiber | null,

    // work 的过期时间,可用于标识一个 work 优先级顺序
    expirationTime: ExpirationTime,
};

lifecycle

该数据结构是一个通过链表实现的树的结构,整个React的阶段可分为Render Phase、Pre-Commit Phase以及Commit Phase,Fiber的设计初衷是利用浏览器渲染过程中剩余的时间碎片来进行render,而要达到这个目的需要能够对渲染过程的工作进行暂停、终止以及复用,Fiber便是利用数据结构实现了这样一个虚拟堆栈帧。

hooks05

这里不再对协调(Reconciliation)和调度(Scheduling)的具体过程,如expirationTime的权重设计、Effect lists的DFS算法设计等进行讲述,有兴趣的同学可以参看这篇文章(React Fiber 源码解析)

基于React Hooks涉及到的workInProgress,我们重点看一下这里的设计

// This is used to create an alternate fiber to do work on.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    // We use a double buffering pooling technique because we know that we'll
    // only ever need at most two versions of a tree. We pool the "other" unused
    // node that we're free to reuse. This is lazily created to avoid allocating
    // extra objects for things that are never updated. It also allow us to
    // reclaim the extra memory if needed.
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    if (__DEV__) {
      // DEV-only fields
      workInProgress._debugID = current._debugID;
      workInProgress._debugSource = current._debugSource;
      workInProgress._debugOwner = current._debugOwner;
      workInProgress._debugHookTypes = current._debugHookTypes;
    }

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    // Needed because Blocks store data on type.
    workInProgress.type = current.type;

    // We already have an alternate.
    workInProgress.subtreeTag = NoSubtreeEffect;
    workInProgress.deletions = null;

    // The effect list is no longer valid.
    workInProgress.nextEffect = null;
    workInProgress.firstEffect = null;
    workInProgress.lastEffect = null;

    if (enableProfilerTimer) {
      // We intentionally reset, rather than copy, actualDuration & actualStartTime.
      // This prevents time from endlessly accumulating in new commits.
      // This has the downside of resetting values for different priority renders,
      // But works for yielding (the common case) and should support resuming.
      workInProgress.actualDuration = 0;
      workInProgress.actualStartTime = -1;
    }
  }

  // Reset all effects except static ones.
  // Static effects are not specific to a render.
  workInProgress.effectTag = current.effectTag & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  // Clone the dependencies object. This is mutated during the render phase, so
  // it cannot be shared with the current fiber.
  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };

  // These will be overridden during the parent's reconciliation
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  if (enableProfilerTimer) {
    workInProgress.selfBaseDuration = current.selfBaseDuration;
    workInProgress.treeBaseDuration = current.treeBaseDuration;
  }

  if (__DEV__) {
    workInProgress._debugNeedsRemount = current._debugNeedsRemount;
    switch (workInProgress.tag) {
      case IndeterminateComponent:
      case FunctionComponent:
      case SimpleMemoComponent:
        workInProgress.type = resolveFunctionForHotReloading(current.type);
        break;
      case ClassComponent:
        workInProgress.type = resolveClassForHotReloading(current.type);
        break;
      case ForwardRef:
        workInProgress.type = resolveForwardRefForHotReloading(current.type);
        break;
      default:
        break;
    }
  }

  return workInProgress;
}

这里涉及到的workInProgress和current两个树通过alternate这个指针的互相指引操作来实现首次渲染和非首次渲染的对比更新,保证两个队列都更新而不会丢失,并且确保更新始终是workInProgress的一部分,这里还做了一个内存缓冲,奇次更新和偶次更新的循环复用

# 总结

通过学习React16关于Fiber源码及React Hooks的源码,我们发现整个React16的底层核心是基于Fiber的优化与扩展,包括dom-diff的扩展等,相较于Vue3对于Vue2的更新,可以看出React的优化迭代思路更加充满对计算机原理底层的思考与发现,当然这两个框架从出发点设计上也是有所不同,Vue是基于组件级的优化,因而并不需要这样一个Fiber的数据结构去构建,但从真正的设计来看Fiber的架构设计思维方式确实更加符合国外程序员的方法与韵味。(ps: 想要了解Andrew Clark介绍Fiber的同学,可以参看这篇文章react-fiber-architecure)

# 参考