# React Hooks在SD-WAN项目中实践
# 前言
React Hooks是React16新出的基于函数式组件的一组新的api,其不同于之前class组件的内层嵌套方式,利用hooks进行钩子方式的对数据进行了组件间的流向组织,sdwan项目中都是基于函数式组件的封装,本文为sdwan项目中的react hooks的应用实践
# 目录
- 添加警告规则弹窗组件实践
- React Hooks源码解读
- React Fiber数据结构分析
# 探索案例
# 添加警告规则弹窗组件实践
[组件目录]
- 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>
符合以下 <Select
placeholder='请选择'
defaultValue='0'
style={{width: '120px'}}
>
{options.rule.map((d) => (
<Select.Option value={d.status} key={d.status}>
{d.text}
</Select.Option>
))}
</Select> 条件:
</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源码解读
[组件目录]
- packages
- react
- src
- ReactHooks.js
- src
- react
这里仅仅是做了一个名称的导出包括:
- useContext
- useState
- useReducer
- useRef
- useEffect
- useLayoutEffect
- useCallback
- useMemo
- useImperativeHandles
- useDebugValue
- useTransition
- useDeferredValue
- useOpaqueIdentifier
- useMutableSource
这里真正的源码是放在了packages/react-reconciler/src/ReactFiberHooks.js里,可以看出其利用的仍然是React的核心数据结构Fiber的调度作用
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数据结构分析
[组件目录]
- packages
- react-reconciler
- src
- ReactFiber.js
- src
- react-reconciler
简单来说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,
};
该数据结构是一个通过链表实现的树的结构,整个React的阶段可分为Render Phase、Pre-Commit Phase以及Commit Phase,Fiber的设计初衷是利用浏览器渲染过程中剩余的时间碎片来进行render,而要达到这个目的需要能够对渲染过程的工作进行暂停、终止以及复用,Fiber便是利用数据结构实现了这样一个虚拟堆栈帧。
这里不再对协调(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)