前言
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() }; return arr; }; ...... return ( <> {children} { typeValue == options.type[1].status ? dBm : } setTrNum(trNum + 1)}/> trNum>1 && setTrNum(trNum - 1)}/> > ); }; ...... 符合以下 条件:{ trLoop(trNum) }
React Hooks源码解读
[组件目录]
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的调度作用
export function renderWithHooks( 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 ) : 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
简单来说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)
参考
- Hook 简介
- 【第2044期】React Hooks 设计思想
- 【第2037期】React Hooks 实践指南
- ReactHooks源码解析之useEffect
- react hooks 源码分析 --- useState
- 剖析React Hooks底层源码
- React Hook 的体系设计之一 - 分层
- React Hooks 的体系设计之二 - 状态粒度
- React Hooks 的体系设计之三 - 什么是 ref
- React Hooks 源码解析(译)
- [译]深入React fiber架构及源码
- React Fiber 源码解析