前言
React Hooks是React16新出的基于函數(shù)式組件的一組新的api肥印,其不同于之前class組件的內(nèi)層嵌套方式,利用hooks進行鉤子方式的對數(shù)據(jù)進行了組件間的流向組織锥债,sdwan項目中都是基于函數(shù)式組件的封裝曼振,本文為sdwan項目中的react hooks的應用實踐
目錄
- 添加警告規(guī)則彈窗組件實踐
- React Hooks源碼解讀
- React Fiber數(shù)據(jù)結(jié)構分析
探索案例
添加警告規(guī)則彈窗組件實踐
[組件目錄]
components
addRule.jsx
RuleList.jsx
index.jsx
index.less
[目錄描述] addRule是點擊彈窗后彈出的主體組件
[源碼分析] addRule是添加規(guī)則的彈窗,其中在告警規(guī)則一欄中仗嗦,需要對列表中的行進行加減操作,這里最先想到的就是利用useState進行數(shù)據(jù)的管理,但其實useState是useReducer的語法糖虱饿,后續(xù)源碼中會分析,我們看到使用了useState后可以將所有狀態(tài)抽離到頂部悼嫉,后續(xù)凡是需要使用trNum或setTrNum的便可以直接使用解孙,這樣就省去了在setState中的設置以及對相應this的綁定問題,使得數(shù)據(jù)的操作更加純粹而且明晰
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">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="告警規(guī)則" label="告警規(guī)則"> <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> </> ); };</pre>
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的核心數(shù)據(jù)結(jié)構Fiber的調(diào)度作用
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">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; }</pre>
從中抽離出核心的hooks渲染虹蒋,其他的具體的use方法可以在其上進行擴展,可以看出其實質(zhì)是是基于Fiber的workInProgress的全局變量的更改與調(diào)度飒货,其中包含記錄當前hook狀態(tài)的memoizedState以及需要更新的隊列updateQueue魄衅,hooks的隊列通過memoizedState及next構成了一個鏈表,整個hook的核心是基于Dispatcher的切換hook的調(diào)用塘辅,這里就涉及到Fiber的整個數(shù)據(jù)結(jié)構晃虫,在下一節(jié)中進行描述
React Fiber數(shù)據(jù)結(jié)構分析
[組件目錄]
packages
react-reconciler
src
ReactFiber.js
簡單來說React的Fiber數(shù)據(jù)結(jié)構是維護了一個如下的數(shù)據(jù)格式:
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">Fiber = { // 標識 fiber 類型的標簽,詳情參看下述 WorkTag tag: WorkTag, // 指向父節(jié)點 return: Fiber | null, // 指向子節(jié)點 child: Fiber | null, // 指向兄弟節(jié)點 sibling: Fiber | null, // 在開始執(zhí)行時設置 props 值 pendingProps: any, // 在結(jié)束時設置的 props 值 memoizedProps: any, // 當前 state memoizedState: any, // Effect 類型扣墩,詳情查看以下 effectTag effectTag: SideEffectTag, // effect 節(jié)點指針哲银,指向下一個 effect nextEffect: Fiber | null, // effect list 是單向鏈表扛吞,第一個 effect firstEffect: Fiber | null, // effect list 是單向鏈表,最后一個 effect lastEffect: Fiber | null, // work 的過期時間荆责,可用于標識一個 work 優(yōu)先級順序 expirationTime: ExpirationTime, };</pre>
該數(shù)據(jù)結(jié)構是一個通過鏈表實現(xiàn)的樹的結(jié)構滥比,整個React的階段可分為Render Phase、Pre-Commit Phase以及Commit Phase做院,F(xiàn)iber的設計初衷是利用瀏覽器渲染過程中剩余的時間碎片來進行render盲泛,而要達到這個目的需要能夠?qū)︿秩具^程的工作進行暫停、終止以及復用键耕,F(xiàn)iber便是利用數(shù)據(jù)結(jié)構實現(xiàn)了這樣一個虛擬堆棧幀查乒。
這里不再對協(xié)調(diào)(Reconciliation)和調(diào)度(Scheduling)的具體過程,如expirationTime的權重設計郁竟、Effect lists的DFS算法設計等進行講述玛迄,有興趣的同學可以參看這篇文章(React Fiber 源碼解析)
基于React Hooks涉及到的workInProgress,我們重點看一下這里的設計
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">// 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; }</pre>
這里涉及到的workInProgress和current兩個樹通過alternate這個指針的互相指引操作來實現(xiàn)首次渲染和非首次渲染的對比更新棚亩,保證兩個隊列都更新而不會丟失蓖议,并且確保更新始終是workInProgress的一部分,這里還做了一個內(nèi)存緩沖讥蟆,奇次更新和偶次更新的循環(huán)復用
總結(jié)
通過學習React16關于Fiber源碼及React Hooks的源碼勒虾,我們發(fā)現(xiàn)整個React16的底層核心是基于Fiber的優(yōu)化與擴展,包括dom-diff的擴展等瘸彤,相較于Vue3對于Vue2的更新修然,可以看出React的優(yōu)化迭代思路更加充滿對計算機原理底層的思考與發(fā)現(xiàn),當然這兩個框架從出發(fā)點設計上也是有所不同质况,Vue是基于組件級的優(yōu)化愕宋,因而并不需要這樣一個Fiber的數(shù)據(jù)結(jié)構去構建,但從真正的設計來看Fiber的架構設計思維方式確實更加符合國外程序員的方法與韻味结榄。(ps: 想要了解Andrew Clark介紹Fiber的同學中贝,可以參看這篇文章react-fiber-architecure)